diff --git a/code/game/objects/effects/effect_system/effects_sparks.dm b/code/game/objects/effects/effect_system/effects_sparks.dm index 611103cbb917..313470343d19 100644 --- a/code/game/objects/effects/effect_system/effects_sparks.dm +++ b/code/game/objects/effects/effect_system/effects_sparks.dm @@ -25,6 +25,9 @@ light_power = 0.5 light_color = LIGHT_COLOR_FIRE light_flags = LIGHT_NO_LUMCOUNT + var/duration = 2 SECONDS + var/volume = 100 + var/sound = "sparks" /obj/effect/particle_effect/sparks/Initialize(mapload) ..() @@ -32,11 +35,11 @@ /obj/effect/particle_effect/sparks/LateInitialize() flick(icon_state, src) // replay the animation - playsound(src, "sparks", 100, TRUE) + playsound(src, sound, volume, TRUE) var/turf/T = loc if(isturf(T)) T.hotspot_expose(1000,100) - QDEL_IN(src, 2 SECONDS) + QDEL_IN(src, duration) /obj/effect/particle_effect/sparks/Destroy() var/turf/T = loc @@ -63,6 +66,13 @@ name = "lightning" icon_state = "electricity" +/obj/effect/particle_effect/sparks/electricity/short //used for wirecrawling + name = "lightning" + icon_state = "electricity" + duration = 8 + volume = 40 + sound = "softsparks" + /obj/effect/particle_effect/sparks/quantum name = "quantum sparks" icon_state = "quantum_sparks" diff --git a/code/game/objects/items/granters/martial_arts/racial.dm b/code/game/objects/items/granters/martial_arts/racial.dm index 473d0d32a903..1364fdef2d1a 100644 --- a/code/game/objects/items/granters/martial_arts/racial.dm +++ b/code/game/objects/items/granters/martial_arts/racial.dm @@ -153,3 +153,36 @@ var/obj/item/reagent_containers/glass/bottle/vial/empty = new(get_turf(user)) qdel(src) user.put_in_active_hand(empty) + + +/obj/item/book/granter/action/wirecrawl + name = "modified yellow slime extract" + desc = "An experimental yellow slime extract that when absorbed by an Ethereal, grants control over electrical powers." + icon = 'icons/mob/slimes.dmi' + icon_state = "yellow slime extract" + granted_action = /datum/action/cooldown/spell/jaunt/wirecrawl + action_name = "Wirecrawling" + drop_sound = null + pickup_sound = null + remarks = list("Shock...", "Zap...", "High Voltage...", "Dissolve...", "Dissipate...", "Disperse...", "Red Hot...", "Spiral...", "Electro-magnetic...", "Turbo...") + book_sounds = list('sound/effects/sparks1.ogg','sound/effects/sparks2.ogg','sound/effects/sparks3.ogg') + var/admin = FALSE + +/obj/item/book/granter/action/wirecrawl/on_reading_start(mob/user) + to_chat(user, span_notice("You hold \the [src] directly to your chest...")) + +/obj/item/book/granter/action/wirecrawl/can_learn(mob/user) + if(isethereal(user) || admin) + return ..() + to_chat(user, span_warning("Yup, that's a slime extract alright.")) + return FALSE + +/obj/item/book/granter/action/wirecrawl/on_reading_finished(mob/living/carbon/user) + ..() + if(!uses) + name = "grey slime extract" + desc = "Goo extracted from a slime. Legends claim these to have \"magical powers\"." + icon_state = "grey slime extract" + +/obj/item/book/granter/action/wirecrawl/admin //if someone wants to spawn it in + admin = TRUE diff --git a/code/game/sound.dm b/code/game/sound.dm index 6487f4aaab84..7d6f0a7691ad 100644 --- a/code/game/sound.dm +++ b/code/game/sound.dm @@ -215,6 +215,8 @@ distance_multiplier - Can be used to multiply the distance at which the sound is soundin = pick('sound/effects/creak1.ogg', 'sound/effects/creak2.ogg', 'sound/effects/creak3.ogg') if ("sparks") soundin = pick('sound/effects/sparks1.ogg','sound/effects/sparks2.ogg','sound/effects/sparks3.ogg','sound/effects/sparks4.ogg') + if ("softsparks")//doesn't have the cracking sound of sparks 4 + soundin = pick('sound/effects/sparks1.ogg','sound/effects/sparks2.ogg','sound/effects/sparks3.ogg') if ("rustle") soundin = pick('sound/effects/rustle1.ogg','sound/effects/rustle2.ogg','sound/effects/rustle3.ogg','sound/effects/rustle4.ogg','sound/effects/rustle5.ogg') if ("bodyfall") diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm index ecd70371f442..fe3ef2db1ab6 100644 --- a/code/modules/mob/living/living.dm +++ b/code/modules/mob/living/living.dm @@ -1432,6 +1432,7 @@ else clear_fullscreen("remote_view", 0) update_pipe_vision() + update_wire_vision() /mob/living/vv_edit_var(var_name, var_value) switch(var_name) diff --git a/code/modules/mob/living/living_defines.dm b/code/modules/mob/living/living_defines.dm index 6c11bbeb2d3a..49e6ad535ad6 100644 --- a/code/modules/mob/living/living_defines.dm +++ b/code/modules/mob/living/living_defines.dm @@ -69,6 +69,7 @@ var/has_limbs = 0 //does the mob have distinct limbs?(arms,legs, chest,head) var/list/pipes_shown = list() + var/list/wires_shown = list() var/last_played_vent var/smoke_delay = FALSE //used to prevent spam with smoke reagent reaction on mob. diff --git a/code/modules/power/cable.dm b/code/modules/power/cable.dm index 0563410f38f9..ece72e77d1c8 100644 --- a/code/modules/power/cable.dm +++ b/code/modules/power/cable.dm @@ -44,6 +44,8 @@ By design, d1 is the smallest direction and d2 is the highest pipe_group = "cable-[cable_color]"\ ) + var/image/wire_vision_img //specifically for wirecrawling + /obj/structure/cable/yellow cable_color = "yellow" color = "#ffff00" @@ -96,6 +98,8 @@ By design, d1 is the smallest direction and d2 is the highest if(powernet) cut_cable_from_powernet() // update the powernets GLOB.cable_list -= src //remove it from global cable list + if(wire_vision_img) + qdel(wire_vision_img) return ..() // then go ahead and delete the cable /obj/structure/cable/deconstruct(disassembled = TRUE) diff --git a/code/modules/power/power.dm b/code/modules/power/power.dm index 405273ab02ce..a65ecc3426b7 100644 --- a/code/modules/power/power.dm +++ b/code/modules/power/power.dm @@ -15,6 +15,14 @@ idle_power_usage = 0 active_power_usage = 0 + var/image/wire_vision_img //specifically for wirecrawling + +/obj/machinery/power/Destroy() + if(wire_vision_img) + qdel(wire_vision_img) + disconnect_from_network() + return ..() + /////////////////////////////// // General procedures ////////////////////////////// diff --git a/code/modules/spells/spell_types/jaunt/wirecrawl.dm b/code/modules/spells/spell_types/jaunt/wirecrawl.dm new file mode 100644 index 000000000000..ba0ec9a5d277 --- /dev/null +++ b/code/modules/spells/spell_types/jaunt/wirecrawl.dm @@ -0,0 +1,252 @@ +/** + * ### Blood Crawl + * + * Lets the caster enter and exit pools of blood. + */ +/datum/action/cooldown/spell/jaunt/wirecrawl + name = "Wire Crawl" + desc = "Allows you to break your body down into electricity and travel through wires." + + button_icon = 'icons/effects/effects.dmi' + button_icon_state = "electricity2" + + spell_requirements = NONE + antimagic_flags = NONE + panel = null + check_flags = AB_CHECK_CONSCIOUS|AB_CHECK_INCAPACITATED + + /// The time it takes to enter blood + var/enter_blood_time = 3 SECONDS + /// The time it takes to exit blood + var/exit_blood_time = 0 SECONDS + /// The radius around us that we look for wires in + var/enter_radius = 1 + /// If TRUE, we equip "wire crawl" hands to the jaunter to prevent using items + var/equip_wire_hands = TRUE + + jaunt_type = /obj/effect/dummy/phased_mob/wirecrawl + +/datum/action/cooldown/spell/jaunt/wirecrawl/Grant(mob/grant_to) + . = ..() + RegisterSignal(grant_to, COMSIG_MOVABLE_MOVED, PROC_REF(update_status_on_signal)) + +/datum/action/cooldown/spell/jaunt/wirecrawl/Remove(mob/remove_from) + . = ..() + UnregisterSignal(remove_from, COMSIG_MOVABLE_MOVED) + +/datum/action/cooldown/spell/jaunt/wirecrawl/can_cast_spell(feedback = TRUE) + . = ..() + if(!.) + return FALSE + if(find_nearby_power(get_turf(owner))) + return TRUE + if(feedback) + to_chat(owner, span_warning("There must be a nearby power outlet!")) + return FALSE + +/datum/action/cooldown/spell/jaunt/wirecrawl/cast(mob/living/cast_on) + . = ..() + // Should always return something because we checked that in can_cast_spell before arriving here + var/obj/structure/cable/wire = find_nearby_power(get_turf(cast_on)) + do_wirecrawl(wire, cast_on) + +/// Returns a nearby blood decal, or null if there aren't any +/datum/action/cooldown/spell/jaunt/wirecrawl/proc/find_nearby_power(turf/origin) + var/machinery = FALSE + for(var/obj/machinery/power/thing in range(enter_radius, origin)) + machinery = TRUE + for(var/obj/structure/cable/thing in range(enter_radius, origin)) + if(machinery || !thing.invisibility)//only enter it if it's not covered by a tile or is connected to a power machinery + return thing + return null + +/** + * Attempts to enter or exit the passed blood pool. + * Returns TRUE if we successfully entered or exited said pool, FALSE otherwise + */ +/datum/action/cooldown/spell/jaunt/wirecrawl/proc/do_wirecrawl(obj/structure/cable/wire, mob/living/jaunter) + if(is_jaunting(jaunter)) + . = try_exit_jaunt(wire, jaunter) + else + . = try_enter_jaunt(wire, jaunter) + + if(!.) + reset_spell_cooldown() + to_chat(jaunter, span_warning("You are unable to wire crawl!")) + +/** + * Attempts to enter the passed blood pool. + * If forced is TRUE, it will override enter_blood_time. + */ +/datum/action/cooldown/spell/jaunt/wirecrawl/proc/try_enter_jaunt(obj/structure/cable/wire, mob/living/jaunter, forced = FALSE) + if(!forced) + if(enter_blood_time > 0 SECONDS) + do_sparks(5, FALSE, jaunter) + wire.visible_message(span_warning("[jaunter] starts to sink into [wire]!")) + if(!do_after(jaunter, enter_blood_time, wire)) + return FALSE + + // The actual turf we enter + var/turf/jaunt_turf = get_turf(wire) + + // Begin the jaunt + jaunter.notransform = TRUE + var/obj/effect/dummy/phased_mob/wirecrawl/holder = enter_jaunt(jaunter, jaunt_turf) + if(!holder) + jaunter.notransform = FALSE + return FALSE + + RegisterSignal(holder, COMSIG_MOVABLE_MOVED, PROC_REF(update_status_on_signal)) + if(equip_wire_hands && iscarbon(jaunter)) + jaunter.drop_all_held_items() + // Give them some bloody hands to prevent them from doing things + var/obj/item/wirecrawl/left_hand = new(jaunter) + var/obj/item/wirecrawl/right_hand = new(jaunter) + jaunter.put_in_hands(left_hand) + jaunter.put_in_hands(right_hand) + + wire.visible_message(span_warning("[jaunter] zips into [wire]!")) + jaunter.extinguish_mob() + + jaunter.sight |= (SEE_TURFS|BLIND) + jaunter.add_wirevision(wire) + holder.travelled = wire.powernet + do_sparks(10, FALSE, jaunter) + + jaunter.notransform = FALSE + return TRUE + +/** + * Attempts to Exit the passed blood pool. + * If forced is TRUE, it will override exit_blood_time, and if we're currently consuming someone. + */ +/datum/action/cooldown/spell/jaunt/wirecrawl/proc/try_exit_jaunt(obj/structure/cable/wire, mob/living/jaunter, forced = FALSE) + if(!forced) + if(jaunter.notransform) + to_chat(jaunter, span_warning("You cannot exit yet!!")) + return FALSE + + if(exit_blood_time > 0 SECONDS) + wire.visible_message(span_warning("[wire] starts to crackle...")) + do_sparks(5, FALSE, jaunter) + if(!do_after(jaunter, exit_blood_time, wire)) + return FALSE + + if(!exit_jaunt(jaunter, get_turf(wire))) + return FALSE + + jaunter.sight &= ~(SEE_TURFS|BLIND) + jaunter.remove_wirevision() + do_sparks(10, FALSE, jaunter) + + wire.visible_message(span_boldwarning("[jaunter] zips out of [wire]!")) + return TRUE + +/datum/action/cooldown/spell/jaunt/wirecrawl/on_jaunt_exited(obj/effect/dummy/phased_mob/jaunt, mob/living/unjaunter) + UnregisterSignal(jaunt, COMSIG_MOVABLE_MOVED) + if(equip_wire_hands && iscarbon(unjaunter)) + for(var/obj/item/wirecrawl/wire_hand in unjaunter.held_items) + unjaunter.temporarilyRemoveItemFromInventory(wire_hand, force = TRUE) + qdel(wire_hand) + return ..() + +/obj/effect/dummy/phased_mob/wirecrawl + name = "wire" + ///keep track of the powernet we're in + var/datum/powernet/travelled + +/obj/effect/dummy/phased_mob/wirecrawl/Initialize(mapload, atom/movable/jaunter) + . = ..() + START_PROCESSING(SSprocessing, src) + +/obj/effect/dummy/phased_mob/wirecrawl/Destroy() + STOP_PROCESSING(SSprocessing, src) + return ..() + +/obj/effect/dummy/phased_mob/wirecrawl/phased_check(mob/living/user, direction) + var/turf/oldloc = get_turf(user) + var/obj/structure/cable/current = locate() in oldloc + if(!current || !istype(current) || !travelled)//if someone snips the wire you're currently in, or it gets destroyed in some way, get out + eject_jaunter() + return + + var/turf/newloc = ..() + for(var/obj/structure/cable/wire in newloc) + if(travelled != wire.powernet) //no jumping to a different powernet + continue + user.update_wire_vision(wire) + new /obj/effect/particle_effect/sparks/electricity/short(get_turf(user)) + return newloc + +/obj/effect/dummy/phased_mob/wirecrawl/process(delta_time)//so if the existing wire is destroyed, they are forced out + . = ..() + var/turf/currentloc = get_turf(jaunter) + var/obj/structure/cable/current = locate() in currentloc + if(!current || !istype(current) || !travelled)//if someone snips the wire you're currently in, or it gets destroyed in some way, get out + eject_jaunter() + return + +/obj/effect/dummy/phased_mob/wirecrawl/eject_jaunter() + var/mob/living/ejected = jaunter + ejected.remove_wirevision() + . = ..() + +//vision +/mob/living/proc/update_wire_vision(var/obj/structure/cable/wire) + if(!wire) //if there's no wire, grab a random one in the location + wire = locate() in get_turf(src) + remove_wirevision() + if(!wire) //if there's STILL no wire, just give up + return + add_wirevision(wire) + +/mob/living/proc/add_wirevision(obj/structure/cable/wire) + if(!wire || !istype(wire) || !wire.powernet) + return + var/list/totalMembers = list() + + var/datum/powernet/P = wire.powernet + if(P) + totalMembers += P.nodes + totalMembers += P.cables + + if(!totalMembers.len) + return + + if(client) + for(var/object in totalMembers)//cables and power machinery are not the same unfortunately + if(in_view_range(client.mob, object)) + if(istype(object, /obj/structure/cable)) + var/obj/structure/cable/display = object + if(!display.wire_vision_img) + display.wire_vision_img = image(display, display.loc, layer = ABOVE_HUD_LAYER, dir = display.dir) + display.wire_vision_img.plane = ABOVE_HUD_PLANE + client.images += display.wire_vision_img + wires_shown += display.wire_vision_img + + else if(istype(object, /obj/machinery/power)) + var/obj/machinery/power/display = object + if(!display.wire_vision_img) + display.wire_vision_img = image(display, display.loc, layer = ABOVE_HUD_LAYER, dir = display.dir) + display.wire_vision_img.plane = ABOVE_HUD_PLANE + client.images += display.wire_vision_img + wires_shown += display.wire_vision_img + +/mob/living/proc/remove_wirevision() + if(client) + for(var/image/current_image in wires_shown) + client.images -= current_image + wires_shown.len = 0 + + +/obj/item/wirecrawl + name = "wire crawl" + desc = "You are unable to hold anything while in this form." + icon = 'icons/effects/effects.dmi' + icon_state = "electricity2" + item_flags = ABSTRACT | DROPDEL + +/obj/item/wirecrawl/Initialize(mapload) + . = ..() + ADD_TRAIT(src, TRAIT_NODROP, ABSTRACT_ITEM_TRAIT) + diff --git a/yogstation.dme b/yogstation.dme index 5f8fc3713f78..86bed317399c 100644 --- a/yogstation.dme +++ b/yogstation.dme @@ -3534,6 +3534,7 @@ #include "code\modules\spells\spell_types\jaunt\bloodcrawl.dm" #include "code\modules\spells\spell_types\jaunt\ethereal_jaunt.dm" #include "code\modules\spells\spell_types\jaunt\shadow_walk.dm" +#include "code\modules\spells\spell_types\jaunt\wirecrawl.dm" #include "code\modules\spells\spell_types\list_target\_list_target.dm" #include "code\modules\spells\spell_types\list_target\telepathy.dm" #include "code\modules\spells\spell_types\pointed\_pointed.dm" diff --git a/yogstation/code/modules/uplink/uplink_item.dm b/yogstation/code/modules/uplink/uplink_item.dm index b370d4697899..8fecb4497711 100644 --- a/yogstation/code/modules/uplink/uplink_item.dm +++ b/yogstation/code/modules/uplink/uplink_item.dm @@ -185,6 +185,13 @@ restricted_species = list("ipc") include_objectives = list(/datum/objective/hijack, /datum/objective/martyr, /datum/objective/nuclear) // designed around mass murder, no need to use this if you aren't allowed to do that +/datum/uplink_item/race_restricted/wirecrawl + name = "Modified yellow slime extract" + desc = "An experimental yellow slime extract that when absorbed by an Ethereal, grants control over electrical powers." + cost = 8 + item = /obj/item/book/granter/action/wirecrawl + restricted_species = list("ethereal") + /datum/uplink_item/stealthy_weapons/camera_flash name = "Camera Flash" desc = "A camera with an upgraded flashbulb. Can be used much like a handheld flash except with a longer cooldown between uses, allowing the bulb to cool down — avoiding burning out altogether."