diff --git a/code/__HELPERS/traits.dm b/code/__HELPERS/traits.dm
index 922200948bb..efe5a33b3da 100644
--- a/code/__HELPERS/traits.dm
+++ b/code/__HELPERS/traits.dm
@@ -199,6 +199,7 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
/// Show what machine/door wires do when held.
#define TRAIT_SHOW_WIRE_INFO "show_wire_info"
#define TRAIT_BUTCHERS_HUMANS "butchers_humans"
+#define TRAIT_CMAGGED "cmagged"
//
// common trait sources
diff --git a/code/_globalvars/traits.dm b/code/_globalvars/traits.dm
index fa009ae3102..015cfe38f2a 100644
--- a/code/_globalvars/traits.dm
+++ b/code/_globalvars/traits.dm
@@ -71,7 +71,8 @@ GLOBAL_LIST_INIT(traits_by_type, list(
),
/obj/item = list(
"TRAIT_SHOW_WIRE_INFO" = TRAIT_SHOW_WIRE_INFO,
- "TRAIT_BUTCHER_HUMANS" = TRAIT_BUTCHERS_HUMANS
+ "TRAIT_BUTCHER_HUMANS" = TRAIT_BUTCHERS_HUMANS,
+ "TRAIT_CMAGGED" = TRAIT_CMAGGED
)
))
diff --git a/code/datums/uplink_item.dm b/code/datums/uplink_item.dm
index c394b3c37fc..4d61e94048a 100644
--- a/code/datums/uplink_item.dm
+++ b/code/datums/uplink_item.dm
@@ -182,6 +182,15 @@ GLOBAL_LIST_INIT(uplink_items, subtypesof(/datum/uplink_item))
surplus = 75
job = list("Clown")
+/datum/uplink_item/jobspecific/cmag
+ name = "Jestographic Sequencer"
+ desc = "The jestographic sequencer, also known as a cmag, is a small card that inverts the access on any door it's used on. Perfect for locking command out of their own departments. Honk!"
+ reference = "CMG"
+ item = /obj/item/card/cmag
+ cost = 4
+ surplus = 75
+ job = list("Clown")
+
/datum/uplink_item/jobspecific/trick_revolver
name = "Trick Revolver"
desc = "A revolver that will fire backwards and kill whoever attempts to use it. Perfect for those pesky vigilante or just a good laugh."
diff --git a/code/game/atoms.dm b/code/game/atoms.dm
index 90848fc9a12..8e941dfd4bb 100644
--- a/code/game/atoms.dm
+++ b/code/game/atoms.dm
@@ -423,7 +423,7 @@
SHOULD_CALL_PARENT(TRUE)
if(updates & NONE)
- return // NONE is being sent on purpose, and thus no signal should be sent.
+ return // NONE is being sent on purpose, and thus no signal should be sent.
updates &= ~SEND_SIGNAL(src, COMSIG_ATOM_UPDATE_ICON, updates)
if(updates & UPDATE_ICON_STATE)
@@ -514,6 +514,9 @@
/atom/proc/unemag()
return
+/atom/proc/cmag_act()
+ return
+
/**
* Respond to a radioactive wave hitting this atom
*
diff --git a/code/game/machinery/doors/airlock.dm b/code/game/machinery/doors/airlock.dm
index 54b52dade9c..9d6f100ad6c 100644
--- a/code/game/machinery/doors/airlock.dm
+++ b/code/game/machinery/doors/airlock.dm
@@ -605,6 +605,8 @@ GLOBAL_LIST_EMPTY(airlock_emissive_underlays)
. = ..()
if(emagged)
. += "Its access panel is smoking slightly."
+ if(HAS_TRAIT(src, TRAIT_CMAGGED))
+ . += "The access panel is coated in yellow ooze..."
if(note)
if(!in_range(user, src))
. += "There's a [note.name] pinned to the front. You can't [note_type() == "note" ? "read" : "see"] it from here."
@@ -802,7 +804,7 @@ GLOBAL_LIST_EMPTY(airlock_emissive_underlays)
/obj/machinery/door/airlock/proc/ai_control_check(mob/user)
if(!issilicon(user))
return TRUE
- if(emagged)
+ if(emagged || HAS_TRAIT(src, TRAIT_CMAGGED))
to_chat(user, "Unable to interface: Internal error.")
return FALSE
if(!canAIControl())
@@ -1336,6 +1338,19 @@ GLOBAL_LIST_EMPTY(airlock_emissive_underlays)
emagged = TRUE
return 1
+/obj/machinery/door/airlock/cmag_act(mob/user)
+ if(operating || HAS_TRAIT(src, TRAIT_CMAGGED) || !density || !arePowerSystemsOn())
+ return
+ operating = TRUE
+ update_icon(AIRLOCK_EMAG, 1)
+ sleep(6)
+ if(QDELETED(src))
+ return
+ operating = FALSE
+ update_icon(AIRLOCK_CLOSED, 1)
+ ADD_TRAIT(src, TRAIT_CMAGGED, "clown_emag")
+ return TRUE
+
/obj/machinery/door/airlock/emp_act(severity)
. = ..()
if(prob(20 / severity))
diff --git a/code/game/machinery/doors/door.dm b/code/game/machinery/doors/door.dm
index 289e0098b21..c6f82284212 100644
--- a/code/game/machinery/doors/door.dm
+++ b/code/game/machinery/doors/door.dm
@@ -36,6 +36,9 @@
var/unres_sides = 0
//Multi-tile doors
var/width = 1
+ //Whether nonstandard door sounds (cmag laughter) are off cooldown.
+ var/sound_ready = TRUE
+ var/sound_cooldown = 1 SECONDS
/// ID for the window tint button, or another external control
var/id
@@ -116,8 +119,14 @@
if(world.time - mecha.occupant.last_bumped <= 10)
return
if(mecha.occupant && allowed(mecha.occupant) || check_access_list(mecha.operation_req_access))
+ if(HAS_TRAIT(src, TRAIT_CMAGGED))
+ cmag_switch(FALSE, mecha.occupant)
+ return
open()
else
+ if(HAS_TRAIT(src, TRAIT_CMAGGED))
+ cmag_switch(TRUE, mecha.occupant)
+ return
do_animate("deny")
return
@@ -149,6 +158,9 @@
if(density && !emagged)
if(allowed(user))
+ if(HAS_TRAIT(src, TRAIT_CMAGGED))
+ cmag_switch(FALSE, user)
+ return
open()
if(isbot(user))
var/mob/living/simple_animal/bot/B = user
@@ -156,6 +168,9 @@
else
if(pry_open_check(user))
return
+ if(HAS_TRAIT(src, TRAIT_CMAGGED))
+ cmag_switch(TRUE, user)
+ return
do_animate("deny")
/obj/machinery/door/proc/pry_open_check(mob/user)
@@ -204,10 +219,18 @@
return
if(requiresID() && (allowed(user) || user.can_advanced_admin_interact()))
if(density)
+ if(HAS_TRAIT(src, TRAIT_CMAGGED) && !user.can_advanced_admin_interact()) //cmag should not prevent admin intervention
+ cmag_switch(FALSE, user)
+ return
open()
else
+ if(HAS_TRAIT(src, TRAIT_CMAGGED) && !user.can_advanced_admin_interact())
+ return
close()
return TRUE
+ if(HAS_TRAIT(src, TRAIT_CMAGGED))
+ cmag_switch(TRUE, user)
+ return
if(density)
do_animate("deny")
@@ -226,7 +249,28 @@
/obj/machinery/door/proc/try_to_crowbar(mob/user, obj/item/I)
return
+/obj/machinery/door/proc/clean_cmag_ooze(obj/item/I, mob/user) //Emags are Engineering's problem, cmags are the janitor's problem
+ var/cleaning = FALSE
+ if(istype(I, /obj/item/reagent_containers/spray/cleaner))
+ var/obj/item/reagent_containers/spray/cleaner/C = I
+ if(C.reagents.total_volume >= C.amount_per_transfer_from_this)
+ cleaning = TRUE
+ else
+ return
+ if(istype(I, /obj/item/soap))
+ cleaning = TRUE
+
+ if(!cleaning)
+ return
+ user.visible_message("[user] starts to clean the ooze off the access panel.", "You start to clean the ooze off the access panel.")
+ if(do_after(user, 50, target = src))
+ user.visible_message("[user] cleans the ooze off [src].", "You clean the ooze off [src].")
+ REMOVE_TRAIT(src, TRAIT_CMAGGED, "clown_emag")
+
/obj/machinery/door/attackby(obj/item/I, mob/user, params)
+ if(HAS_TRAIT(src, TRAIT_CMAGGED))
+ clean_cmag_ooze(I, user)
+
if(user.a_intent != INTENT_HARM && istype(I, /obj/item/twohanded/fireaxe))
try_to_crowbar(user, I)
return 1
@@ -235,7 +279,6 @@
return 1
return ..()
-
/obj/machinery/door/crowbar_act(mob/user, obj/item/I)
if(user.a_intent == INTENT_HARM)
return
@@ -272,6 +315,46 @@
emagged = TRUE
return TRUE
+/obj/machinery/door/cmag_act(mob/user)
+ if(!density)
+ return
+ flick("door_spark", src)
+ sleep(6) //The cmag doesn't automatically open doors. It inverts access, not provides it!
+ ADD_TRAIT(src, TRAIT_CMAGGED, "clown_emag")
+ return TRUE
+
+//Proc for inverting access on cmagged doors."canopen" should always return the OPPOSITE of the normal result.
+/obj/machinery/door/proc/cmag_switch(canopen, mob/living/user)
+ if(!canopen || locked || !hasPower())
+ if(density) //Windoors can still do their deny animation in unpowered environments, this bugs out if density isn't checked for
+ do_animate("deny")
+ if(hasPower() && sound_ready)
+ playsound(loc, 'sound/machines/honkbot_evil_laugh.ogg', 25, TRUE, ignore_walls = FALSE)
+ soundcooldown()
+ return
+ if(ishuman(user))
+ var/mob/living/carbon/human/H = user
+ var/obj/item/card/id/idcard = H.get_idcard()
+ if(!idcard?.assignment) //Humans can't game inverted access by taking their ID off or using spare IDs.
+ if(!density)
+ return
+ do_animate("deny")
+ to_chat(H, "The airlock speaker chuckles: 'What's wrong, pal? Lost your ID? Nyuk nyuk nyuk!'")
+ if(sound_ready)
+ playsound(loc, 'sound/machines/honkbot_evil_laugh.ogg', 25, TRUE, ignore_walls = FALSE)
+ soundcooldown() //Thanks, mechs
+ return
+ if(density)
+ open()
+ else
+ close()
+
+/obj/machinery/door/proc/soundcooldown()
+ if(!sound_ready)
+ return
+ sound_ready = FALSE
+ addtimer(VARSET_CALLBACK(src, sound_ready, TRUE), sound_cooldown)
+
/obj/machinery/door/proc/toggle_polarization()
return
diff --git a/code/game/machinery/doors/windowdoor.dm b/code/game/machinery/doors/windowdoor.dm
index 07e4d7a5a26..14b3fdcc979 100644
--- a/code/game/machinery/doors/windowdoor.dm
+++ b/code/game/machinery/doors/windowdoor.dm
@@ -62,6 +62,8 @@
. = ..()
if(emagged)
. += "Its access panel is smoking slightly."
+ if(HAS_TRAIT(src, TRAIT_CMAGGED))
+ . += "The access panel is coated in yellow ooze..."
/obj/machinery/door/window/emp_act(severity)
. = ..()
@@ -83,8 +85,14 @@
if(ismecha(AM))
var/obj/mecha/mecha = AM
if(mecha.occupant && allowed(mecha.occupant))
+ if(HAS_TRAIT(src, TRAIT_CMAGGED))
+ cmag_switch(FALSE)
+ return
open_and_close()
else
+ if(HAS_TRAIT(src, TRAIT_CMAGGED))
+ cmag_switch(TRUE)
+ return
do_animate("deny")
return
if(!SSticker)
@@ -98,8 +106,14 @@
return
add_fingerprint(user)
if(!requiresID() || allowed(user))
+ if(HAS_TRAIT(src, TRAIT_CMAGGED))
+ cmag_switch(FALSE, user)
+ return
open_and_close()
else
+ if(HAS_TRAIT(src, TRAIT_CMAGGED))
+ cmag_switch(TRUE, user)
+ return
do_animate("deny")
/obj/machinery/door/window/unrestricted_side(mob/M)
@@ -247,6 +261,17 @@
open(2)
return 1
+/obj/machinery/door/window/cmag_act(mob/user, obj/weapon)
+ if(operating || !density || HAS_TRAIT(src, TRAIT_CMAGGED))
+ return
+ ADD_TRAIT(src, TRAIT_CMAGGED, "clown_emag")
+ operating = TRUE
+ flick("[base_state]spark", src)
+ playsound(src, "sparks", 75, TRUE, SHORT_RANGE_SOUND_EXTRARANGE)
+ sleep(6)
+ operating = FALSE
+ return TRUE
+
/obj/machinery/door/window/attackby(obj/item/I, mob/living/user, params)
//If it's in the process of opening/closing, ignore the click
if(operating)
diff --git a/code/game/objects/items/control_wand.dm b/code/game/objects/items/control_wand.dm
index cb347fe9105..a0235f9f4a7 100644
--- a/code/game/objects/items/control_wand.dm
+++ b/code/game/objects/items/control_wand.dm
@@ -48,6 +48,9 @@
/obj/item/door_remote/afterattack(obj/machinery/door/airlock/D, mob/user)
if(!istype(D))
return
+ if(HAS_TRAIT(D, TRAIT_CMAGGED))
+ to_chat(user, "The door doesn't respond to [src]!")
+ return
if(D.is_special)
to_chat(user, "[src] cannot access this kind of door!")
return
diff --git a/code/game/objects/items/weapons/cards_ids.dm b/code/game/objects/items/weapons/cards_ids.dm
index 62e54e713e8..082ad69ca61 100644
--- a/code/game/objects/items/weapons/cards_ids.dm
+++ b/code/game/objects/items/weapons/cards_ids.dm
@@ -80,6 +80,27 @@
return
A.emag_act(user)
+/obj/item/card/cmag
+ desc = "It's a card coated in a slurry of electromagnetic bananium."
+ name = "jestographic sequencer"
+ icon_state = "cmag"
+ item_state = "card-id"
+ origin_tech = "magnets=2;syndicate=2"
+ flags = NOBLUDGEON
+ flags_2 = NO_MAT_REDEMPTION_2
+
+/obj/item/card/cmag/Initialize(mapload)
+ . = ..()
+ AddComponent(/datum/component/slippery, src, 16 SECONDS, 100)
+
+/obj/item/card/cmag/attack()
+ return
+
+/obj/item/card/cmag/afterattack(atom/target, mob/user, proximity)
+ if(!proximity)
+ return
+ target.cmag_act(user)
+
/obj/item/card/id
name = "identification card"
desc = "A card used to provide ID and determine access across the station."
diff --git a/icons/obj/card.dmi b/icons/obj/card.dmi
index f51593e0216..41882bb12c0 100644
Binary files a/icons/obj/card.dmi and b/icons/obj/card.dmi differ