diff --git a/beestation.dme b/beestation.dme index ea82cb06cf4aa..d4abc6470f3b1 100644 --- a/beestation.dme +++ b/beestation.dme @@ -40,6 +40,7 @@ #include "code\__DEFINES\areas.dm" #include "code\__DEFINES\armor.dm" #include "code\__DEFINES\art.dm" +#include "code\__DEFINES\assemblies.dm" #include "code\__DEFINES\assets.dm" #include "code\__DEFINES\async.dm" #include "code\__DEFINES\atmospherics.dm" diff --git a/code/__DEFINES/assemblies.dm b/code/__DEFINES/assemblies.dm new file mode 100644 index 0000000000000..45eeb611fae8e --- /dev/null +++ b/code/__DEFINES/assemblies.dm @@ -0,0 +1,8 @@ +/// When combined in a holder, blacklists duplicate assemblies +#define ASSEMBLY_NO_DUPLICATES (1<<0) + +/// How loud do assemblies beep at +#define ASSEMBLY_BEEP_VOLUME 5 + +/// The max amount of assemblies attachable on an assembly holder +#define HOLDER_MAX_ASSEMBLIES 2 diff --git a/code/__DEFINES/cooldowns.dm b/code/__DEFINES/cooldowns.dm index 31a92638dc460..4a0502d319458 100644 --- a/code/__DEFINES/cooldowns.dm +++ b/code/__DEFINES/cooldowns.dm @@ -38,6 +38,9 @@ #define COOLDOWN_CIRCUIT_PATHFIND_DIF "circuit_pathfind_different" #define COOLDOWN_CIRCUIT_TARGET_INTERCEPT "circuit_target_intercept" +//Item cooldowns +#define COOLDOWN_SIGNALLER_SEND "cooldown_signaller_send" + //Mecha cooldowns #define COOLDOWN_MECHA_MESSAGE "mecha_message" #define COOLDOWN_MECHA_EQUIPMENT "mecha_equipment" diff --git a/code/__DEFINES/dcs/signals/signals_obj/signals_item/signals_item.dm b/code/__DEFINES/dcs/signals/signals_obj/signals_item/signals_item.dm index 1e123e41a5d4f..7feed06591d9a 100644 --- a/code/__DEFINES/dcs/signals/signals_obj/signals_item/signals_item.dm +++ b/code/__DEFINES/dcs/signals/signals_obj/signals_item/signals_item.dm @@ -82,3 +82,18 @@ /// Tell a deployable item to force its deployment (datum/source, atom/location) #define COMSIG_DEPLOYABLE_FORCE_DEPLOY "force_deploy" #define DEPLOYMENT_SUCCESS (1 << 0) //Indicates that something was successfully deployed + +#define COMSIG_IGNITER_ACTIVATE "ignite_activate" //called when an igniter activates + +/// Called before beam is redrawn +#define COMSIG_BEAM_BEFORE_DRAW "beam_before_draw" + #define BEAM_CANCEL_DRAW (1 << 0) + +/// Sent to a beam when an atom enters any turf the beam covers: (obj/effect/ebeam/hit_beam, atom/movable/entered) +#define COMSIG_BEAM_ENTERED "beam_entered" + +/// Sent to a beam when an atom exits any turf the beam covers: (obj/effect/ebeam/hit_beam, atom/movable/exited) +#define COMSIG_BEAM_EXITED "beam_exited" + +/// Sent to a beam when any turf the beam covers changes: (list/datum/callback/post_change_callbacks) +#define COMSIG_BEAM_TURFS_CHANGED "beam_turfs_changed" diff --git a/code/__DEFINES/dcs/signals/signals_obj/signals_object.dm b/code/__DEFINES/dcs/signals/signals_obj/signals_object.dm index a8f84509c1816..c3eb6af24a09d 100644 --- a/code/__DEFINES/dcs/signals/signals_obj/signals_object.dm +++ b/code/__DEFINES/dcs/signals/signals_obj/signals_object.dm @@ -32,3 +32,5 @@ ///Called when a payment component changes value #define COMSIG_OBJ_ATTEMPT_CHARGE_CHANGE "obj_attempt_simple_charge_change" +///from /obj/item/assembly/proc/pulsed(mob/pulser) +#define COMSIG_ASSEMBLY_PULSED "assembly_pulsed" diff --git a/code/__DEFINES/logging.dm b/code/__DEFINES/logging.dm index 69b78270635b4..b4d9de4343f4d 100644 --- a/code/__DEFINES/logging.dm +++ b/code/__DEFINES/logging.dm @@ -22,6 +22,9 @@ #define INVESTIGATE_VERB_DROPPED "dropped" #define INVESTIGATE_VERB_EQUIPPED "equipped" +// The maximum number of entries allowed in the signaler investigate log, keep this relatively small to prevent performance issues when an admin tries to query it +#define INVESTIGATE_SIGNALER_LOG_MAX_LENGTH 500 + // Logging types for log_message() #define LOG_ATTACK (1 << 0) #define LOG_SAY (1 << 1) diff --git a/code/__DEFINES/machines.dm b/code/__DEFINES/machines.dm index 18c1b7f0fbf3a..7e1b5088279d5 100644 --- a/code/__DEFINES/machines.dm +++ b/code/__DEFINES/machines.dm @@ -214,7 +214,6 @@ GLOBAL_LIST_INIT(approved_status_pictures, list( #define WIRE_PULSE_SPECIAL (1<<2) #define WIRE_RADIO_RECEIVE (1<<3) #define WIRE_RADIO_PULSE (1<<4) -#define ASSEMBLY_BEEP_VOLUME 5 // Camera defines // --------------------------------------------------- diff --git a/code/__HELPERS/maths.dm b/code/__HELPERS/maths.dm index 52a88ef88dda2..ae404c8ca3491 100644 --- a/code/__HELPERS/maths.dm +++ b/code/__HELPERS/maths.dm @@ -14,6 +14,28 @@ else if(dx < 0) . += 360 +/// Calculate the angle produced by a pair of x and y deltas +/proc/delta_to_angle(x, y) + if(!y) + return (x >= 0) ? 90 : 270 + . = arctan(x/y) + if(y < 0) + . += 180 + else if(x < 0) + . += 360 + +/// Angle between two arbitrary points and horizontal line same as [/proc/get_angle] +/proc/get_angle_raw(start_x, start_y, start_pixel_x, start_pixel_y, end_x, end_y, end_pixel_x, end_pixel_y) + var/dy = (32 * end_y + end_pixel_y) - (32 * start_y + start_pixel_y) + var/dx = (32 * end_x + end_pixel_x) - (32 * start_x + start_pixel_x) + if(!dy) + return (dx >= 0) ? 90 : 270 + . = arctan(dx/dy) + if(dy < 0) + . += 180 + else if(dx < 0) + . += 360 + ///for getting the angle when animating something's pixel_x and pixel_y /proc/get_pixel_angle(y, x) if(!y) @@ -70,52 +92,6 @@ line += locate(current_x_step, current_y_step, starting_z) return line -/** - * Get a list of turfs in a line from `starting_atom` to `ending_atom`. - * - * Uses the ultra-fast [Bresenham Line-Drawing Algorithm](https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm). - */ -/proc/get_line(atom/starting_atom, atom/ending_atom) - var/current_x_step = starting_atom.x//start at x and y, then add 1 or -1 to these to get every turf from starting_atom to ending_atom - var/current_y_step = starting_atom.y - var/starting_z = starting_atom.z - - var/list/line = list(get_turf(starting_atom))//get_turf(atom) is faster than locate(x, y, z) - - var/x_distance = ending_atom.x - current_x_step //x distance - var/y_distance = ending_atom.y - current_y_step - - var/abs_x_distance = abs(x_distance)//Absolute value of x distance - var/abs_y_distance = abs(y_distance) - - var/x_distance_sign = SIGN(x_distance) //Sign of x distance (+ or -) - var/y_distance_sign = SIGN(y_distance) - - var/x = abs_x_distance >> 1 //Counters for steps taken, setting to distance/2 - var/y = abs_y_distance >> 1 //Bit-shifting makes me l33t. It also makes get_line() unnessecarrily fast. - - if(abs_x_distance >= abs_y_distance) //x distance is greater than y - for(var/distance_counter in 0 to (abs_x_distance - 1))//It'll take abs_x_distance steps to get there - y += abs_y_distance - - if(y >= abs_x_distance) //Every abs_y_distance steps, step once in y direction - y -= abs_x_distance - current_y_step += y_distance_sign - - current_x_step += x_distance_sign //Step on in x direction - line += locate(current_x_step, current_y_step, starting_z)//Add the turf to the list - else - for(var/distance_counter in 0 to (abs_y_distance - 1)) - x += abs_x_distance - - if(x >= abs_y_distance) - x -= abs_y_distance - current_x_step += x_distance_sign - - current_y_step += y_distance_sign - line += locate(current_x_step, current_y_step, starting_z) - return line - //chances are 1:value. anyprob(1) will always return true /proc/anyprob(value) return (rand(1,value)==value) diff --git a/code/_globalvars/logging.dm b/code/_globalvars/logging.dm index d706b5c99c5d1..09a4a668b2f52 100644 --- a/code/_globalvars/logging.dm +++ b/code/_globalvars/logging.dm @@ -88,6 +88,15 @@ GLOBAL_PROTECT(admin_log) GLOBAL_LIST_EMPTY(lastsignalers) //! keeps last 100 signals here in format: "[src] used [REF(src)] @ location [src.loc]: [freq]/[code]" GLOBAL_PROTECT(lastsignalers) +/// Used to add a text log to the signaler investigation log. +/// Do not add to the list directly; if the list is too large it can cause lag when an admin tries to view it. +/proc/add_to_signaler_investigate_log(text) + var/log_length = length(GLOB.lastsignalers) + if(log_length >= INVESTIGATE_SIGNALER_LOG_MAX_LENGTH) + GLOB.lastsignalers = GLOB.lastsignalers.Copy((INVESTIGATE_SIGNALER_LOG_MAX_LENGTH - log_length) + 2) + GLOB.lastsignalers += list(text) + + GLOBAL_LIST_EMPTY(lawchanges) //! Stores who uploaded laws to which silicon-based lifeform, and what the law was GLOBAL_PROTECT(lawchanges) diff --git a/code/datums/beam.dm b/code/datums/beam.dm index 13535f0dc5bca..117eac2b758b9 100644 --- a/code/datums/beam.dm +++ b/code/datums/beam.dm @@ -27,14 +27,50 @@ var/beam_type = /obj/effect/ebeam ///This is used as the visual_contents of beams, so you can apply one effect to this and the whole beam will look like that. never gets deleted on redrawing. var/obj/effect/ebeam/visuals + ///The color of the beam we're drawing. + var/beam_color + ///If we use an emissive appearance + var/emissive = TRUE + /// If set will be used instead of origin's pixel_x in offset calculations + var/override_origin_pixel_x = null + /// If set will be used instead of origin's pixel_y in offset calculations + var/override_origin_pixel_y = null + /// If set will be used instead of targets's pixel_x in offset calculations + var/override_target_pixel_x = null + /// If set will be used instead of targets's pixel_y in offset calculations + var/override_target_pixel_y = null + ///the layer of our beam + var/beam_layer -/datum/beam/New(beam_origin, beam_target, beam_icon='icons/effects/beam.dmi', beam_icon_state="b_beam", time=INFINITY, maxdistance=INFINITY, btype = /obj/effect/ebeam) - origin = beam_origin - target = beam_target - max_distance = maxdistance - icon = beam_icon - icon_state = beam_icon_state - beam_type = btype +/datum/beam/New( + origin, + target, + icon = 'icons/effects/beam.dmi', + icon_state = "b_beam", + time = INFINITY, + max_distance = INFINITY, + beam_type = /obj/effect/ebeam, + beam_color = null, + emissive = TRUE, + override_origin_pixel_x = null, + override_origin_pixel_y = null, + override_target_pixel_x = null, + override_target_pixel_y = null, + beam_layer = ABOVE_ALL_MOB_LAYER +) + src.origin = origin + src.target = target + src.icon = icon + src.icon_state = icon_state + src.max_distance = max_distance + src.beam_type = beam_type + src.beam_color = beam_color + src.emissive = emissive + src.override_origin_pixel_x = override_origin_pixel_x + src.override_origin_pixel_y = override_origin_pixel_y + src.override_target_pixel_x = override_target_pixel_x + src.override_target_pixel_y = override_target_pixel_y + src.beam_layer = beam_layer if(time < INFINITY) QDEL_IN(src, time) @@ -45,9 +81,14 @@ visuals = new beam_type() visuals.icon = icon visuals.icon_state = icon_state + visuals.color = beam_color + visuals.vis_flags = VIS_INHERIT_PLANE|VIS_INHERIT_LAYER + visuals.emissive = emissive + visuals.layer = beam_layer + visuals.update_appearance() Draw() - RegisterSignal(origin, COMSIG_MOVABLE_MOVED, PROC_REF(redrawing)) - RegisterSignal(target, COMSIG_MOVABLE_MOVED, PROC_REF(redrawing)) + RegisterSignal(origin, COMSIG_MOVABLE_MOVED, PROC_REF(redrawing), override = TRUE) + RegisterSignal(target, COMSIG_MOVABLE_MOVED, PROC_REF(redrawing), override = TRUE) /** * Triggered by signals set up when the beam is set up. If it's still sane to create a beam, it removes the old beam, creates a new one. Otherwise it kills the beam. @@ -58,15 +99,16 @@ * direction: in what direction mover moved from. */ /datum/beam/proc/redrawing(atom/movable/mover, atom/oldloc, direction) - if(origin && target && get_dist(origin,target)length) //went past the target, we draw a box of space to cut away from the beam sprite so the icon actually ends at the center of the target sprite var/icon/II = new(icon, icon_state)//this means we exclude the overshooting object from the visual contents which does mean those visuals don't show up for the final bit of the beam... II.DrawBox(null,1,(length-N),32,32)//in the future if you want to improve this, remove the drawbox and instead use a 513 filter to cut away at the final object's icon - X.icon = II + segment.icon = II + segment.color = beam_color else - X.vis_contents += visuals - X.transform = rot_matrix + segment.vis_contents += visuals + segment.transform = rot_matrix //Calculate pixel offsets (If necessary) var/Pixel_x @@ -118,34 +166,111 @@ Pixel_y = round(cos(Angle)+32*cos(Angle)*(N+16)/32) //Position the effect so the beam is one continous line - var/a + var/final_x = segment.x + var/final_y = segment.y if(abs(Pixel_x)>32) - a = Pixel_x > 0 ? round(Pixel_x/32) : CEILING(Pixel_x/32, 1) - X.x += a + final_x += Pixel_x > 0 ? round(Pixel_x/32) : CEILING(Pixel_x/32, 1) Pixel_x %= 32 if(abs(Pixel_y)>32) - a = Pixel_y > 0 ? round(Pixel_y/32) : CEILING(Pixel_y/32, 1) - X.y += a + final_y += Pixel_y > 0 ? round(Pixel_y/32) : CEILING(Pixel_y/32, 1) Pixel_y %= 32 - X.pixel_x = Pixel_x - X.pixel_y = Pixel_y + segment.forceMove(locate(final_x, final_y, segment.z)) + segment.pixel_x = origin_px + Pixel_x + segment.pixel_y = origin_py + Pixel_y CHECK_TICK /obj/effect/ebeam mouse_opacity = MOUSE_OPACITY_TRANSPARENT + layer = ABOVE_ALL_MOB_LAYER anchored = TRUE + var/emissive = TRUE var/datum/beam/owner + +/obj/effect/ebeam/Initialize(mapload, beam_owner) + owner = beam_owner + return ..() + +/obj/effect/ebeam/update_overlays() + . = ..() + if(!emissive) + return + var/mutable_appearance/emissive_overlay = emissive_appearance(icon, icon_state, layer, alpha, appearance_flags, filters) + emissive_overlay.transform = transform + emissive_overlay.alpha = alpha + ADD_LUM_SOURCE(src, LUM_SOURCE_MANAGED_OVERLAY) + . += emissive_overlay + /obj/effect/ebeam/Destroy() owner = null return ..() /obj/effect/ebeam/singularity_pull() return + /obj/effect/ebeam/singularity_act() return +/// A beam subtype used for advanced beams, to react to atoms entering the beam +/obj/effect/ebeam/reacting + /// If TRUE, atoms that exist in the beam's loc when inited count as "entering" the beam + var/react_on_init = FALSE + +/obj/effect/ebeam/reacting/Initialize(mapload, beam_owner) + . = ..() + var/static/list/loc_connections = list( + COMSIG_ATOM_ENTERED = PROC_REF(on_entered), + COMSIG_ATOM_EXITED = PROC_REF(on_exited), + COMSIG_TURF_CHANGE = PROC_REF(on_turf_change), + ) + AddElement(/datum/element/connect_loc, loc_connections) + + if(!isturf(loc) || isnull(owner) || mapload || !react_on_init) + return + + for(var/atom/movable/existing as anything in loc) + beam_entered(existing) + +/obj/effect/ebeam/reacting/proc/on_entered(datum/source, atom/movable/entered) + SIGNAL_HANDLER + + if(isnull(owner)) + return + + beam_entered(entered) + +/obj/effect/ebeam/reacting/proc/on_exited(datum/source, atom/movable/exited) + SIGNAL_HANDLER + + if(isnull(owner)) + return + + beam_exited(exited) + +/obj/effect/ebeam/reacting/proc/on_turf_change(datum/source, path, new_baseturfs, flags, list/datum/callback/post_change_callbacks) + SIGNAL_HANDLER + + if(isnull(owner)) + return + + beam_turfs_changed(post_change_callbacks) + +/// Some atom entered the beam's line +/obj/effect/ebeam/reacting/proc/beam_entered(atom/movable/entered) + SHOULD_CALL_PARENT(TRUE) + SEND_SIGNAL(owner, COMSIG_BEAM_ENTERED, src, entered) + +/// Some atom exited the beam's line +/obj/effect/ebeam/reacting/proc/beam_exited(atom/movable/exited) + SHOULD_CALL_PARENT(TRUE) + SEND_SIGNAL(owner, COMSIG_BEAM_EXITED, src, exited) + +/// Some turf the beam covers has changed to a new turf type +/obj/effect/ebeam/reacting/proc/beam_turfs_changed(list/datum/callback/post_change_callbacks) + SHOULD_CALL_PARENT(TRUE) + SEND_SIGNAL(owner, COMSIG_BEAM_TURFS_CHANGED, post_change_callbacks) + /** * This is what you use to start a beam. Example: origin.Beam(target, args). **Store the return of this proc if you don't set maxdist or time, you need it to delete the beam.** * @@ -158,7 +283,18 @@ * maxdistance: how far the beam will go before stopping itself. Used mainly for two things: preventing lag if the beam may go in that direction and setting a range to abilities that use beams. * beam_type: The type of your custom beam. This is for adding other wacky stuff for your beam only. Most likely, you won't (and shouldn't) change it. */ -/atom/proc/Beam(atom/BeamTarget,icon_state="b_beam",icon='icons/effects/beam.dmi',time=INFINITY,maxdistance=INFINITY,beam_type=/obj/effect/ebeam) - var/datum/beam/newbeam = new(src,BeamTarget,icon,icon_state,time,maxdistance,beam_type) +/atom/proc/Beam(atom/BeamTarget, + icon_state="b_beam", + icon='icons/effects/beam.dmi', + time=INFINITY,maxdistance=INFINITY, + beam_type=/obj/effect/ebeam, + beam_color = null, emissive = TRUE, + override_origin_pixel_x = null, + override_origin_pixel_y = null, + override_target_pixel_x = null, + override_target_pixel_y = null, + layer = ABOVE_ALL_MOB_LAYER +) + var/datum/beam/newbeam = new(src,BeamTarget,icon,icon_state,time,maxdistance,beam_type, beam_color, emissive, override_origin_pixel_x, override_origin_pixel_y, override_target_pixel_x, override_target_pixel_y, layer) INVOKE_ASYNC(newbeam, TYPE_PROC_REF(/datum/beam, Start)) return newbeam diff --git a/code/datums/wires/_wires.dm b/code/datums/wires/_wires.dm index ab15cefb5386f..3f49137762e68 100644 --- a/code/datums/wires/_wires.dm +++ b/code/datums/wires/_wires.dm @@ -58,7 +58,12 @@ /datum/wires/Destroy() holder = null - assemblies.Cut() + //properly clear refs to avoid harddels & other problems + for(var/color in assemblies) + var/obj/item/assembly/assembly = assemblies[color] + assembly.holder = null + assembly.connected = null + LAZYCLEARLIST(assemblies) return ..() /datum/wires/proc/add_duds(duds) @@ -178,7 +183,16 @@ if(S && istype(S) && S.attachable && !is_attached(color)) assemblies[color] = S S.forceMove(holder) + /** + * special snowflake check for machines + * someone attached a signaler to the machines wires + * move it to the machines component parts so it doesn't get moved out in dump_inventory_contents() which gets called a lot + */ + if(istype(holder, /obj/machinery)) + var/obj/machinery/machine = holder + LAZYADD(machine.component_parts, S) S.connected = src + S.on_attach() // Notify assembly that it is attached ui_update() return S @@ -186,8 +200,7 @@ var/obj/item/assembly/S = get_attached(color) if(S && istype(S)) assemblies -= color - S.connected = null - S.forceMove(holder.drop_location()) + S.on_detach() // Notify the assembly. This should remove the reference to our holder ui_update() return S diff --git a/code/game/communications.dm b/code/game/communications.dm index 041bfa2e005ec..19a321899713d 100644 --- a/code/game/communications.dm +++ b/code/game/communications.dm @@ -178,10 +178,13 @@ GLOBAL_LIST_INIT(reverseradiochannels, list( if (!filter) filter = "_default" + var/datum/weakref/new_listener = WEAKREF(device) + if(isnull(new_listener)) + CRASH("null, non-datum, or qdeleted device") var/list/devices_line = devices[filter] if(!devices_line) devices[filter] = devices_line = list() - devices_line += WEAKREF(device) + devices_line += new_listener /datum/radio_frequency/proc/remove_listener(obj/device) @@ -198,11 +201,21 @@ GLOBAL_LIST_INIT(reverseradiochannels, list( return /datum/signal + /// The source of this signal. var/obj/source + /// The frequency on which this signal was emitted. var/frequency = 0 + /// The method through which this signal was transmitted. + /// See all of the `TRANSMISSION_X` in `code/__DEFINES/radio.dm` for + /// all of the possible options. var/transmission_method + /// The data carried through this signal. Defaults to `null`, otherwise it's + /// an associative list of (string, any). var/list/data + /// Logging data, used for logging purposes. Makes sense, right? + var/logging_data -/datum/signal/New(data, transmission_method = TRANSMISSION_RADIO) +/datum/signal/New(data, transmission_method = TRANSMISSION_RADIO, logging_data = null) src.data = data || list() src.transmission_method = transmission_method + src.logging_data = logging_data diff --git a/code/game/machinery/buttons.dm b/code/game/machinery/buttons.dm index e2f637ce90b3c..54182368f4896 100644 --- a/code/game/machinery/buttons.dm +++ b/code/game/machinery/buttons.dm @@ -173,7 +173,7 @@ CREATION_TEST_IGNORE_SUBTYPES(/obj/machinery/button) icon_state = "[skin]1" if(device) - device.pulsed() + device.pulsed(user) addtimer(CALLBACK(src, TYPE_PROC_REF(/atom, update_icon)), 15) diff --git a/code/game/objects/items/carp_lasso.dm b/code/game/objects/items/carp_lasso.dm index c2fea3899e1de..13a36119865cc 100644 --- a/code/game/objects/items/carp_lasso.dm +++ b/code/game/objects/items/carp_lasso.dm @@ -77,14 +77,13 @@ to_chat(user, "You can't do that right now!") return //Do lasso/beam for style points - var/datum/beam/B = new(loc, C, time=1 SECONDS, beam_icon='icons/effects/beam.dmi', beam_icon_state="carp_lasso", btype=/obj/effect/ebeam) - INVOKE_ASYNC(B, TYPE_PROC_REF(/datum/beam, Start)) + user.Beam(BeamTarget=C,icon_state = "carp_lasso",icon='icons/effects/beam.dmi', time = 1 SECONDS) C.unbuckle_all_mobs() mob_target = C C.throw_at(get_turf(src), 9, 2, user, FALSE, force = 0) C.transform = transform.Turn(180) C.toggle_ai(AI_OFF) - RegisterSignal(C, COMSIG_PARENT_QDELETING, PROC_REF(handle_hard_del)) + RegisterSignal(C, COMSIG_PARENT_QDELETING, PROC_REF(handle_hard_del), override=TRUE) to_chat(user, "You lasso [C]!") timer = addtimer(CALLBACK(src, PROC_REF(fail_ally)), 6 SECONDS, TIMER_STOPPABLE) //after 6 seconds set the carp back @@ -92,14 +91,17 @@ return ((!whitelist_mobs || is_type_in_typecache(target, whitelist_mobs)) && (!blacklist_mobs || !is_type_in_typecache(target, blacklist_mobs))) /obj/item/mob_lasso/proc/fail_ally() + if(!mob_target) + return visible_message("[mob_target] breaks free!") - mob_target?.transform = transform.Turn(0) - mob_target?.toggle_ai(AI_ON) + mob_target.transform = transform.Turn(0) + mob_target.toggle_ai(AI_ON) UnregisterSignal(mob_target, COMSIG_PARENT_QDELETING) mob_target = null timer = null /obj/item/mob_lasso/proc/handle_hard_del() + SIGNAL_HANDLER mob_target = null timer = null diff --git a/code/game/objects/items/devices/transfer_valve.dm b/code/game/objects/items/devices/transfer_valve.dm index 8b6d1ad9d75a1..3e38084aca31a 100644 --- a/code/game/objects/items/devices/transfer_valve.dm +++ b/code/game/objects/items/devices/transfer_valve.dm @@ -58,8 +58,8 @@ return attached_device = A to_chat(user, "You attach the [item] to the valve controls and secure it.") - A.on_attach() A.holder = src + A.on_attach() A.toggle_secure() //this calls update_icon(), which calls update_icon() on the holder (i.e. the bomb). log_bomber(user, "attached a [item.name] to a ttv -", src, null, FALSE) attacher = user diff --git a/code/modules/assembly/assembly.dm b/code/modules/assembly/assembly.dm index 3f1b46ffdc635..6f9591ef72777 100644 --- a/code/modules/assembly/assembly.dm +++ b/code/modules/assembly/assembly.dm @@ -14,20 +14,14 @@ var/is_position_sensitive = FALSE //set to true if the device has different icons for each position. //This will prevent things such as visible lasers from facing the incorrect direction when transformed by assembly_holder's update_icon() + var/assembly_flags = NONE var/secured = TRUE - var/securable = TRUE //False for assemblies that are always unsecured var/list/attached_overlays = null var/obj/item/assembly_holder/holder = null - var/wire_type = WIRE_RECEIVE | WIRE_PULSE + var/obj/structure/reagent_dispensers/rig = null var/attachable = FALSE // can this be attached to wires var/datum/wires/connected = null - var/next_activate = 0 //When we're next allowed to activate - for spam control - var/activate_delay = 30 - -/obj/item/assembly/Initialize(mapload) - . = ..() - secured &&= securable /obj/item/assembly/Destroy() holder = null @@ -35,18 +29,33 @@ /obj/item/assembly/get_part_rating() return 1 - -/obj/item/assembly/proc/on_attach() - -//Call this when detaching it from a device. handles any special functions that need to be updated ex post facto +/** + * on_attach: Called when attached to a holder, wiring datum, or other special assembly + * + * Will also be called if the assembly holder is attached to a plasma (internals) tank or welding fuel (dispenser) tank. + */ + +/obj/item/assembly/proc/on_attach(var/obj/structure/reagent_dispensers/T) + if(!holder && connected) + holder = connected.holder + if(T) + rig = T + +/** + * on_detach: Called when removed from an assembly holder or wiring datum + */ /obj/item/assembly/proc/on_detach() + if(connected) + connected = null if(!holder) return FALSE forceMove(holder.drop_location()) holder = null return TRUE -//Called when the holder is moved +/** + * holder_movement: Called when the assembly's holder detects movement + */ /obj/item/assembly/proc/holder_movement() if(!holder) return FALSE @@ -61,57 +70,71 @@ //Called when another assembly acts on this one, var/radio will determine where it came from for wire calcs -/obj/item/assembly/proc/pulsed(radio = FALSE) - if(wire_type & WIRE_RECEIVE) - INVOKE_ASYNC(src, PROC_REF(activate)) - if(radio && (wire_type & WIRE_RADIO_RECEIVE)) - INVOKE_ASYNC(src, PROC_REF(activate)) +/obj/item/assembly/proc/pulsed(mob/pulser) + INVOKE_ASYNC(src, PROC_REF(activate), pulser) + SEND_SIGNAL(src, COMSIG_ASSEMBLY_PULSED) return TRUE //Called when this device attempts to act on another device, var/radio determines if it was sent via radio or direct /obj/item/assembly/proc/pulse(radio = FALSE) - if(connected && wire_type) + if(connected) // if we have connected wires and are a pulsing assembly, pulse it connected.pulse_assembly(src) - return TRUE - if(holder && (wire_type & WIRE_PULSE)) - holder.process_activation(src, 1, 0) - if(holder && (wire_type & WIRE_PULSE_SPECIAL)) - holder.process_activation(src, 0, 1) + else if(holder) // otherwise if we're attached to a holder, process the activation of it with our flags + holder.process_activation(src) return TRUE // What the device does when turned on -/obj/item/assembly/proc/activate() - if(QDELETED(src) || (securable && !secured) || (next_activate > world.time)) +/obj/item/assembly/proc/activate(mob/activator) + if(QDELETED(src) || !secured || (next_activate > world.time)) return FALSE - next_activate = world.time + activate_delay + next_activate = world.time + 30 return TRUE /obj/item/assembly/proc/toggle_secure() - secured = securable && !secured - update_icon() + secured = !secured + update_appearance() return secured +// This is overwritten so that clumsy people can set off mousetraps even when in a holder. +// We are not going deeper than that however (won't set off if in a tank bomb or anything with wires) +// That would need to be added to all parent objects, or a signal created, whatever. +// Anyway this return check prevents you from picking up every assembly inside the holder at once. +/obj/item/assembly/attack_hand(mob/living/user, list/modifiers) + if(holder || connected) + return + . = ..() /obj/item/assembly/attackby(obj/item/W, mob/user, params) if(isassembly(W)) - var/obj/item/assembly/A = W - if((!A.secured) && (!secured)) - holder = new/obj/item/assembly_holder(get_turf(src)) - holder.assemble(src,A,user) - to_chat(user, "You attach and secure \the [A] to \the [src]!") - else - to_chat(user, "Both devices must be in attachable mode to be attached together.") + var/obj/item/assembly/new_assembly = W + + // Check both our's and their's assembly flags to see if either should not duplicate + // If so, and we match types, don't create a holder - block it + if(((new_assembly.assembly_flags|assembly_flags) & ASSEMBLY_NO_DUPLICATES) && istype(new_assembly, type)) + balloon_alert(user, "can't attach another of that!") + return + if(new_assembly.secured || secured) + balloon_alert(user, "both devices not attachable!") + return + + holder = new /obj/item/assembly_holder(get_turf(src)) + holder.assemble(src, new_assembly, user) + to_chat(user, "You attach and secure \the [new_assembly] to \the [src]!") + return + + if(istype(W, /obj/item/assembly_holder)) + var/obj/item/assembly_holder/added_to_holder = W + added_to_holder.try_add_assembly(src, user) return - ..() + + return ..() /obj/item/assembly/screwdriver_act(mob/living/user, obj/item/I) if(..()) return TRUE - if(!securable) - return FALSE if(toggle_secure()) to_chat(user, "\The [src] is ready!") else @@ -123,18 +146,13 @@ . = ..() . += "\The [src] [secured? "is secured and ready to be used!" : "can be attached to other things."]" - -/obj/item/assembly/attack_self(mob/user) - if(!user) - return FALSE - user.set_machine(src) - interact(user) - return TRUE - -/obj/item/assembly/interact(mob/user) - return ui_interact(user) - /obj/item/assembly/ui_host(mob/user) - if(holder) - return holder - return src + // In order, return: + // - The conencted wiring datum's owner, or + // - The thing your assembly holder is attached to, or + // - the assembly holder itself, or + // - us + return connected?.holder || holder?.master || holder || src + +/obj/item/assembly/ui_state(mob/user) + return GLOB.hands_state diff --git a/code/modules/assembly/holder.dm b/code/modules/assembly/holder.dm index c4898219fac7d..3ec654e578876 100644 --- a/code/modules/assembly/holder.dm +++ b/code/modules/assembly/holder.dm @@ -11,28 +11,41 @@ throw_speed = 2 throw_range = 7 - var/obj/item/assembly/a_left = null - var/obj/item/assembly/a_right = null + var/list/obj/item/assembly/assemblies /// used to store the list of assemblies making up our assembly holder /obj/item/assembly_holder/Initialize(mapload) . = ..() - var/static/list/loc_connections = list( - COMSIG_ATOM_ENTERED = PROC_REF(on_entered), - ) - AddElement(/datum/element/connect_loc, loc_connections) + AddComponent(/datum/component/simple_rotation,ROTATION_ALTCLICK | ROTATION_CLOCKWISE | ROTATION_COUNTERCLOCKWISE | ROTATION_VERBS ) + +/obj/item/assembly_holder/Destroy() + QDEL_LAZYLIST(assemblies) + return ..() /obj/item/assembly_holder/proc/on_entered(datum/source, atom/movable/AM) SIGNAL_HANDLER return -/obj/item/assembly_holder/ComponentInitialize() +/obj/item/assembly_holder/Exited(atom/movable/gone, direction) . = ..() - var/static/rotation_flags = ROTATION_ALTCLICK | ROTATION_CLOCKWISE | ROTATION_COUNTERCLOCKWISE | ROTATION_FLIP | ROTATION_VERBS - AddComponent(/datum/component/simple_rotation, rotation_flags) + LAZYREMOVE(assemblies, gone) /obj/item/assembly_holder/IsAssemblyHolder() return TRUE +/obj/item/assembly_holder/examine(mob/user) + . = ..() + for(var/assembly in assemblies) + if(istype(assembly, /obj/item/assembly/timer)) + var/obj/item/assembly/timer/timer = assembly + . += "The timer is [timer.timing ? "counting down from [timer.time]":"set for [timer.time] seconds"]." + +/obj/item/assembly_holder/Moved(atom/old_loc, movement_dir) + . = ..() + on_move(old_loc, movement_dir) + +/obj/item/assembly_holder/proc/on_move(atom/old_loc, movement_dir) + for(var/obj/item/assembly/infra/assembly in assemblies) + assembly.on_move(old_loc, movement_dir) /obj/item/assembly_holder/proc/assemble(obj/item/assembly/A, obj/item/assembly/A2, mob/user) attach(A,user) @@ -41,6 +54,49 @@ update_icon() SSblackbox.record_feedback("tally", "assembly_made", 1, "[initial(A.name)]-[initial(A2.name)]") +// on_attach: Pass on_attach message to child assemblies +/obj/item/assembly_holder/proc/on_attach(var/obj/structure/reagent_dispensers/rig) + var/obj/item/newloc = loc + if(!newloc.IsSpecialAssembly() && !newloc.IsAssemblyHolder()) + return + for(var/obj/item/assembly/assembly in assemblies) + assembly.on_attach(rig) + +/obj/item/assembly_holder/proc/try_add_assembly(obj/item/assembly/attached_assembly, mob/user) + if(attached_assembly.secured) + balloon_alert(attached_assembly, "not attachable!") + return FALSE + + if(LAZYLEN(assemblies) >= HOLDER_MAX_ASSEMBLIES) + balloon_alert(user, "too many assemblies!") + return FALSE + + if(attached_assembly.assembly_flags & ASSEMBLY_NO_DUPLICATES) + if(locate(attached_assembly.type) in assemblies) + balloon_alert(user, "can't attach another of that!") + return FALSE + + add_assembly(attached_assembly, user) + balloon_alert(user, "part attached") + return TRUE + +/** + * Adds an assembly to the assembly holder + * + * This proc is used to add an assembly to the assembly holder, update the appearance, and the name of it. + * Arguments: + * * attached_assembly - assembly we are adding to the assembly holder + * * user - user we pass into attach() + */ +/obj/item/assembly_holder/proc/add_assembly(obj/item/assembly/attached_assembly, mob/user) + attach(attached_assembly, user) + name = "" + for(var/obj/item/assembly/assembly as anything in assemblies) + name += "[assembly.name]-" + name = splicetext(name, length(name), length(name) + 1, "") + name += " assembly" + update_appearance() + /obj/item/assembly_holder/proc/attach(obj/item/assembly/A, mob/user) if(!A.remove_item_from_storage(src)) if(user) @@ -49,102 +105,97 @@ A.forceMove(src) A.holder = src A.toggle_secure() - if(!a_left) - a_left = A - else - a_right = A + LAZYADD(assemblies, A) A.holder_movement() A.on_attach() -/obj/item/assembly_holder/update_icon() - cut_overlays() - if(a_left) - add_overlay("[a_left.icon_state]_left") - for(var/O in a_left.attached_overlays) - add_overlay("[O]_l") - - if(a_right) - if(a_right.is_position_sensitive) - add_overlay("[a_right.icon_state]_right") - for(var/O in a_right.attached_overlays) - add_overlay("[O]_r") - else - var/mutable_appearance/right = mutable_appearance(icon, "[a_right.icon_state]_left") - right.transform = matrix(-1, 0, 0, 0, 1, 0) - for(var/O in a_right.attached_overlays) - right.add_overlay("[O]_l") - add_overlay(right) +/obj/item/assembly_holder/update_icon(updates=ALL) + . = ..() + master?.update_appearance(updates) - if(master) - master.update_icon() +/obj/item/assembly_holder/update_overlays() + . = ..() + for(var/i in 1 to LAZYLEN(assemblies)) + if(i % 2 == 1) + var/obj/item/assembly/assembly = assemblies[i] + . += "[assembly.icon_state]_left" + for(var/left_overlay in assembly.attached_overlays) + . += "[left_overlay]_l" + if(i % 2 == 0) + var/obj/item/assembly/assembly = assemblies[i] + var/mutable_appearance/right = mutable_appearance(icon, "[assembly.icon_state]_left") + right.transform = matrix(-1, 0, 0, 0, 1, 0) + for(var/right_overlay in assembly.attached_overlays) + right.add_overlay("[right_overlay]_l") + . += right /obj/item/assembly_holder/on_found(mob/finder) - if(a_left) - a_left.on_found(finder) - if(a_right) - a_right.on_found(finder) + for(var/obj/item/assembly/assembly as anything in assemblies) + assembly.on_found(finder) /obj/item/assembly_holder/setDir() . = ..() - if(a_left) - a_left.holder_movement() - if(a_right) - a_right.holder_movement() + for(var/obj/item/assembly/assembly as anything in assemblies) + assembly.holder_movement() + /obj/item/assembly_holder/dropped(mob/user) - ..() - if(a_left) - a_left.dropped(user) - if(a_right) - a_right.dropped(user) + . = ..() + for(var/obj/item/assembly/assembly as anything in assemblies) + assembly.dropped(user) -/obj/item/assembly_holder/attack_hand()//Perhapse this should be a holder_pickup proc instead, can add if needbe I guess +/obj/item/assembly_holder/attack_hand(mob/living/user, list/modifiers)//Perhapse this should be a holder_pickup proc instead, can add if needbe I guess . = ..() if(.) return - if(a_left) - a_left.attack_hand() - if(a_right) - a_right.attack_hand() + for(var/obj/item/assembly/assembly as anything in assemblies) + assembly.attack_hand(user, modifiers) // Note override in assembly.dm to prevent side effects here + +/obj/item/assembly_holder/attackby(obj/item/weapon, mob/user, params) + if(isassembly(weapon)) + try_add_assembly(weapon, user) + return + + return ..() + +/obj/item/assembly_holder/AltClick(mob/user) + return ..() // This hotkey is BLACKLISTED since it's used by /datum/component/simple_rotation /obj/item/assembly_holder/screwdriver_act(mob/user, obj/item/tool) if(..()) return TRUE - to_chat(user, "You disassemble [src]!") - if(a_left) - a_left.on_detach() - a_left = null - if(a_right) - a_right.on_detach() - a_right = null + balloon_alert(user, "disassembled") + for(var/obj/item/assembly/assembly as anything in assemblies) + assembly.on_detach() + LAZYREMOVE(assemblies, assembly) qdel(src) return TRUE /obj/item/assembly_holder/attack_self(mob/user) - add_fingerprint(user) - if(!a_left || !a_right) - to_chat(user, "Assembly part missing!") - return - if(istype(a_left,a_right.type))//If they are the same type it causes issues due to window code - switch(alert("Which side would you like to use?",,"Left","Right")) - if("Left") - a_left.attack_self(user) - if("Right") - a_right.attack_self(user) + src.add_fingerprint(user) + if(LAZYLEN(assemblies) == 1) + balloon_alert(user, "part missing!") return - else - a_left.attack_self(user) - a_right.attack_self(user) + + for(var/obj/item/assembly/assembly as anything in assemblies) + assembly.attack_self(user) -/obj/item/assembly_holder/proc/process_activation(obj/D, normal = 1, special = 1) +/** + * this proc is used to process the activation of the assembly holder + * + * This proc is usually called by signalers, timers, or anything that can trigger and + * send a pulse to the assembly holder, which then calls this proc that actually activates the assemblies + * Arguments: + * * /obj/D - the device we sent the pulse from which called this proc + */ +/obj/item/assembly_holder/proc/process_activation(obj/D) if(!D) return FALSE - if((normal) && (a_right) && (a_left)) - if(a_right != D) - a_right.pulsed(FALSE) - if(a_left != D) - a_left.pulsed(FALSE) + if(LAZYLEN(assemblies) >= 2) + for(var/obj/item/assembly/assembly as anything in assemblies) + if(assembly != D) + assembly.pulsed() if(master) master.receive_signal() return TRUE diff --git a/code/modules/assembly/igniter.dm b/code/modules/assembly/igniter.dm index 3da6b42804653..2a88d120969d4 100644 --- a/code/modules/assembly/igniter.dm +++ b/code/modules/assembly/igniter.dm @@ -5,6 +5,9 @@ custom_materials = list(/datum/material/iron=500, /datum/material/glass=50) var/datum/effect_system/spark_spread/sparks heat = 1000 + drop_sound = 'sound/items/handling/component_drop.ogg' + pickup_sound = 'sound/items/handling/component_pickup.ogg' + assembly_flags = ASSEMBLY_NO_DUPLICATES /obj/item/assembly/igniter/suicide_act(mob/living/carbon/user) user.visible_message("[user] is trying to ignite [user.p_them()]self with \the [src]! It looks like [user.p_theyre()] trying to commit suicide!") @@ -29,6 +32,10 @@ var/turf/location = get_turf(loc) if(location) location.hotspot_expose(1000,1000) + if(holder) + SEND_SIGNAL(holder.loc, COMSIG_IGNITER_ACTIVATE) + if(QDELETED(src)) + return TRUE sparks.start() return TRUE diff --git a/code/modules/assembly/infrared.dm b/code/modules/assembly/infrared.dm index 5bf8e46cefaa5..04d3ce725c7be 100644 --- a/code/modules/assembly/infrared.dm +++ b/code/modules/assembly/infrared.dm @@ -5,194 +5,282 @@ custom_materials = list(/datum/material/iron=1000, /datum/material/glass=500) is_position_sensitive = TRUE item_flags = NO_PIXEL_RANDOM_DROP - - var/on = FALSE - var/visible = FALSE - var/maxlength = 8 - var/list/obj/effect/beam/i_beam/beams - var/olddir = 0 - var/turf/listeningTo - var/hearing_range = 3 + drop_sound = 'sound/items/handling/component_drop.ogg' + pickup_sound = 'sound/items/handling/component_pickup.ogg' + set_dir_on_move = FALSE + var/on = FALSE // Whether the beam is beaming + var/visible = FALSE // Whether the beam is visible + var/maxlength = 8 // The length the beam can go + var/beam_pass_flags = PASSTABLE|PASSTRANSPARENT|PASSGRILLE // Pass flags the beam uses to determine what it can pass through + var/hearing_range = 3 // The radius of which people can hear triggers + var/datum/beam/active_beam // The current active beam datum + var/turf/buffer_turf // A reference to the turf at the END of our active beam /obj/item/assembly/infra/Initialize(mapload) . = ..() - beams = list() - START_PROCESSING(SSobj, src) - -/obj/item/assembly/infra/ComponentInitialize() - . = ..() - var/static/rotation_flags = ROTATION_ALTCLICK | ROTATION_CLOCKWISE | ROTATION_COUNTERCLOCKWISE | ROTATION_FLIP | ROTATION_VERBS - AddComponent(/datum/component/simple_rotation, rotation_flags, after_rotation=CALLBACK(src,PROC_REF(after_rotation))) - -/obj/item/assembly/infra/proc/after_rotation() - refreshBeam() + AddComponent(/datum/component/simple_rotation,ROTATION_ALTCLICK | ROTATION_CLOCKWISE | ROTATION_COUNTERCLOCKWISE | ROTATION_VERBS ) /obj/item/assembly/infra/Destroy() - STOP_PROCESSING(SSobj, src) - listeningTo = null - QDEL_LIST(beams) - . = ..() + QDEL_NULL(active_beam) + buffer_turf = null + return ..() /obj/item/assembly/infra/examine(mob/user) . = ..() . += "The infrared trigger is [on?"on":"off"]." -/obj/item/assembly/infra/activate() - if(!..()) - return FALSE //Cooldown check - on = !on - refreshBeam() - update_icon() +/// Checks if the passed movable can block the beam. +/obj/item/assembly/infra/proc/atom_blocks_beam(atom/movable/beam_atom) + if(isnull(beam_atom)) + return FALSE + if(beam_atom == src || beam_atom == holder) + return FALSE + // Blocks beams from triggering themselves, important to avoid infinite loops + if(istype(beam_atom, /obj/effect/ebeam)) + return FALSE + // Anti-revenant / anti-ghost guard + if(beam_atom.invisibility) + return FALSE + // In general non-dense items should not block beams, but make special cases for things being thrown + if(!beam_atom.density && !beam_atom.throwing) + return FALSE + // The actually important check. Ensures stuff like mobs trip it but stuff like laser projectiles don't + if(beam_atom.pass_flags_self & beam_pass_flags) + return FALSE + if(isitem(beam_atom)) + var/obj/item/beam_item = beam_atom + if(beam_item.item_flags & ABSTRACT) + return FALSE + return TRUE -/obj/item/assembly/infra/toggle_secure() - secured = !secured - if(secured) - START_PROCESSING(SSobj, src) - refreshBeam() - else - QDEL_LIST(beams) - STOP_PROCESSING(SSobj, src) - update_icon() - return secured +/// Checks if the passed turf (or something on it) can block the beam. +/obj/item/assembly/infra/proc/turf_blocks_beam(turf/beam_turf) + if(beam_turf.density) + return TRUE + for(var/atom/movable/blocker as anything in beam_turf) + if(atom_blocks_beam(blocker)) + return TRUE + return FALSE -/obj/item/assembly/infra/update_icon() - cut_overlays() - attached_overlays = list() - if(on) - add_overlay("infrared_on") - attached_overlays += "infrared_on" - if(visible && secured) - add_overlay("infrared_visible") - attached_overlays += "infrared_visible" +/// Used to refresh the beam in whatever context. +/obj/item/assembly/infra/proc/make_beam() + SHOULD_NOT_SLEEP(TRUE) - if(holder) - holder.update_icon() - return + if(!isnull(buffer_turf)) + UnregisterSignal(buffer_turf, list(COMSIG_ATOM_EXITED, COMSIG_TURF_CHANGE)) + buffer_turf = null -/obj/item/assembly/infra/dropped() - ..() - if(holder) - holder_movement() //sync the dir of the device as well if it's contained in a TTV or an assembly holder - else - refreshBeam() - -/obj/item/assembly/infra/process() + QDEL_NULL(active_beam) + if(QDELETED(src)) + return if(!on || !secured) - refreshBeam() return -/obj/item/assembly/infra/proc/refreshBeam() - QDEL_LIST(beams) - if(throwing || !on || !secured) + var/atom/start_loc = rig || holder || src + var/turf/start_turf = start_loc.loc + if(!istype(start_turf)) return - if(holder) - if(holder.master) //incase the sensor is part of an assembly that's contained in another item, such as a single tank bomb - if(!holder.master.IsSpecialAssembly() || !isturf(holder.master.loc)) - return - else if(!isturf(holder.loc)) //else just check where the holder is - return - else if(!isturf(loc)) //or just where the fuck we are in general + // One extra turf is added to max length to get an extra buffer + var/list/turf/potential_turfs = getline(start_turf, get_ranged_target_turf(start_turf, dir, maxlength + 1)) + if(!length(potential_turfs)) return - var/turf/T = get_turf(src) - var/_dir = dir - var/turf/_T = get_step(T, _dir) - if(_T) - for(var/i in 1 to maxlength) - var/obj/effect/beam/i_beam/I = new(T) - if(istype(holder, /obj/item/assembly_holder)) - var/obj/item/assembly_holder/assembly_holder = holder - I.icon_state = "[initial(I.icon_state)]_[(assembly_holder.a_left == src) ? "l":"r"]" //Sync the offset of the beam with the position of the sensor. - else if(istype(holder, /obj/item/transfer_valve)) - I.icon_state = "[initial(I.icon_state)]_ttv" - I.set_density(TRUE) - if(!I.Move(_T)) - qdel(I) - switchListener(_T) - break - I.set_density(FALSE) - beams += I - I.master = src - I.setDir(_dir) - I.invisibility = visible? 0 : INVISIBILITY_ABSTRACT - T = _T - _T = get_step(_T, _dir) - CHECK_TICK -/obj/item/assembly/infra/on_detach() + var/list/turf/final_turfs = list() + for(var/turf/target_turf as anything in potential_turfs) + if(target_turf != start_turf && turf_blocks_beam(target_turf)) + break + final_turfs += target_turf + + if(!length(final_turfs)) + return + + var/turf/last_turf = final_turfs[length(final_turfs)] + buffer_turf = get_step(last_turf, dir) + + var/beam_target_x = pixel_x + var/beam_target_y = pixel_y + // The beam by default will go to middle of turf (because items are in the middle of turfs) + // So we need to offset it + if(dir & NORTH) + beam_target_y += 16 + else if(dir & SOUTH) + beam_target_y -= 16 + if(dir & WEST) + beam_target_x -= 16 + else if(dir & EAST) + beam_target_x += 16 + + active_beam = start_loc.Beam( + BeamTarget = last_turf, + beam_type = /obj/effect/ebeam/reacting/infrared, + icon = 'icons/effects/beam.dmi', + icon_state = "1-full", + beam_color = COLOR_RED, + emissive = TRUE, + override_target_pixel_x = beam_target_x, + override_target_pixel_y = beam_target_y, + ) + RegisterSignal(active_beam, COMSIG_BEAM_ENTERED, PROC_REF(beam_entered)) + RegisterSignal(active_beam, COMSIG_BEAM_TURFS_CHANGED, PROC_REF(beam_turfs_changed)) + update_visible() + // Buffer can be null (if we're at map edge for an example) but this fine + if(!isnull(buffer_turf)) + // We need to check the state of the turf at the end of the beam, to determine when we need to re-grow (if blocked) + RegisterSignal(buffer_turf, COMSIG_ATOM_EXITED, PROC_REF(buffer_exited)) + RegisterSignal(buffer_turf, COMSIG_TURF_CHANGE, PROC_REF(buffer_changed)) + +/obj/item/assembly/infra/proc/beam_entered(datum/beam/source, obj/effect/ebeam/hit, atom/movable/entered) + SIGNAL_HANDLER + + // First doesn't count + if(hit == active_beam.elements[1]) + return + if(!atom_blocks_beam(entered)) + return + + beam_trigger(hit, entered) + +/obj/item/assembly/infra/proc/beam_turfs_changed(datum/beam/source, list/datum/callback/post_change_callbacks) + SIGNAL_HANDLER + // If the turfs changed it's possible something is now blocking it, remake when done + post_change_callbacks += CALLBACK(src, PROC_REF(make_beam)) + +/obj/item/assembly/infra/proc/buffer_exited(turf/source, atom/movable/exited, ...) + SIGNAL_HANDLER + + if(!atom_blocks_beam(exited)) + return + + make_beam() + +/obj/item/assembly/infra/proc/buffer_changed(turf/source, path, list/new_baseturfs, flags, list/datum/callback/post_change_callbacks) + SIGNAL_HANDLER + + post_change_callbacks += CALLBACK(src, PROC_REF(make_beam)) + +/obj/item/assembly/infra/proc/beam_trigger(obj/effect/ebeam/hit, atom/movable/entered) + make_beam() + if(!COOLDOWN_FINISHED(src, next_activate)) + return + + pulse() + audible_message("[icon2html(src, hearers(src))] *beep* *beep* *beep*", null, hearing_range) + playsound(src, 'sound/machines/triple_beep.ogg', ASSEMBLY_BEEP_VOLUME, TRUE, extrarange = hearing_range - SOUND_RANGE + 1, falloff_distance = hearing_range) + COOLDOWN_START(src, next_activate, 3 SECONDS) + +/obj/item/assembly/infra/activate() . = ..() if(!.) return - refreshBeam() + toggle_on() -/obj/item/assembly/infra/attack_hand() +/obj/item/assembly/infra/toggle_secure() . = ..() - refreshBeam() + make_beam() + +/obj/item/assembly/infra/proc/toggle_on() // Toggles the beam on or off. + on = !on + make_beam() + update_icon() + +/obj/item/assembly/infra/proc/toggle_visible() // Toggles the visibility of the beam. + visible = !visible + update_visible() + update_icon() + +/obj/item/assembly/infra/proc/update_visible() // Updates the visibility of the beam (if active). + if(visible) + for(var/obj/effect/ebeam/beam as anything in active_beam?.elements) + beam.invisibility = FALSE + else + for(var/obj/effect/ebeam/beam as anything in active_beam?.elements) + beam.invisibility = TRUE -/obj/item/assembly/infra/Moved() - var/t = dir +/obj/item/assembly/infra/vv_edit_var(var_name, var_value) . = ..() - setDir(t) + if(!.) + return + switch(var_name) + if(NAMEOF(src, visible)) + update_visible() + update_icon() + + if(NAMEOF(src, on), NAMEOF(src, maxlength), NAMEOF(src, beam_pass_flags)) + make_beam() + update_icon() -/obj/item/assembly/infra/throw_at(atom/target, range, speed, mob/thrower, spin=1, diagonals_first = 0, datum/callback/callback, force, gentle = FALSE, quickstart = TRUE) + +/obj/item/assembly/infra/update_icon() . = ..() - olddir = dir + holder?.update_appearance() + attached_overlays = list() + if(on) + attached_overlays += "[icon_state]_on" + + add_overlay(attached_overlays) -/obj/item/assembly/infra/throw_impact(atom/hit_atom, datum/thrownthing/throwingdatum) +/obj/item/assembly/infra/dropped() . = ..() - if(!olddir) - return - setDir(olddir) - olddir = null + if(holder) + holder_movement() //sync the dir of the device as well if it's contained in a TTV or an assembly holder + make_beam() -/obj/item/assembly/infra/proc/trigger_beam(atom/movable/AM, turf/location) - refreshBeam() - switchListener(location) - if(!secured || !on || next_activate > world.time) - return FALSE - pulse(FALSE) - audible_message("[icon2html(src, hearers(src))] *beep* *beep* *beep*", null, hearing_range) - playsound(get_turf(src), 'sound/machines/triple_beep.ogg', ASSEMBLY_BEEP_VOLUME, TRUE) - next_activate = world.time + 30 +/obj/item/assembly/infra/on_attach() + . = ..() + make_beam() + holder.set_dir_on_move = set_dir_on_move -/obj/item/assembly/infra/proc/switchListener(turf/newloc) - if(listeningTo == newloc) +/obj/item/assembly/infra/on_detach() + holder.set_dir_on_move = initial(holder.set_dir_on_move) + . = ..() + if(!.) return - if(listeningTo) - UnregisterSignal(listeningTo, COMSIG_ATOM_EXITED) - RegisterSignal(newloc, COMSIG_ATOM_EXITED, PROC_REF(check_exit)) - listeningTo = newloc + make_beam() -/obj/item/assembly/infra/proc/check_exit(datum/source, atom/movable/gone, direction) - SIGNAL_HANDLER +/obj/item/assembly/infra/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change) + . = ..() + on_move(old_loc, movement_dir) - if(QDELETED(src)) +/obj/item/assembly/infra/proc/on_move(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change) + if(loc == old_loc) return - if(src == gone || istype(gone, /obj/effect/beam/i_beam)) + make_beam() + if(!visible || forced || !movement_dir || !Adjacent(old_loc)) return - if(isitem(gone)) - var/obj/item/I = gone - if (I.item_flags & ABSTRACT) - return - INVOKE_ASYNC(src, PROC_REF(refreshBeam)) - -/obj/item/assembly/infra/setDir() + // Because the new beam is made in the new loc, it "jumps" from one turf to another + // We can do an animate to pretend we're gliding between turfs rather than making a whole new beam + var/x_move = 0 + var/y_move = 0 + if(movement_dir & NORTH) + y_move = -32 + else if(movement_dir & SOUTH) + y_move = 32 + if(movement_dir & WEST) + x_move = 32 + else if(movement_dir & EAST) + x_move = -32 + + var/fake_glide_time = round(world.icon_size / glide_size * world.tick_lag, world.tick_lag) + for(var/obj/effect/ebeam/beam as anything in active_beam?.elements) + var/matrix/base_transform = matrix(beam.transform) + beam.transform = beam.transform.Translate(x_move, y_move) + animate(beam, transform = base_transform, time = fake_glide_time) + +/obj/item/assembly/infra/setDir(newdir) + var/prev_dir = dir . = ..() - refreshBeam() - -/obj/item/assembly/infra/ui_status(mob/user) - if(is_secured(user)) - return ..() - return UI_CLOSE - + if(dir == prev_dir) + return + make_beam() -/obj/item/assembly/infra/ui_state(mob/user) - return GLOB.hands_state +/obj/item/assembly/infra/ui_status(mob/user, datum/ui_state/state) + return is_secured(user) ? ..() : UI_CLOSE /obj/item/assembly/infra/ui_interact(mob/user, datum/tgui/ui) ui = SStgui.try_update_ui(user, src, ui) if(!ui) - ui = new(user, src, "InfraredEmitter") + ui = new(user, src, "InfraredEmitter", name) ui.open() /obj/item/assembly/infra/ui_data(mob/user) @@ -202,47 +290,19 @@ return data /obj/item/assembly/infra/ui_act(action, params) - if(..()) - return + . = ..() + if(.) + return . switch(action) if("power") - on = !on - . = TRUE + toggle_on() + return TRUE if("visibility") - visible = !visible - . = TRUE - - if(.) - update_icon() - refreshBeam() - -/***************************IBeam*********************************/ + toggle_visible() + return TRUE -/obj/effect/beam/i_beam +// Beam subtype for the infrared emitter +/obj/effect/ebeam/reacting/infrared name = "infrared beam" - icon = 'icons/obj/projectiles.dmi' - icon_state = "ibeam" - anchored = TRUE - density = FALSE - pass_flags = PASSTABLE|PASSTRANSPARENT|PASSGRILLE - pass_flags_self = LETPASSTHROW - var/obj/item/assembly/infra/master - -/obj/effect/beam/i_beam/Initialize(mapload) - . = ..() - var/static/list/loc_connections = list( - COMSIG_ATOM_ENTERED = PROC_REF(on_entered), - ) - AddElement(/datum/element/connect_loc, loc_connections) - -/obj/effect/beam/i_beam/proc/on_entered(datum/source, atom/movable/AM as mob|obj) - SIGNAL_HANDLER - - if(istype(AM, /obj/effect/beam)) - return - if (isitem(AM)) - var/obj/item/I = AM - if (I.item_flags & ABSTRACT) - return - INVOKE_ASYNC(master, TYPE_PROC_REF(/obj/item/assembly/infra, trigger_beam), AM, get_turf(src)) + alpha = 175 diff --git a/code/modules/assembly/mousetrap.dm b/code/modules/assembly/mousetrap.dm index f44f72f3a7268..4769f01272687 100644 --- a/code/modules/assembly/mousetrap.dm +++ b/code/modules/assembly/mousetrap.dm @@ -6,23 +6,59 @@ custom_materials = list(/datum/material/iron=100) attachable = TRUE var/armed = FALSE - - ///if we are attached to an assembly holder, we attach a connect_loc element to ourselves that listens to this from the holder - var/static/list/holder_connections = list( - COMSIG_ATOM_ENTERED = PROC_REF(on_entered), - ) + drop_sound = 'sound/items/handling/component_drop.ogg' + pickup_sound = 'sound/items/handling/component_pickup.ogg' + var/obj/item/host = null + var/turf/host_turf = null /obj/item/assembly/mousetrap/Initialize(mapload) . = ..() - var/static/list/loc_connections = list( - COMSIG_ATOM_ENTERED = PROC_REF(on_entered), - ) - AddElement(/datum/element/connect_loc, loc_connections) + update_host(force = TRUE) /obj/item/assembly/mousetrap/examine(mob/user) . = ..() . += "The pressure plate is [armed?"primed":"safe"]." +/obj/item/assembly/mousetrap/proc/update_host(force = FALSE) + var/obj/item/newhost + // Pick the first valid object in this list: + // Wiring datum's owner + // assembly holder's attached object + // assembly holder itself + // us + newhost = connected?.holder || holder?.master || holder || src + //only trigger step-on mode if the host is an item + if(!istype(newhost,/obj/item)) + if(host) + UnregisterSignal(host,COMSIG_MOVABLE_MOVED) + host = src + if(isturf(host_turf)) + UnregisterSignal(host_turf,COMSIG_ATOM_ENTERED) + host_turf = null + return + + // If host changed + if((newhost != host) || force) + if(host) + UnregisterSignal(host,COMSIG_MOVABLE_MOVED) + host = newhost + RegisterSignal(host,COMSIG_MOVABLE_MOVED, PROC_REF(holder_movement)) + + // If host moved + if((host_turf != host.loc) || force) + if(isturf(host_turf)) + UnregisterSignal(host_turf,COMSIG_ATOM_ENTERED) + host_turf = null + if(isturf(host.loc)) + host_turf = host.loc + RegisterSignal(host_turf,COMSIG_ATOM_ENTERED, PROC_REF(on_entered)) + else + host_turf = null + +/obj/item/assembly/mousetrap/holder_movement() + . = ..() + update_host() + /obj/item/assembly/mousetrap/activate() if(..()) armed = !armed @@ -36,31 +72,28 @@ playsound(src, 'sound/weapons/handcuffs.ogg', 30, TRUE, -3) /obj/item/assembly/mousetrap/update_icon() - if(armed) - icon_state = "mousetraparmed" - else - icon_state = "mousetrap" + icon_state = "mousetrap[armed ? "armed" : ""]" if(holder) holder.update_icon() /obj/item/assembly/mousetrap/on_attach() . = ..() - AddComponent(/datum/component/connect_loc_behalf, holder, holder_connections) + update_host() /obj/item/assembly/mousetrap/on_detach() . = ..() - qdel(GetComponent(/datum/component/connect_loc_behalf)) + update_host() /obj/item/assembly/mousetrap/proc/triggered(mob/target, type = "feet") if(!armed) return + armed = FALSE // moved to the top because you could trigger it more than once under some circumstances + update_icon() var/obj/item/bodypart/affecting = null if(ishuman(target)) var/mob/living/carbon/human/H = target if(HAS_TRAIT(H, TRAIT_PIERCEIMMUNE)) playsound(src, 'sound/effects/snap.ogg', 50, TRUE) - armed = FALSE - update_icon() pulse(FALSE) return FALSE switch(type) @@ -68,10 +101,14 @@ if(!H.shoes) affecting = H.get_bodypart(pick(BODY_ZONE_L_LEG, BODY_ZONE_R_LEG)) H.Paralyze(60) + else + to_chat(H, "Your [H.shoes] protects you from [src].") if(BODY_ZONE_PRECISE_L_HAND, BODY_ZONE_PRECISE_R_HAND) if(!H.gloves) affecting = H.get_bodypart(type) H.Stun(60) + else + to_chat(H, "Your [H.gloves] protects you from [src].") if(affecting) if(affecting.receive_damage(1, 0)) H.update_damage_overlays() @@ -80,10 +117,26 @@ visible_message("SPLAT!") M.splat() playsound(src, 'sound/effects/snap.ogg', 50, TRUE) - armed = FALSE - update_icon() pulse(FALSE) +/** + * clumsy_check: Sets off the mousetrap if handled by a clown (with some probability) + * + * Arguments: + * * user: The mob handling the trap + */ +/obj/item/assembly/mousetrap/proc/clumsy_check(mob/living/carbon/human/user) + if(!armed) + return FALSE + if((HAS_TRAIT(user, TRAIT_DUMB) || HAS_TRAIT(user, TRAIT_CLUMSY)) && prob(50)) + var/which_hand = BODY_ZONE_PRECISE_L_HAND + if(!(user.active_hand_index % 2)) + which_hand = BODY_ZONE_PRECISE_R_HAND + triggered(user, which_hand) + user.visible_message(("[user] accidentally sets off [src], breaking their fingers."), \ + ("You accidentally trigger [src]!")) + return TRUE + return FALSE /obj/item/assembly/mousetrap/attack_self(mob/living/carbon/human/user) if(!armed) @@ -154,6 +207,14 @@ visible_message("[src] is triggered by [AM].") triggered(null) +/obj/item/assembly/mousetrap/Destroy() + if(host) + UnregisterSignal(host,COMSIG_MOVABLE_MOVED) + host = null + if(isturf(host_turf)) + UnregisterSignal(host_turf,COMSIG_ATOM_ENTERED) + host_turf = null + return ..() /obj/item/assembly/mousetrap/armed icon_state = "mousetraparmed" diff --git a/code/modules/assembly/proximity.dm b/code/modules/assembly/proximity.dm index 967220f201e02..4d000b79b5d6f 100644 --- a/code/modules/assembly/proximity.dm +++ b/code/modules/assembly/proximity.dm @@ -4,13 +4,14 @@ icon_state = "prox" custom_materials = list(/datum/material/iron=800, /datum/material/glass=200) attachable = TRUE - - + drop_sound = 'sound/items/handling/component_drop.ogg' + pickup_sound = 'sound/items/handling/component_pickup.ogg' var/scanning = FALSE var/timing = FALSE var/time = 20 var/sensitivity = 0 var/hearing_range = 3 + ///Proximity monitor associated with this atom, needed for it to work after this rework /obj/item/assembly/prox_sensor/Initialize(mapload) . = ..() @@ -19,6 +20,7 @@ /obj/item/assembly/prox_sensor/Destroy() STOP_PROCESSING(SSobj, src) + QDEL_NULL(proximity_monitor) . = ..() /obj/item/assembly/prox_sensor/examine(mob/user) @@ -35,12 +37,35 @@ update_icon() return TRUE +/obj/item/assembly/prox_sensor/dropped() + . = ..() + // Pick the first valid object in this list: + // Wiring datum's owner + // assembly holder's attached object + // assembly holder itself + // us + proximity_monitor?.SetHost(connected?.holder || holder?.master || holder || src, src) + +/obj/item/assembly/prox_sensor/on_attach() + . = ..() + // Pick the first valid object in this list: + // Wiring datum's owner + // assembly holder's attached object + // assembly holder itself + // us + proximity_monitor.SetHost(connected?.holder || holder?.master || holder || src, src) + /obj/item/assembly/prox_sensor/on_detach() . = ..() if(!.) return else - proximity_monitor.SetHost(src,src) + // Pick the first valid object in this list: + // Wiring datum's owner + // assembly holder's attached object + // assembly holder itself + // us + proximity_monitor.SetHost(connected?.holder || holder?.master || holder || src, src) /obj/item/assembly/prox_sensor/toggle_secure() secured = !secured @@ -64,10 +89,11 @@ /obj/item/assembly/prox_sensor/proc/sense() if(!scanning || !secured || next_activate > world.time) return FALSE + next_activate = world.time + 30 pulse(FALSE) audible_message("[icon2html(src, hearers(src))] *beep* *beep* *beep*", null, hearing_range) playsound(get_turf(src), 'sound/machines/triple_beep.ogg', ASSEMBLY_BEEP_VOLUME, TRUE) - next_activate = world.time + 30 + return TRUE /obj/item/assembly/prox_sensor/process(delta_time) diff --git a/code/modules/assembly/signaler.dm b/code/modules/assembly/signaler.dm index ceb6c37234717..8b5409ca65362 100644 --- a/code/modules/assembly/signaler.dm +++ b/code/modules/assembly/signaler.dm @@ -6,18 +6,18 @@ lefthand_file = 'icons/mob/inhands/misc/devices_lefthand.dmi' righthand_file = 'icons/mob/inhands/misc/devices_righthand.dmi' custom_materials = list(/datum/material/iron=400, /datum/material/glass=120) - wires = WIRE_RECEIVE | WIRE_PULSE | WIRE_RADIO_PULSE | WIRE_RADIO_RECEIVE attachable = TRUE - - - var/code = DEFAULT_SIGNALER_CODE - var/frequency = FREQ_SIGNALER - var/datum/radio_frequency/radio_connection - ///Holds the mind that commited suicide. - var/datum/mind/suicider - ///Holds a reference string to the mob, decides how much of a gamer you are. - var/suicide_mob - var/hearing_range = 1 + drop_sound = 'sound/items/handling/component_drop.ogg' + pickup_sound = 'sound/items/handling/component_pickup.ogg' + + var/code = DEFAULT_SIGNALER_CODE /// The code sent by this signaler. + var/frequency = FREQ_SIGNALER /// The frequency this signaler is set to. + var/cooldown_length = 1 SECONDS /// How long of a cooldown exists on this signaller. + var/datum/radio_frequency/radio_connection /// The radio frequency connection this signaler is using. + var/datum/mind/suicider /// Holds the mind that commited suicide. + var/suicide_mob /// Holds a reference string to the mob, decides how much of a gamer you are. + var/hearing_range = 1 /// How many tiles away can you hear when this signaler is used or gets activated. + var/last_receive_signal_log /// String containing the last piece of logging data relating to when this signaller has received a signal. /obj/item/assembly/signaler/suicide_act(mob/living/carbon/user) user.visible_message("[user] eats \the [src]! If it is signaled, [user.p_they()] will die!") @@ -56,41 +56,43 @@ return TRUE /obj/item/assembly/signaler/update_icon() - if(holder) - holder.update_icon() - return + . = ..() + holder?.update_icon() -/obj/item/assembly/signaler/ui_status(mob/user) +/obj/item/assembly/signaler/ui_status(mob/user, datum/ui_state/state) if(is_secured(user)) return ..() return UI_CLOSE - -/obj/item/assembly/signaler/ui_state(mob/user) - return GLOB.hands_state - /obj/item/assembly/signaler/ui_interact(mob/user, datum/tgui/ui) ui = SStgui.try_update_ui(user, src, ui) if(!ui) - ui = new(user, src, "Signaler") + ui = new(user, src, "Signaler", name) ui.open() /obj/item/assembly/signaler/ui_data(mob/user) var/list/data = list() data["frequency"] = frequency + data["cooldown"] = cooldown_length data["code"] = code data["minFrequency"] = MIN_FREE_FREQ data["maxFrequency"] = MAX_FREE_FREQ - data["connection"] = !!radio_connection return data -/obj/item/assembly/signaler/ui_act(action, params) - if(..()) +/obj/item/assembly/signaler/ui_act(action, params, datum/tgui/ui) + . = ..() + if(.) return switch(action) if("signal") + if(cooldown_length > 0) + if(TIMER_COOLDOWN_CHECK(src, COOLDOWN_SIGNALLER_SEND)) + balloon_alert(ui.user, "recharging!") + return + TIMER_COOLDOWN_START(src, COOLDOWN_SIGNALLER_SEND, cooldown_length) INVOKE_ASYNC(src, PROC_REF(signal)) + balloon_alert(ui.user, "signaled") . = TRUE if("freq") var/new_frequency = sanitize_frequency(unformat_frequency(params["freq"]), TRUE) @@ -107,8 +109,7 @@ code = initial(code) . = TRUE - if(.) - update_icon() + update_icon() /obj/item/assembly/signaler/attackby(obj/item/W, mob/user, params) if(issignaler(W)) @@ -117,22 +118,20 @@ code = signaler2.code set_frequency(signaler2.frequency) to_chat(user, "You transfer the frequency and code of \the [signaler2.name] to \the [name]") - ui_update() ..() /obj/item/assembly/signaler/proc/signal() if(!radio_connection) return - var/datum/signal/signal = new(list("code" = code)) - radio_connection.post_signal(src, signal) - var/time = time2text(world.realtime,"hh:mm:ss") var/turf/T = get_turf(src) - if(usr) - GLOB.lastsignalers.Add("[time] : [usr.key] used [src] @ location ([T.x],[T.y],[T.z]) : with frequency: [format_frequency(frequency)]/[code]") - log_telecomms("[time] : [usr.key] used [src] @ location [AREACOORD(T)] : with frequency: [format_frequency(frequency)]/[code]") - message_admins(": [usr.key] used [src] @ location [AREACOORD(T)] : with frequency: [format_frequency(frequency)]/[code]") + + var/logging_data = "[time] : [key_name(usr)] used [src] @ location ([T.x],[T.y],[T.z]) : [format_frequency(frequency)]/[code]" + add_to_signaler_investigate_log(logging_data) + + var/datum/signal/signal = new(list("code" = code), logging_data = logging_data) + radio_connection.post_signal(src, signal) /obj/item/assembly/signaler/receive_signal(datum/signal/signal) . = FALSE @@ -140,42 +139,48 @@ return if(signal.data["code"] != code) return - if(!(src.wires & WIRE_RADIO_RECEIVE)) - return if(suicider) manual_suicide(suicider) return - pulse(TRUE) - audible_message("[icon2html(src, hearers(src))] *beep* *beep* *beep*", null, hearing_range) - playsound(get_turf(src), 'sound/machines/triple_beep.ogg', ASSEMBLY_BEEP_VOLUME, TRUE) + + // If the holder is a TTV, we want to store the last received signal to incorporate it into TTV logging, else wipe it. + last_receive_signal_log = istype(holder, /obj/item/transfer_valve) ? signal.logging_data : null + + pulse() + audible_message("[icon2html(src, hearers(src))] *beep* *beep* *beep*", null, hearing_range) + for(var/mob/hearing_mob in get_hearers_in_view(hearing_range, src)) + hearing_mob.playsound_local(get_turf(src), 'sound/machines/triple_beep.ogg', ASSEMBLY_BEEP_VOLUME, TRUE) return TRUE /obj/item/assembly/signaler/proc/set_frequency(new_frequency) SSradio.remove_object(src, frequency) frequency = new_frequency radio_connection = SSradio.add_object(src, frequency, RADIO_SIGNALER) + return -// Embedded signaller used in grenade construction. -// It's necessary because the signaler doens't have an off state. -// Generated during grenade construction. -Sayu -/obj/item/assembly/signaler/receiver - var/on = FALSE +/obj/item/assembly/signaler/cyborg -/obj/item/assembly/signaler/receiver/proc/toggle_safety() - on = !on +/obj/item/assembly/signaler/cyborg/attackby(obj/item/W, mob/user, params) + return +/obj/item/assembly/signaler/cyborg/screwdriver_act(mob/living/user, obj/item/I) + return -/obj/item/assembly/signaler/receiver/activate() - toggle_safety() - return TRUE +/obj/item/assembly/signaler/internal + name = "internal remote signaling device" -/obj/item/assembly/signaler/receiver/examine(mob/user) - . = ..() - . += "The radio receiver is [on?"on":"off"]." +/obj/item/assembly/signaler/internal/ui_state(mob/user) + return GLOB.inventory_state -/obj/item/assembly/signaler/receiver/receive_signal(datum/signal/signal) - if(!on) - return - return ..(signal) +/obj/item/assembly/signaler/internal/attackby(obj/item/W, mob/user, params) + return + +/obj/item/assembly/signaler/internal/screwdriver_act(mob/living/user, obj/item/I) + return + +/obj/item/assembly/signaler/internal/can_interact(mob/user) + if(ispAI(user)) + return TRUE + . = ..() // Embedded signaller used in anomalies. /obj/item/assembly/signaler/anomaly @@ -260,27 +265,3 @@ /obj/item/assembly/signaler/anomaly/attack_self() return - -/obj/item/assembly/signaler/cyborg - -/obj/item/assembly/signaler/cyborg/attackby(obj/item/W, mob/user, params) - return -/obj/item/assembly/signaler/cyborg/screwdriver_act(mob/living/user, obj/item/I) - return - -/obj/item/assembly/signaler/internal - name = "internal remote signaling device" - -/obj/item/assembly/signaler/internal/ui_state(mob/user) - return GLOB.inventory_state - -/obj/item/assembly/signaler/internal/attackby(obj/item/W, mob/user, params) - return - -/obj/item/assembly/signaler/internal/screwdriver_act(mob/living/user, obj/item/I) - return - -/obj/item/assembly/signaler/internal/can_interact(mob/user) - if(istype(user, /mob/living/silicon/pai)) - return TRUE - . = ..() diff --git a/code/modules/assembly/timer.dm b/code/modules/assembly/timer.dm index 830a8c32f8efc..df486b1e4144e 100644 --- a/code/modules/assembly/timer.dm +++ b/code/modules/assembly/timer.dm @@ -4,7 +4,8 @@ icon_state = "timer" custom_materials = list(/datum/material/iron=500, /datum/material/glass=50) attachable = TRUE - + drop_sound = 'sound/items/handling/component_drop.ogg' + pickup_sound = 'sound/items/handling/component_pickup.ogg' var/timing = FALSE var/time = 10 diff --git a/code/modules/holoparasite/holoparasite_barriers.dm b/code/modules/holoparasite/holoparasite_barriers.dm index d7d7273ad1959..af5f7b5e27ca6 100644 --- a/code/modules/holoparasite/holoparasite_barriers.dm +++ b/code/modules/holoparasite/holoparasite_barriers.dm @@ -47,7 +47,7 @@ if(WEST) start_pos = locate(min(sx + range + 1, world.maxx), max(sy - range, 1), sz) end_pos = locate(min(sx + range + 1, world.maxx), min(sy + range, world.maxy), sz) - for(var/turf/edge in get_line(start_pos, end_pos)) + for(var/turf/edge in getline(start_pos, end_pos)) ADD_BARRIER_IMAGE(edge, direction) ADD_BARRIER_IMAGE(locate(max(sx - range - 1, 1), sy + range + 1, sz), SOUTHEAST) diff --git a/code/modules/modular_computers/file_system/programs/signaller.dm b/code/modules/modular_computers/file_system/programs/signaller.dm index 6f9dfbf9b12d7..9b574a13abac6 100644 --- a/code/modules/modular_computers/file_system/programs/signaller.dm +++ b/code/modules/modular_computers/file_system/programs/signaller.dm @@ -14,6 +14,10 @@ var/signal_code = DEFAULT_SIGNALER_CODE /// Radio connection datum used by signallers. var/datum/radio_frequency/radio_connection + /// How long do we cooldown before we can send another signal? + var/signal_cooldown_time = 1 SECONDS + /// Cooldown store + COOLDOWN_DECLARE(signal_cooldown) /datum/computer_file/program/signaller/on_start(mob/living/user) . = ..() @@ -32,6 +36,7 @@ var/obj/item/computer_hardware/radio_card/sensor = computer?.get_modular_computer_part(MC_SIGNALLER) if(sensor?.check_functionality()) data["frequency"] = signal_frequency + data["cooldown"] = signal_cooldown_time data["code"] = signal_code data["minFrequency"] = MIN_FREE_FREQ data["maxFrequency"] = MAX_FREE_FREQ @@ -69,12 +74,20 @@ playsound(src, 'sound/machines/scanbuzz.ogg', 100, FALSE) return + if(!COOLDOWN_FINISHED(src, signal_cooldown)) + computer.balloon_alert(usr, "cooling down!") + return + + COOLDOWN_START(src, signal_cooldown, signal_cooldown_time) + computer.balloon_alert(usr, "signaled") + var/time = time2text(world.realtime,"hh:mm:ss") var/turf/T = get_turf(computer) if(usr) - GLOB.lastsignalers.Add("[time] : [usr.key] used [src] @ location ([T.x],[T.y],[T.z]) : with frequency: [format_frequency(signal_frequency)]/[signal_code]") log_telecomms("[time] : [usr.key] used [src] @ location [AREACOORD(T)] : with frequency: [format_frequency(signal_frequency)]/[signal_code]") message_admins(": [usr.key] used [src] @ location [AREACOORD(T)] : with frequency: [format_frequency(signal_frequency)]/[signal_code]") + var/logging_data = "[time] : [key_name(usr)] used the computer '[initial(computer.name)]' @ location ([T.x],[T.y],[T.z]) : [format_frequency(signal_frequency)]/[signal_code]" + add_to_signaler_investigate_log(logging_data) var/datum/signal/signal = new(list("code" = signal_code)) radio_connection.post_signal(src, signal) diff --git a/code/modules/reagents/reagent_dispenser.dm b/code/modules/reagents/reagent_dispenser.dm index 2d275eb4daed0..0221fce1e147f 100644 --- a/code/modules/reagents/reagent_dispenser.dm +++ b/code/modules/reagents/reagent_dispenser.dm @@ -9,6 +9,43 @@ max_integrity = 300 var/tank_volume = 1000 //In units, how much the dispenser can hold var/reagent_id = /datum/reagent/water //The ID of the reagent that the dispenser uses + var/obj/item/assembly_holder/rig = null // An assembly attached to the tank - if this dispenser accepts_rig + var/accepts_rig = FALSE // Whether this dispenser can be rigged with an assembly (and blown up with an igniter) + var/mutable_appearance/assembliesoverlay //overlay of attached assemblies + var/last_rigger = "" // The person who attached an assembly to this dispenser, for bomb logging purposes + +/obj/structure/reagent_dispensers/IsSpecialAssembly() // This check is necessary for assemblies to automatically detect that we are compatible + return accepts_rig + +/obj/structure/reagent_dispensers/Destroy() + QDEL_NULL(rig) + return ..() + +/** + * rig_boom: Wrapper to log when a reagent_dispenser is set off by an assembly + * + */ +/obj/structure/reagent_dispensers/proc/rig_boom() + log_bomber(last_rigger, "rigged [src] exploded", src) + boom() + +/obj/structure/reagent_dispensers/Initialize(mapload) + create_reagents(tank_volume, DRAINABLE | AMOUNT_VISIBLE) + if(reagent_id) + reagents.add_reagent(reagent_id, tank_volume) + . = ..() + +/obj/structure/reagent_dispensers/examine(mob/user) + . = ..() + if(accepts_rig && get_dist(user, src) <= 2) + if(rig) + . += ("There is some kind of device rigged to the tank!") + else + . += ("It looks like you could rig a device to the tank.") + for(var/assembly in rig.assemblies) + if(istype(assembly, /obj/item/assembly/timer)) + var/obj/item/assembly/timer/timer = assembly + . += "There is a timer [timer.timing ? "counting down from [timer.time]":"set for [timer.time] seconds"]." /obj/structure/reagent_dispensers/take_damage(damage_amount, damage_type = BRUTE, damage_flag = 0, sound_effect = 1, attack_dir, armour_penetration = 0) . = ..() @@ -19,20 +56,98 @@ /obj/structure/reagent_dispensers/attackby(obj/item/W, mob/user, params) if(W.is_refillable()) return 0 //so we can refill them via their afterattack. - else - return ..() + if(istype(W, /obj/item/assembly_holder) && accepts_rig) + if(rig) + balloon_alert(user, "another device is in the way!") + return ..() + var/obj/item/assembly_holder/holder = W + if(!(locate(/obj/item/assembly/igniter) in holder.assemblies)) + return ..() + + user.balloon_alert_to_viewers("attaching rig...") + add_fingerprint(user) + if(!do_after(user, 2 SECONDS, target = src) || !user.transferItemToLoc(holder, src)) + return + rig = holder + holder.master = src + holder.on_attach(src) + assembliesoverlay = holder + assembliesoverlay.pixel_x += 6 + assembliesoverlay.pixel_y += 1 + add_overlay(assembliesoverlay) + RegisterSignal(src, COMSIG_IGNITER_ACTIVATE, PROC_REF(rig_boom)) + log_bomber(user, "attached [holder.name] to ", src) + last_rigger = user + user.balloon_alert_to_viewers("attached rig") + return + return ..() -/obj/structure/reagent_dispensers/Initialize(mapload) - create_reagents(tank_volume, DRAINABLE | AMOUNT_VISIBLE) - if(reagent_id) - reagents.add_reagent(reagent_id, tank_volume) +/obj/structure/reagent_dispensers/Exited(atom/movable/gone, direction) . = ..() + if(gone == rig) + rig = null + +/obj/structure/reagent_dispensers/attack_hand(mob/user, list/modifiers) + . = ..() + if(. || !rig) + return + // mousetrap rigs only make sense if you can set them off, can't step on them + // If you see a mousetrap-rigged fuel tank, just leave it alone + rig.on_found() + if(QDELETED(src)) + return + user.balloon_alert_to_viewers("detaching rig...") + if(!do_after(user, 2 SECONDS, target = src)) + return + user.balloon_alert_to_viewers("detached rig") + user.log_message("detached [rig] from [src].", LOG_GAME) + if(!user.put_in_hands(rig)) + rig.forceMove(get_turf(user)) + rig = null + last_rigger = null + cut_overlays(assembliesoverlay) + UnregisterSignal(src, COMSIG_IGNITER_ACTIVATE) /obj/structure/reagent_dispensers/proc/boom() - visible_message("\The [src] ruptures!") - chem_splash(loc, 5, list(reagents)) + if(QDELETED(src)) + return // little bit of sanity sauce before we wreck ourselves somehow + var/datum/reagent/fuel/volatiles = reagents.has_reagent(/datum/reagent/fuel) + var/fuel_amt = 0 + if(istype(volatiles) && volatiles.volume >= 25) + fuel_amt = volatiles.volume + reagents.del_reagent(/datum/reagent/fuel) // not actually used for the explosion + if(reagents.total_volume) + if(!fuel_amt) + visible_message(("\The [src] ruptures!")) + // Leave it up to future terrorists to figure out the best way to mix reagents with fuel for a useful boom here + chem_splash(loc, null, 2 + (reagents.total_volume + fuel_amt) / 1000, list(reagents), extra_heat=(fuel_amt / 50),adminlog=(fuel_amt<25)) + + if(fuel_amt) // with that done, actually explode + visible_message(("\The [src] explodes!")) + // old code for reference: + // standard fuel tank = 1000 units = heavy_impact_range = 1, light_impact_range = 5, flame_range = 5 + // big fuel tank = 5000 units = devastation_range = 1, heavy_impact_range = 2, light_impact_range = 7, flame_range = 12 + // It did not account for how much fuel was actually in the tank at all, just the size of the tank. + // I encourage others to better scale these numbers in the future. + // As it stands this is a minor nerf in exchange for an easy bombing technique working that has been broken for a while. + switch(fuel_amt) + if(25 to 150) + explosion(src, light_impact_range = 1, flame_range = 2) + if(150 to 300) + explosion(src, light_impact_range = 2, flame_range = 3) + if(300 to 750) + explosion(src, heavy_impact_range = 1, light_impact_range = 3, flame_range = 5) + if(750 to 1500) + explosion(src, heavy_impact_range = 1, light_impact_range = 4, flame_range = 6) + if(1500 to INFINITY) + explosion(src, devastation_range = 1, heavy_impact_range = 2, light_impact_range = 6, flame_range = 8) qdel(src) +/obj/structure/reagent_dispensers/Moved(atom/old_loc, movement_dir) + . = ..() + if(rig) + rig.on_move(old_loc, movement_dir) + /obj/structure/reagent_dispensers/deconstruct(disassembled = TRUE) if(!(flags_1 & NODECONSTRUCT_1)) if(!disassembled) @@ -63,13 +178,7 @@ desc = "A tank full of industrial welding fuel. Do not consume." icon_state = "fuel" reagent_id = /datum/reagent/fuel - -/obj/structure/reagent_dispensers/fueltank/boom() - var/light_explosion_range = clamp(round(reagents.get_reagent_amount(/datum/reagent/fuel)/200, 1), 1, 5) //explosion range should decrease when there is less fuel in the tank - var/flame_explosion_range = clamp(light_explosion_range + 1, 1, 5) //Fire explosion is always one bigger than light explosion - var/heavy_explosion_range = round(light_explosion_range/5, 1) //if there is less than 500 fuel in the tank, no heavy explosion - explosion(get_turf(src), 0, heavy_explosion_range, light_explosion_range, flame_range = flame_explosion_range) - qdel(src) + accepts_rig = TRUE /obj/structure/reagent_dispensers/fueltank/blob_act(obj/structure/blob/B) boom() diff --git a/code/modules/wiremod/shell/assembly.dm b/code/modules/wiremod/shell/assembly.dm index a3f7952fb2f18..0966727d6662d 100644 --- a/code/modules/wiremod/shell/assembly.dm +++ b/code/modules/wiremod/shell/assembly.dm @@ -12,8 +12,6 @@ lefthand_file = 'icons/mob/inhands/misc/devices_lefthand.dmi' righthand_file = 'icons/mob/inhands/misc/devices_righthand.dmi' - securable = FALSE //This item should only ever be used as an assembly and the shell datum uses screwdriver_act, might as well make it permanently unsecured - light_system = MOVABLE_LIGHT light_range = 6 light_power = 1 diff --git a/code/modules/xenoarchaeology/xenoartifact.dm b/code/modules/xenoarchaeology/xenoartifact.dm index 4cf30749629c7..56b12f6b6f870 100644 --- a/code/modules/xenoarchaeology/xenoartifact.dm +++ b/code/modules/xenoarchaeology/xenoartifact.dm @@ -365,8 +365,8 @@ CREATION_TEST_IGNORE_SUBTYPES(/obj/item/xenoartifact) /obj/item/xenoartifact/proc/create_beam(atom/target) if((locate(src) in target?.contents) || !get_turf(target)) return - var/datum/beam/xenoa_beam/B = new((!isturf(loc) ? loc : src), target, time=1.5 SECONDS, beam_icon='icons/obj/xenoarchaeology/xenoartifact.dmi', beam_icon_state="xenoa_beam", btype=/obj/effect/ebeam/xenoa_ebeam) - B.set_color(material) + var/datum/beam/xenoa_beam/B = src.Beam(BeamTarget=target,icon_state = "xenoa_beam",icon='icons/obj/xenoarchaeology/xenoartifact.dmi', time = 1.5 SECONDS, beam_type=/obj/effect/ebeam/xenoa_ebeam) + B.beam_color = material INVOKE_ASYNC(B, TYPE_PROC_REF(/datum/beam/xenoa_beam, Start)) ///Default template used to interface with activator signals. @@ -466,10 +466,6 @@ CREATION_TEST_IGNORE_SUBTYPES(/obj/item/xenoartifact/objective) name = "artifact beam" /datum/beam/xenoa_beam - var/color - -/datum/beam/xenoa_beam/proc/set_color(col) //Custom proc to set beam colour - color = col /datum/beam/xenoa_beam/Draw() var/Angle = round(get_angle(origin,target)) @@ -487,7 +483,7 @@ CREATION_TEST_IGNORE_SUBTYPES(/obj/item/xenoartifact/objective) if(QDELETED(src)) break var/obj/effect/ebeam/xenoa_ebeam/X = new(origin_turf) // Start Xenoartifact - This assigns colour to the beam - X.color = color + X.color = beam_color X.owner = src elements += X // End Xenoartifact diff --git a/tgui/packages/tgui/interfaces/Signaler.js b/tgui/packages/tgui/interfaces/Signaler.js index 7ac6fda3a58c9..42243419605ca 100644 --- a/tgui/packages/tgui/interfaces/Signaler.js +++ b/tgui/packages/tgui/interfaces/Signaler.js @@ -15,7 +15,7 @@ export const Signaler = (props, context) => { export const SignalerContent = (props, context) => { const { act, data } = useBackend(context); - const { code, frequency, minFrequency, maxFrequency, connection } = data; + const { code, frequency, cooldown, minFrequency, maxFrequency } = data; return (
@@ -91,10 +91,10 @@ export const SignalerContent = (props, context) => {