diff --git a/code/__DEFINES/dcs/signals/signals_traitor.dm b/code/__DEFINES/dcs/signals/signals_traitor.dm index 66aebd7ae95..1728ef8c679 100644 --- a/code/__DEFINES/dcs/signals/signals_traitor.dm +++ b/code/__DEFINES/dcs/signals/signals_traitor.dm @@ -3,6 +3,8 @@ /// Called whenever the uplink handler receives any sort of update. Used by uplinks to update their UI. No arguments passed #define COMSIG_UPLINK_HANDLER_ON_UPDATE "uplink_handler_on_update" +/// Sent from the uplink handler when the traitor uses the syndicate uplink beacon to order a replacement uplink. +#define COMSIG_UPLINK_HANDLER_REPLACEMENT_ORDERED "uplink_handler_replacement_ordered" /// Called before the traitor objective is generated #define COMSIG_TRAITOR_OBJECTIVE_PRE_GENERATE "traitor_objective_pre_generate" diff --git a/code/__DEFINES/radio.dm b/code/__DEFINES/radio.dm index 9644a1c09f2..e75ff9ea9eb 100644 --- a/code/__DEFINES/radio.dm +++ b/code/__DEFINES/radio.dm @@ -90,6 +90,8 @@ #define FREQ_SIGNALER 1457 // the default for new signalers #define FREQ_COMMON 1459 // Common comms frequency, dark green +#define MIN_UNUSED_FREQ 1461 // Prevents rolling AI Private or Common + #define MAX_FREQ 1489 // ------------------------------------------------------ #define MAX_FREE_FREQ 1599 // ------------------------------------------------- diff --git a/code/datums/components/crafting/crafting.dm b/code/datums/components/crafting/crafting.dm index 51f74fd389e..1b50b4e7b63 100644 --- a/code/datums/components/crafting/crafting.dm +++ b/code/datums/components/crafting/crafting.dm @@ -605,10 +605,15 @@ //Mind helpers -/datum/mind/proc/teach_crafting_recipe(R) +/// proc that teaches user a non-standard crafting recipe +/datum/mind/proc/teach_crafting_recipe(recipe) if(!learned_recipes) learned_recipes = list() - learned_recipes |= R + learned_recipes |= recipe + +/// proc that makes user forget a specific crafting recipe +/datum/mind/proc/forget_crafting_recipe(recipe) + learned_recipes -= recipe /datum/mind/proc/has_crafting_recipe(mob/user, potential_recipe) if(!learned_recipes) diff --git a/code/datums/components/crafting/structures.dm b/code/datums/components/crafting/structures.dm index 4a56852ecd5..2cbe2c35302 100644 --- a/code/datums/components/crafting/structures.dm +++ b/code/datums/components/crafting/structures.dm @@ -60,3 +60,17 @@ /obj/item/stack/cable_coil = 10, ) category = CAT_STRUCTURE + +/datum/crafting_recipe/syndicate_uplink_beacon + name = "Syndicate Uplink Beacon" + result = /obj/structure/syndicate_uplink_beacon + tool_behaviors = list(TOOL_SCREWDRIVER) + always_available = FALSE + time = 6 SECONDS + reqs = list( + /obj/item/stack/sheet/iron = 5, + /obj/item/stack/cable_coil = 5, + /obj/item/beacon = 1, + /obj/item/stack/ore/bluespace_crystal = 1, + ) + category = CAT_STRUCTURE diff --git a/code/datums/components/uplink.dm b/code/datums/components/uplink.dm index 50907528ce4..eea548a7551 100644 --- a/code/datums/components/uplink.dm +++ b/code/datums/components/uplink.dm @@ -65,6 +65,8 @@ RegisterSignal(parent, COMSIG_RADIO_NEW_MESSAGE, PROC_REF(new_message)) else if(istype(parent, /obj/item/pen)) RegisterSignal(parent, COMSIG_PEN_ROTATED, PROC_REF(pen_rotation)) + else if(istype(parent, /obj/item/uplink/replacement)) + RegisterSignal(parent, COMSIG_MOVABLE_HEAR, PROC_REF(on_heard)) if(owner) src.owner = owner @@ -86,6 +88,7 @@ else uplink_handler = uplink_handler_override RegisterSignal(uplink_handler, COMSIG_UPLINK_HANDLER_ON_UPDATE, PROC_REF(handle_uplink_handler_update)) + RegisterSignal(uplink_handler, COMSIG_UPLINK_HANDLER_REPLACEMENT_ORDERED, PROC_REF(handle_uplink_replaced)) if(!lockable) active = TRUE locked = FALSE @@ -96,6 +99,19 @@ SIGNAL_HANDLER SStgui.update_uis(src) +/// When a new uplink is made via the syndicate beacon it locks all lockable uplinks and destroys replacement uplinks +/datum/component/uplink/proc/handle_uplink_replaced() + SIGNAL_HANDLER + if(lockable) + lock_uplink() + if(!istype(parent, /obj/item/uplink/replacement)) + return + var/obj/item/uplink_item = parent + do_sparks(number = 3, cardinal_only = FALSE, source = uplink_item) + uplink_item.visible_message(span_warning("The [uplink_item] suddenly combusts!"), vision_distance = COMBAT_MESSAGE_RANGE) + new /obj/effect/decal/cleanable/ash(get_turf(uplink_item)) + qdel(uplink_item) + /// Adds telecrystals to the uplink. It is bad practice to use this outside of the component itself. /datum/component/uplink/proc/add_telecrystals(telecrystals_added) set_telecrystals(uplink_handler.telecrystals + telecrystals_added) @@ -236,7 +252,7 @@ data["extra_purchasable_stock"] = extra_purchasable_stock data["current_stock"] = remaining_stock data["shop_locked"] = uplink_handler.shop_locked - data["purchased_items"] = length(uplink_handler.purchase_log.purchase_log) + data["purchased_items"] = length(uplink_handler.purchase_log?.purchase_log) return data /datum/component/uplink/ui_static_data(mob/user) @@ -277,9 +293,7 @@ if("lock") if(!lockable) return TRUE - active = FALSE - locked = TRUE - SStgui.close_uis(src) + lock_uplink() if(!uplink_handler.has_objectives) return TRUE @@ -321,6 +335,12 @@ uplink_handler.abort_objective(objective) return TRUE +/// Proc that locks uplinks +/datum/component/uplink/proc/lock_uplink() + active = FALSE + locked = TRUE + SStgui.close_uis(src) + // Implant signal responses /datum/component/uplink/proc/implant_activation() SIGNAL_HANDLER @@ -459,6 +479,17 @@ return returnable_code +/// Proc that unlocks a locked replacement uplink when it hears the unlock code from their datum +/datum/component/uplink/proc/on_heard(datum/source, list/hearing_args) + SIGNAL_HANDLER + if(!locked) + return + if(!findtext(hearing_args[HEARING_RAW_MESSAGE], unlock_code)) + return + var/atom/replacement_uplink = parent + locked = FALSE + replacement_uplink.balloon_alert_to_viewers("beep", vision_distance = COMBAT_MESSAGE_RANGE) + /datum/component/uplink/proc/failsafe(mob/living/carbon/user) if(!parent) return diff --git a/code/datums/looping_sounds/machinery_sounds.dm b/code/datums/looping_sounds/machinery_sounds.dm index 9287f927333..1e8ce73e992 100644 --- a/code/datums/looping_sounds/machinery_sounds.dm +++ b/code/datums/looping_sounds/machinery_sounds.dm @@ -132,3 +132,16 @@ mid_sounds = list('sound/effects/bubbles2.ogg' = 1) mid_length = 7 SECONDS volume = 25 + +/datum/looping_sound/typing + mid_sounds = list( + 'sound/machines/terminal_button01.ogg' = 1, + 'sound/machines/terminal_button02.ogg' = 1, + 'sound/machines/terminal_button03.ogg' = 1, + 'sound/machines/terminal_button04.ogg' = 1, + 'sound/machines/terminal_button05.ogg' = 1, + 'sound/machines/terminal_button06.ogg' = 1, + 'sound/machines/terminal_button07.ogg' = 1, + 'sound/machines/terminal_button08.ogg' = 1, + ) + mid_length = 0.3 SECONDS diff --git a/code/game/objects/structures/syndicate_uplink_beacon.dm b/code/game/objects/structures/syndicate_uplink_beacon.dm new file mode 100644 index 00000000000..f4d4ea6fe64 --- /dev/null +++ b/code/game/objects/structures/syndicate_uplink_beacon.dm @@ -0,0 +1,114 @@ +/// Device that traitors can craft in order to be sent a new, undisguised uplink +/obj/structure/syndicate_uplink_beacon + name = "suspicious beacon" + icon = 'icons/obj/machines/telecomms.dmi' + icon_state = "relay_traitor" + desc = "This ramshackle device seems capable of recieving and sending signals for some nefarious purpose." + density = TRUE + anchored = TRUE + /// Traitor's code that they speak into the radio + var/uplink_code = "" + /// weakref to person who is going to use the beacon to get a replacement uplink + var/datum/weakref/owner + /// while constructed the teleport beacon is still active + var/obj/item/beacon/teleport_beacon + /// Radio that the device needs to listen to the codeword from the traitor + var/obj/item/radio/listening_radio + /// prevents traitor from activating teleport_beacon proc too much in a small period of time + COOLDOWN_DECLARE(beacon_cooldown) + +/obj/structure/syndicate_uplink_beacon/Initialize(mapload) + . = ..() + register_context() + + var/static/list/tool_behaviors = list( + TOOL_SCREWDRIVER = list( + SCREENTIP_CONTEXT_RMB = "Deconstruct", + ), + ) + AddElement(/datum/element/contextual_screentip_tools, tool_behaviors) + listening_radio = new(src) + listening_radio.canhear_range = 0 + teleport_beacon = new(src) + +/obj/structure/syndicate_uplink_beacon/attack_hand(mob/living/user, list/modifiers) + if(!IS_TRAITOR(user)) + balloon_alert(user, "don't know how to use!") + return + if(IS_WEAKREF_OF(owner, user)) + balloon_alert(user, "already synchronized to you!") + return + if(owner != null) + balloon_alert(user, "already claimed!") + return + var/datum/looping_sound/typing/typing_sounds = new(src, start_immediately = TRUE) + balloon_alert(user, "synchronizing...") + if(!do_after(user = user, delay = 3 SECONDS, target = src, interaction_key = REF(src))) + typing_sounds.stop() + return + typing_sounds.stop() + probe_traitor(user) + +/obj/structure/syndicate_uplink_beacon/screwdriver_act_secondary(mob/living/user, obj/item/tool) + tool.play_tool_sound(src) + balloon_alert(user, "deconstructing...") + if (!do_after(user, 5 SECONDS, target = src)) + return FALSE + var/turf/beacon_tile = get_turf(src) + new /obj/item/stack/sheet/iron/five(beacon_tile) + new /obj/item/stack/cable_coil/five(beacon_tile) + teleport_beacon.forceMove(beacon_tile) + teleport_beacon = null + qdel(src) + return TRUE + +/obj/structure/syndicate_uplink_beacon/Destroy() + QDEL_NULL(listening_radio) + if(teleport_beacon) + QDEL_NULL(teleport_beacon) + return ..() + +/// Proc reads the user, sets radio to the correct frequency and starts to listen for the replacement uplink code +/obj/structure/syndicate_uplink_beacon/proc/probe_traitor(mob/living/user) + owner = WEAKREF(user) + var/datum/antagonist/traitor/traitor_datum = user.mind.has_antag_datum(/datum/antagonist/traitor) + + uplink_code = traitor_datum.replacement_uplink_code + listening_radio.set_frequency(traitor_datum.replacement_uplink_frequency) + become_hearing_sensitive() + +/obj/structure/syndicate_uplink_beacon/Hear(message, atom/movable/speaker, message_language, raw_message, radio_freq, list/spans, list/message_mods, message_range) + if(ismob(speaker) || radio_freq != listening_radio.get_frequency()) + return + if(!findtext(message, uplink_code)) + return + teleport_uplink() + +/// Proc uses owners uplink handler to create replacement uplink and then lock or destroy their other uplinks +/obj/structure/syndicate_uplink_beacon/proc/teleport_uplink() + if(!COOLDOWN_FINISHED(src, beacon_cooldown)) + return + COOLDOWN_START(src, beacon_cooldown, 10 MINUTES) + + var/mob/living/resolved_owner = owner.resolve() + if(isnull(resolved_owner)) + return + + var/datum/antagonist/traitor/traitor_datum = resolved_owner.mind.has_antag_datum(/datum/antagonist/traitor) + if(isnull(traitor_datum)) + return + + var/datum/uplink_handler/uplink_handler = traitor_datum.uplink_handler + + SEND_SIGNAL(uplink_handler, COMSIG_UPLINK_HANDLER_REPLACEMENT_ORDERED) + new /obj/item/uplink/replacement(get_turf(src), /*owner = */resolved_owner, /*tc_amount = */0, /*uplink_handler_override = */uplink_handler) + flick("relay_traitor_activate", src) + do_sparks(number = 5, cardinal_only = FALSE, source = src) + log_traitor("[key_name(resolved_owner)] acquired a replacement uplink via the syndicate uplink beacon.") + +// Adds screentips +/obj/structure/syndicate_uplink_beacon/add_context(atom/source, list/context, obj/item/held_item, mob/user) + if(held_item || !IS_TRAITOR(user)) + return NONE + context[SCREENTIP_CONTEXT_LMB] = "Synchronize with beacon" + return CONTEXTUAL_SCREENTIP_SET diff --git a/code/modules/antagonists/traitor/datum_traitor.dm b/code/modules/antagonists/traitor/datum_traitor.dm index 2d57748f754..486e6e37586 100644 --- a/code/modules/antagonists/traitor/datum_traitor.dm +++ b/code/modules/antagonists/traitor/datum_traitor.dm @@ -18,6 +18,10 @@ var/should_give_codewords = TRUE ///give this traitor an uplink? var/give_uplink = TRUE + /// Code that allows traitor to get a replacement uplink + var/replacement_uplink_code = "" + /// Radio frequency that traitor must speak on to get a replacement uplink + var/replacement_uplink_frequency = "" ///if TRUE, this traitor will always get hijacking as their final objective var/is_hijacker = FALSE @@ -47,6 +51,7 @@ if(give_uplink) owner.give_uplink(silent = TRUE, antag_datum = src) + generate_replacement_codes() var/datum/component/uplink/uplink = owner.find_syndicate_uplink() uplink_ref = WEAKREF(uplink) @@ -82,6 +87,8 @@ pick_employer() + owner.teach_crafting_recipe(/datum/crafting_recipe/syndicate_uplink_beacon) + owner.current.playsound_local(get_turf(owner.current), 'sound/ambience/antag/tatoralert.ogg', 100, FALSE, pressure_affected = FALSE, use_reverb = FALSE) return ..() @@ -139,8 +146,14 @@ result += "Force add objective
" return result +/// proc that generates the traitors replacement uplink code and radio frequency +/datum/antagonist/traitor/proc/generate_replacement_codes() + replacement_uplink_code = "[pick(GLOB.phonetic_alphabet)] [rand(10,99)]" + replacement_uplink_frequency = sanitize_frequency(rand(MIN_UNUSED_FREQ, MAX_FREQ), free = FALSE, syndie = FALSE) + /datum/antagonist/traitor/on_removal() owner.special_role = null + owner.forget_crafting_recipe(/datum/crafting_recipe/syndicate_uplink_beacon) return ..() /datum/antagonist/traitor/proc/pick_employer() @@ -258,6 +271,8 @@ data["theme"] = traitor_flavor["ui_theme"] data["code"] = uplink?.unlock_code data["failsafe_code"] = uplink?.failsafe_code + data["replacement_code"] = replacement_uplink_code + data["replacement_frequency"] = format_frequency(replacement_uplink_frequency) data["intro"] = traitor_flavor["introduction"] data["allies"] = traitor_flavor["allies"] data["goal"] = traitor_flavor["goal"] diff --git a/code/modules/uplink/uplink_devices.dm b/code/modules/uplink/uplink_devices.dm index c9c2fa3174d..c2409773ada 100644 --- a/code/modules/uplink/uplink_devices.dm +++ b/code/modules/uplink/uplink_devices.dm @@ -23,15 +23,25 @@ /// The uplink flag for this type. /// See [`code/__DEFINES/uplink.dm`] var/uplink_flag = UPLINK_TRAITORS + /// If the uplink is lockable, which defaults to false which most subtypes of this item are for debug reasons + var/lockable_uplink = FALSE -/obj/item/uplink/Initialize(mapload, owner, tc_amount = 20) +/obj/item/uplink/Initialize(mapload, owner, tc_amount = 20, datum/uplink_handler/uplink_handler_override = null) . = ..() - AddComponent(/datum/component/uplink, owner, FALSE, TRUE, uplink_flag, tc_amount) + AddComponent(\ + /datum/component/uplink, \ + owner = owner, \ + lockable = lockable_uplink, \ + enabled = TRUE, \ + uplink_flag = uplink_flag, \ + starting_tc = tc_amount, \ + uplink_handler_override = uplink_handler_override, \ + ) /obj/item/uplink/debug name = "debug uplink" -/obj/item/uplink/debug/Initialize(mapload, owner, tc_amount = 9000) +/obj/item/uplink/debug/Initialize(mapload, owner, tc_amount = 9000, datum/uplink_handler/uplink_handler_override = null) . = ..() var/datum/component/uplink/hidden_uplink = GetComponent(/datum/component/uplink) hidden_uplink.name = "debug uplink" @@ -44,7 +54,7 @@ name = "debug nuclear uplink" uplink_flag = UPLINK_NUKE_OPS -/obj/item/uplink/nuclear/debug/Initialize(mapload, owner, tc_amount = 9000) +/obj/item/uplink/nuclear/debug/Initialize(mapload, owner, tc_amount = 9000, datum/uplink_handler/uplink_handler_override = null) . = ..() var/datum/component/uplink/hidden_uplink = GetComponent(/datum/component/uplink) hidden_uplink.name = "debug nuclear uplink" @@ -65,17 +75,45 @@ name = "dusty radio" desc = "A dusty looking radio." -/obj/item/uplink/old/Initialize(mapload, owner, tc_amount = 10) +/obj/item/uplink/old/Initialize(mapload, owner, tc_amount = 10, datum/uplink_handler/uplink_handler_override = null) . = ..() var/datum/component/uplink/hidden_uplink = GetComponent(/datum/component/uplink) hidden_uplink.name = "dusty radio" +// Uplink subtype used as replacement uplink +/obj/item/uplink/replacement + lockable_uplink = TRUE + +/obj/item/uplink/replacement/Initialize(mapload, owner, tc_amount = 10, datum/uplink_handler/uplink_handler_override = null) + . = ..() + var/datum/component/uplink/hidden_uplink = GetComponent(/datum/component/uplink) + var/mob/living/replacement_needer = owner + if(!istype(replacement_needer)) + return + var/datum/antagonist/traitor/traitor_datum = replacement_needer?.mind.has_antag_datum(/datum/antagonist/traitor) + hidden_uplink.unlock_code = traitor_datum?.replacement_uplink_code + become_hearing_sensitive() + +/obj/item/uplink/replacement/screwdriver_act_secondary(mob/living/user, obj/item/tool) + tool.play_tool_sound(src) + balloon_alert(user, "deconstructing...") + if (!do_after(user, 3 SECONDS, target = src)) + return FALSE + qdel(src) + return TRUE + +/obj/item/uplink/replacement/examine(mob/user) + . = ..() + if(!IS_TRAITOR(user)) + return + . += span_notice("You can destroy this device with a screwdriver.") + // Multitool uplink -/obj/item/multitool/uplink/Initialize(mapload, owner, tc_amount = 20) +/obj/item/multitool/uplink/Initialize(mapload, owner, tc_amount = 20, datum/uplink_handler/uplink_handler_override = null) . = ..() AddComponent(/datum/component/uplink, owner, FALSE, TRUE, UPLINK_TRAITORS, tc_amount) // Pen uplink -/obj/item/pen/uplink/Initialize(mapload, owner, tc_amount = 20) +/obj/item/pen/uplink/Initialize(mapload, owner, tc_amount = 20, datum/uplink_handler/uplink_handler_override = null) . = ..() AddComponent(/datum/component/uplink, owner, TRUE, FALSE, UPLINK_TRAITORS, tc_amount) diff --git a/icons/obj/machines/telecomms.dmi b/icons/obj/machines/telecomms.dmi index 93bb9c477aa..ddce8a96a93 100644 Binary files a/icons/obj/machines/telecomms.dmi and b/icons/obj/machines/telecomms.dmi differ diff --git a/tgstation.dme b/tgstation.dme index 9caadb0df95..57e8cdf1b5c 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -2135,6 +2135,7 @@ #include "code\game\objects\structures\spawner.dm" #include "code\game\objects\structures\spirit_board.dm" #include "code\game\objects\structures\stairs.dm" +#include "code\game\objects\structures\syndicate_uplink_beacon.dm" #include "code\game\objects\structures\table_frames.dm" #include "code\game\objects\structures\tables_racks.dm" #include "code\game\objects\structures\tank_dispenser.dm" diff --git a/tgui/packages/tgui/interfaces/AntagInfoTraitor.tsx b/tgui/packages/tgui/interfaces/AntagInfoTraitor.tsx index cd79be0621d..07d1e1beff3 100644 --- a/tgui/packages/tgui/interfaces/AntagInfoTraitor.tsx +++ b/tgui/packages/tgui/interfaces/AntagInfoTraitor.tsx @@ -35,6 +35,8 @@ type Info = { intro: string; code: string; failsafe_code: string; + replacement_code: string; + replacement_frequency: string; has_uplink: BooleanLike; uplink_intro: string; uplink_unlock_info: string; @@ -120,8 +122,15 @@ const EmployerSection = (props, context) => { const UplinkSection = (props, context) => { const { data } = useBackend(context); - const { has_uplink, uplink_intro, uplink_unlock_info, code, failsafe_code } = - data; + const { + has_uplink, + uplink_intro, + uplink_unlock_info, + code, + failsafe_code, + replacement_code, + replacement_frequency, + } = data; return (
@@ -149,6 +158,19 @@ const UplinkSection = (props, context) => { )} +
+
+ If you lose your uplink, you can craft a Syndicate Uplink Beacon and + then speak{' '} + + {replacement_code} + {' '} + on radio frequency{' '} + + {replacement_frequency} + {' '} + after synchronizing with the beacon. +
); };