Files
Bubberstation/code/datums/components/uplink.dm
T
Sol NandGitHub 11cbbba018 Replaceable Traitor Uplinks (#74315)
## About The Pull Request

Following [from the suggestion in this hackmd
](https://hackmd.io/IkDWWkpfQa2lkdevsnLqhA?view#Replacement-Uplinks)with
a few twists of my own, I have made a method for traitors to acquire a
replacement traitor uplink that has its own set of flaws and limiters in
order to prevent abuse.


![ZC0WYDFRzc](https://user-images.githubusercontent.com/116288367/228101432-9352390b-9538-4c62-8dc4-55e2e798c466.png)

The basic pitch is as follows, all traitors now start with a new,
crafting recipe exclusive to them, it costs a teleport beacon, a
bluespace crystal, and some iron and cable coil, and then allows them to
build a static, dense machine that they can synchronize with, which
allows the machine to know what signal it should be looking out for from
the traitor.

![dreamseeker_iErI3vju0C](https://user-images.githubusercontent.com/116288367/228094286-c2bca198-82cd-4ce0-a4a7-c26c24a9327c.gif)

The traitor then uses any radio, sets it to the frequency that has been
added to their static antagonist ui, and then speaks their codeword,
also in the ui, and then a few things happen.

![dreamseeker_gbzSFeHuS5](https://user-images.githubusercontent.com/116288367/228094354-a649c713-f013-4ac2-b8d7-0754a852f987.gif)

Most obviously, they get a replacement uplink that is in the conspicuous
shape of the nukie or lone op uplink. This uplink can be unlocked by
speaking your replacement codeword to it again, it remembers your
previous TC amount and locks all other uplinks associated with your
uplink handler(they can then be unlocked as normal). It also destroys
any other replacement uplinks associated with your uplink handler, which
means you can never have more than one replacement uplink.

This means that if your uplink has been confiscated and you left it
unlocked, if it hasn't been emptied out you can continue from where you
were, and if you want to get back on the TC grind you won't lose the new
TC to whoever stole your uplink. Of course, the new uplink can not be
locked, so you have to be more careful with it or buy an uplink implant
and destroy it. You can destroy your replacement uplink with a
screwdriver right click, same for the machine.

Additionally, the Syndicate Uplink Beacon has another quirk to it, which
is that the teleporter beacon used to create it is intact, which means
people eagle eyed on the teleporter console could go find you, not to
mention that if you use an existing teleporter beacon, someone might
notice its gone missing...

oh also while making the replacement uplink i found a bug caused by a
recent pr that broke debug uplinks due to them not having a purchase
log. thats fixed too

## Why It's Good For The Game

It can be easy to lose your uplink, and as a traitor having your uplink
confiscated, even if it is locked, feels really bad. While the old
traitor objectives were added back to prog traitor to prevent situations
where a confiscated uplink meant that you were completely aimless, I
think that having a backup solution would be good for more inexperienced
traitors or for ones who get unlucky.

Hopefully this is generally balanced well enough but there are a few
levers that can be pulled, but overall I do think that making it so that
traitors can always get a chance to get an uplink and do some objectives
is good for the game. I like the idea of someone getting perma'd,
someone breaks them out, they both craft a new uplink beacon, and then
they go back and get the traitors old gear with stuff they got from the
new uplink, I think that's a cool possibility to throw into the sandbox.

## Changelog
🆑
add: Added new syndicate uplink beacon and associated systems that allow
you to get a replacement traitor uplink
fix: Debug & nukie uplinks no longer runtime and work properly again
/🆑
2023-04-05 16:54:22 +01:00

506 lines
18 KiB
Plaintext

#define PEN_ROTATIONS 2
/**
* Uplinks
*
* All /obj/item(s) have a hidden_uplink var. By default it's null. Give the item one with 'new(src') (it must be in it's contents). Then add 'uses.'
* Use whatever conditionals you want to check that the user has an uplink, and then call interact() on their uplink.
* You might also want the uplink menu to open if active. Check if the uplink is 'active' and then interact() with it.
**/
/datum/component/uplink
dupe_mode = COMPONENT_DUPE_UNIQUE
/// Name of the uplink
var/name = "syndicate uplink"
/// Whether the uplink is currently active or not
var/active = FALSE
/// Whether this uplink can be locked or not
var/lockable = TRUE
/// Whether the uplink is locked or not.
var/locked = TRUE
/// Whether this uplink allows restricted items to be accessed
var/allow_restricted = TRUE
/// Current owner of the uplink
var/owner = null
/// Purchase log, listing all the purchases this uplink has made
var/datum/uplink_purchase_log/purchase_log
/// The current linked uplink handler.
var/datum/uplink_handler/uplink_handler
/// Code to unlock the uplink.
var/unlock_code
/// Used for pen uplink
var/list/previous_attempts
// Not modular variables. These variables should be removed sometime in the future
/// The unlock text that is sent to the traitor with this uplink. This is not modular and not recommended to expand upon
var/unlock_text
/// The unlock note that is sent to the traitor with this uplink. This is not modular and not recommended to expand upon
var/unlock_note
/// The failsafe code that causes this uplink to blow up.
var/failsafe_code
/datum/component/uplink/Initialize(
owner,
lockable = TRUE,
enabled = FALSE,
uplink_flag = UPLINK_TRAITORS,
starting_tc = TELECRYSTALS_DEFAULT,
has_progression = FALSE,
datum/uplink_handler/uplink_handler_override,
)
if(!isitem(parent))
return COMPONENT_INCOMPATIBLE
RegisterSignal(parent, COMSIG_PARENT_ATTACKBY, PROC_REF(OnAttackBy))
RegisterSignal(parent, COMSIG_ITEM_ATTACK_SELF, PROC_REF(interact))
if(istype(parent, /obj/item/implant))
RegisterSignal(parent, COMSIG_IMPLANT_ACTIVATED, PROC_REF(implant_activation))
RegisterSignal(parent, COMSIG_IMPLANT_IMPLANTING, PROC_REF(implanting))
RegisterSignal(parent, COMSIG_IMPLANT_OTHER, PROC_REF(old_implant))
RegisterSignal(parent, COMSIG_IMPLANT_EXISTING_UPLINK, PROC_REF(new_implant))
else if(istype(parent, /obj/item/modular_computer/pda))
RegisterSignal(parent, COMSIG_TABLET_CHANGE_ID, PROC_REF(new_ringtone))
RegisterSignal(parent, COMSIG_TABLET_CHECK_DETONATE, PROC_REF(check_detonate))
else if(istype(parent, /obj/item/radio))
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
LAZYINITLIST(GLOB.uplink_purchase_logs_by_key)
if(GLOB.uplink_purchase_logs_by_key[owner])
purchase_log = GLOB.uplink_purchase_logs_by_key[owner]
else
purchase_log = new(owner, src)
RegisterSignal(parent, COMSIG_PARENT_EXAMINE, PROC_REF(on_examine))
src.lockable = lockable
src.active = enabled
if(!uplink_handler_override)
uplink_handler = new()
uplink_handler.has_objectives = FALSE
uplink_handler.uplink_flag = uplink_flag
uplink_handler.telecrystals = starting_tc
uplink_handler.has_progression = has_progression
uplink_handler.purchase_log = purchase_log
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
previous_attempts = list()
/datum/component/uplink/proc/handle_uplink_handler_update()
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)
/// Sets the telecrystals of the uplink. It is bad practice to use this outside of the component itself.
/datum/component/uplink/proc/set_telecrystals(new_telecrystal_amount)
uplink_handler.telecrystals = new_telecrystal_amount
/datum/component/uplink/InheritComponent(datum/component/uplink/uplink)
lockable |= uplink.lockable
active |= uplink.active
uplink_handler.uplink_flag |= uplink.uplink_handler.uplink_flag
/datum/component/uplink/Destroy()
purchase_log = null
return ..()
/datum/component/uplink/proc/load_tc(mob/user, obj/item/stack/telecrystal/telecrystals, silent = FALSE)
if(!silent)
to_chat(user, span_notice("You slot [telecrystals] into [parent] and charge its internal uplink."))
var/amt = telecrystals.amount
uplink_handler.telecrystals += amt
telecrystals.use(amt)
log_uplink("[key_name(user)] loaded [amt] telecrystals into [parent]'s uplink")
/datum/component/uplink/proc/OnAttackBy(datum/source, obj/item/item, mob/user)
SIGNAL_HANDLER
if(!active)
return //no hitting everyone/everything just to try to slot tcs in!
if(istype(item, /obj/item/stack/telecrystal))
load_tc(user, item)
/datum/component/uplink/proc/on_examine(datum/source, mob/user, list/examine_list)
SIGNAL_HANDLER
if(user != owner)
return
examine_list += span_warning("[parent] contains your hidden uplink\
[unlock_code ? ", the code to unlock it is [span_boldwarning(unlock_code)]" : null].")
if(failsafe_code)
examine_list += span_warning("The failsafe code is [span_boldwarning(failsafe_code)].")
/datum/component/uplink/proc/interact(datum/source, mob/user)
SIGNAL_HANDLER
if(locked)
return
active = TRUE
if(user)
INVOKE_ASYNC(src, PROC_REF(ui_interact), user)
// an unlocked uplink blocks also opening the PDA or headset menu
return COMPONENT_CANCEL_ATTACK_CHAIN
/datum/component/uplink/ui_state(mob/user)
return GLOB.inventory_state
/datum/component/uplink/ui_interact(mob/user, datum/tgui/ui)
active = TRUE
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "Uplink", name)
// This UI is only ever opened by one person,
// and never is updated outside of user input.
ui.set_autoupdate(FALSE)
ui.open()
/datum/component/uplink/ui_data(mob/user)
if(!user.mind)
return
var/list/data = list()
data["telecrystals"] = uplink_handler.telecrystals
data["progression_points"] = uplink_handler.progression_points
data["current_expected_progression"] = SStraitor.current_global_progression
data["maximum_active_objectives"] = uplink_handler.maximum_active_objectives
data["progression_scaling_deviance"] = SStraitor.progression_scaling_deviance
data["current_progression_scaling"] = SStraitor.current_progression_scaling
data["maximum_potential_objectives"] = uplink_handler.maximum_potential_objectives
if(uplink_handler.has_objectives)
var/list/primary_objectives = list()
for(var/datum/objective/task as anything in uplink_handler.primary_objectives)
var/list/task_data = list()
if(length(primary_objectives) > length(GLOB.phonetic_alphabet))
task_data["task_name"] = "DIRECTIVE [length(primary_objectives) + 1]" //The english alphabet is WEAK
else
task_data["task_name"] = "DIRECTIVE [uppertext(GLOB.phonetic_alphabet[length(primary_objectives) + 1])]"
task_data["task_text"] = task.explanation_text
primary_objectives += list(task_data)
var/list/potential_objectives = list()
for(var/index in 1 to uplink_handler.potential_objectives.len)
var/datum/traitor_objective/objective = uplink_handler.potential_objectives[index]
var/list/objective_data = objective.uplink_ui_data(user)
objective_data["id"] = index
potential_objectives += list(objective_data)
var/list/active_objectives = list()
for(var/index in 1 to uplink_handler.active_objectives.len)
var/datum/traitor_objective/objective = uplink_handler.active_objectives[index]
var/list/objective_data = objective.uplink_ui_data(user)
objective_data["id"] = index
active_objectives += list(objective_data)
data["primary_objectives"] = primary_objectives
data["potential_objectives"] = potential_objectives
data["active_objectives"] = active_objectives
data["completed_final_objective"] = uplink_handler.final_objective
var/list/stock_list = uplink_handler.item_stock.Copy()
var/list/extra_purchasable_stock = list()
var/list/extra_purchasable = list()
for(var/datum/uplink_item/item as anything in uplink_handler.extra_purchasable)
if(item in stock_list)
extra_purchasable_stock[REF(item)] = stock_list[item]
stock_list -= item
extra_purchasable += list(list(
"id" = item.type,
"name" = item.name,
"cost" = item.cost,
"desc" = item.desc,
"category" = item.category ? initial(item.category.name) : null,
"purchasable_from" = item.purchasable_from,
"restricted" = item.restricted,
"limited_stock" = item.limited_stock,
"restricted_roles" = item.restricted_roles,
"restricted_species" = item.restricted_species,
"progression_minimum" = item.progression_minimum,
"ref" = REF(item),
))
var/list/remaining_stock = list()
for(var/item as anything in stock_list)
remaining_stock[item] = stock_list[item]
data["extra_purchasable"] = extra_purchasable
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)
return data
/datum/component/uplink/ui_static_data(mob/user)
var/list/data = list()
data["uplink_flag"] = uplink_handler.uplink_flag
data["has_progression"] = uplink_handler.has_progression
data["has_objectives"] = uplink_handler.has_objectives
data["lockable"] = lockable
data["assigned_role"] = uplink_handler.assigned_role
data["assigned_species"] = uplink_handler.assigned_species
data["debug"] = uplink_handler.debug_mode
return data
/datum/component/uplink/ui_assets(mob/user)
return list(
get_asset_datum(/datum/asset/json/uplink)
)
/datum/component/uplink/ui_act(action, params, datum/tgui/ui, datum/ui_state/state)
. = ..()
if(.)
return
if(!active)
return
switch(action)
if("buy")
var/datum/uplink_item/item
if(params["ref"])
item = locate(params["ref"]) in uplink_handler.extra_purchasable
if(!item)
return
else
var/datum/uplink_item/item_path = text2path(params["path"])
if(!ispath(item_path, /datum/uplink_item))
return
item = SStraitor.uplink_items_by_type[item_path]
uplink_handler.purchase_item(ui.user, item, parent)
if("lock")
if(!lockable)
return TRUE
lock_uplink()
if(!uplink_handler.has_objectives)
return TRUE
if(uplink_handler.owner?.current != ui.user || !uplink_handler.can_take_objectives)
return TRUE
switch(action)
if("regenerate_objectives")
uplink_handler.generate_objectives()
return TRUE
var/list/objectives
switch(action)
if("start_objective")
objectives = uplink_handler.potential_objectives
if("objective_act", "finish_objective", "objective_abort")
objectives = uplink_handler.active_objectives
if(!objectives)
return
var/objective_index = round(text2num(params["index"]))
if(objective_index < 1 || objective_index > length(objectives))
return TRUE
var/datum/traitor_objective/objective = objectives[objective_index]
// Objective actions
switch(action)
if("start_objective")
uplink_handler.take_objective(ui.user, objective)
if("objective_act")
uplink_handler.ui_objective_act(ui.user, objective, params["objective_action"])
if("finish_objective")
if(!objective.finish_objective(ui.user))
return
uplink_handler.complete_objective(objective)
if("objective_abort")
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
var/obj/item/implant/implant = parent
locked = FALSE
interact(null, implant.imp_in)
/datum/component/uplink/proc/implanting(datum/source, list/arguments)
SIGNAL_HANDLER
var/mob/user = arguments[2]
owner = user?.key
if(owner && !purchase_log)
LAZYINITLIST(GLOB.uplink_purchase_logs_by_key)
if(GLOB.uplink_purchase_logs_by_key[owner])
purchase_log = GLOB.uplink_purchase_logs_by_key[owner]
else
purchase_log = new(owner, src)
/datum/component/uplink/proc/old_implant(datum/source, list/arguments, obj/item/implant/new_implant)
SIGNAL_HANDLER
// It kinda has to be weird like this until implants are components
return SEND_SIGNAL(new_implant, COMSIG_IMPLANT_EXISTING_UPLINK, src)
/datum/component/uplink/proc/new_implant(datum/source, datum/component/uplink/uplink)
SIGNAL_HANDLER
return COMPONENT_DELETE_NEW_IMPLANT
// PDA signal responses
/datum/component/uplink/proc/new_ringtone(datum/source, mob/living/user, new_ring_text)
SIGNAL_HANDLER
if(trim(lowertext(new_ring_text)) != trim(lowertext(unlock_code)))
if(trim(lowertext(new_ring_text)) == trim(lowertext(failsafe_code)))
failsafe(user)
return COMPONENT_STOP_RINGTONE_CHANGE
return
locked = FALSE
interact(null, user)
to_chat(user, span_hear("The computer softly beeps."))
return COMPONENT_STOP_RINGTONE_CHANGE
/datum/component/uplink/proc/check_detonate()
SIGNAL_HANDLER
return COMPONENT_TABLET_NO_DETONATE
// Radio signal responses
/datum/component/uplink/proc/new_frequency(datum/source, list/arguments)
SIGNAL_HANDLER
var/obj/item/radio/master = parent
var/frequency = arguments[1]
if(frequency != unlock_code)
if(frequency == failsafe_code)
failsafe(master.loc)
return
locked = FALSE
if(ismob(master.loc))
interact(null, master.loc)
/datum/component/uplink/proc/new_message(datum/source, mob/living/user, message, channel)
SIGNAL_HANDLER
if(channel != RADIO_CHANNEL_UPLINK)
return
if(!findtext(lowertext(message), lowertext(unlock_code)))
if(failsafe_code && findtext(lowertext(message), lowertext(failsafe_code)))
failsafe(user) // no point returning cannot radio, youre probably ded
return
locked = FALSE
interact(null, user)
to_chat(user, "As you whisper the code into your headset, a soft chime fills your ears.")
return COMPONENT_CANNOT_USE_RADIO
// Pen signal responses
/datum/component/uplink/proc/pen_rotation(datum/source, degrees, mob/living/carbon/user)
SIGNAL_HANDLER
var/obj/item/pen/master = parent
previous_attempts += degrees
if(length(previous_attempts) > PEN_ROTATIONS)
popleft(previous_attempts)
if(compare_list(previous_attempts, unlock_code))
locked = FALSE
previous_attempts.Cut()
master.degrees = 0
interact(null, user)
to_chat(user, span_warning("Your pen makes a clicking noise, before quickly rotating back to 0 degrees!"))
else if(compare_list(previous_attempts, failsafe_code))
failsafe(user)
/datum/component/uplink/proc/setup_unlock_code()
unlock_code = generate_code()
var/obj/item/P = parent
if(istype(parent,/obj/item/modular_computer/pda))
unlock_note = "<B>Uplink Passcode:</B> [unlock_code] ([P.name])."
else if(istype(parent,/obj/item/radio))
unlock_note = "<B>Radio Passcode:</B> [unlock_code] ([P.name], [RADIO_TOKEN_UPLINK] channel)."
else if(istype(parent,/obj/item/pen))
unlock_note = "<B>Uplink Degrees:</B> [english_list(unlock_code)] ([P.name])."
/datum/component/uplink/proc/generate_code()
var/returnable_code = ""
if(istype(parent, /obj/item/modular_computer/pda))
returnable_code = "[rand(100,999)] [pick(GLOB.phonetic_alphabet)]"
else if(istype(parent, /obj/item/radio))
returnable_code = pick(GLOB.phonetic_alphabet)
else if(istype(parent, /obj/item/pen))
returnable_code = list()
for(var/i in 1 to PEN_ROTATIONS)
returnable_code += rand(1, 360)
if(!unlock_code) // assume the unlock_code is our "base" code that we don't want to duplicate, and if we don't have an unlock code, immediately return out of it since there's nothing to compare to.
return returnable_code
// duplicate checking, re-run the proc if we get a dupe to prevent the failsafe explodey code being the same as the unlock code.
if(islist(returnable_code))
if(english_list(returnable_code) == english_list(unlock_code)) // we pass english_list to the user anyways and for later processing, so we can just compare the english_list of the two lists.
return generate_code()
else if(unlock_code == returnable_code)
return generate_code()
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
var/turf/T = get_turf(parent)
if(!T)
return
message_admins("[ADMIN_LOOKUPFLW(user)] has triggered an uplink failsafe explosion at [AREACOORD(T)] The owner of the uplink was [ADMIN_LOOKUPFLW(owner)].")
user.log_message("triggered an uplink failsafe explosion. Uplink owner: [key_name(owner)].", LOG_ATTACK)
explosion(parent, devastation_range = 1, heavy_impact_range = 2, light_impact_range = 3)
qdel(parent) //Alternatively could brick the uplink.
#undef PEN_ROTATIONS