Files
Bubberstation/code/datums/components/uplink.dm
Jacquerel 8c6062e948 Traitor Reputation does not scale with population & reintroduces population locked items (#89617)
## About The Pull Request

Closes #89617

Prior to progression traitor some items were only available with a
minimum number of (normally 25) players in the round.
These items were:
- The dual esword
- Noslip shoes
- The ebow
- Holoparasite
- Sleeping Carp
- Contractor Kit
- Maybe a couple of others that I forgot to write down

When we moved to a progression system this concept was merged with
reputation; under 20 players your reputation would advance more slowly
thus making these "dangerous" items less obtainable and also serving as
a sort of scaling factor on rewards (with fewer players the secondary
objectives are easier to complete, so the reward is commesurately
lower).

Now that we have removed secondary objectives this doesn't really make
sense any more, so now reputation simply advances at a rate of one
second per second all the time, but that leaves the old population locks
in question.

So... I just recoded items that are only available when there are enough
players

![image](https://github.com/user-attachments/assets/206577f0-dfdb-4b53-a00f-36e39b2a7f44)

![image](https://github.com/user-attachments/assets/8f840168-9550-4c77-aad0-cb87beb20499)
(This iconography simply vanishes once the pop level is reached).

Note that this is based on "players who have joined" (roundstart +
latejoin), not "players who are online" or "players who are alive".
Once an item becomes available it will never stop being available, but
it only becomes available based on people who are playing and not
watching.

Currently the only items I applied this to (with a value of 20 players)
are:
- Dual esword
- Sleeping Carp
- Spider Extract (the spider antagonist usually requires like 27
players)
- Romerol

It isn't applied to anything else.

## Why It's Good For The Game

Reputation isn't really a tool used to designate how dangerous an item
is any more (if it ever was) and resultingly it doesn't make any sense
to slow its gain based on population.
Some items though we maybe still don't want to show up in a "low pop"
round because they'll create an overall unsatisfying experience, so we
should be able to remove those items from play.

## Changelog

🆑
balance: Traitor reputation now advances at a fixed rate, not dependent
on current server population.
balance: The dual esword, sleeping carp scroll, spider extract, and
romerol vial cannot be purchased if fewer than 20 players have joined
the game.
/🆑
2025-02-22 22:38:40 +00:00

440 lines
15 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 its 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_ATOM_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))
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))
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_ATOM_EXAMINE, PROC_REF(on_examine))
src.lockable = lockable
src.active = enabled
if(!uplink_handler_override)
uplink_handler = new()
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))
if(!lockable)
active = TRUE
locked = FALSE
previous_attempts = list()
/datum/component/uplink/proc/handle_uplink_handler_update()
SIGNAL_HANDLER
SStgui.update_uis(src)
/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.add_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)
if(!istype(item))
return
SEND_SIGNAL(item, COMSIG_ITEM_ATTEMPT_TC_REIMBURSE, user, src)
/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["joined_population"] = length(GLOB.joined_player_list)
data["current_progression_scaling"] = SStraitor.current_progression_scaling
if(uplink_handler.primary_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)
data["primary_objectives"] = primary_objectives
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.stock_key in stock_list)
extra_purchasable_stock[REF(item)] = stock_list[item.stock_key]
var/atom/actual_item = item.item
extra_purchasable += list(list(
"id" = item.type,
"name" = item.name,
"icon" = actual_item.icon,
"icon_state" = actual_item.icon_state,
"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,
"population_minimum" = item.population_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)
data["can_renegotiate"] = user.mind == uplink_handler.owner && uplink_handler.can_replace_objectives?.Invoke() == TRUE
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["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("buy_raw_tc")
if (uplink_handler.telecrystals <= 0)
return
var/desired_amount = tgui_input_number(ui.user, "How many raw telecrystals to buy?", "Buy Raw TC", default = uplink_handler.telecrystals, max_value = uplink_handler.telecrystals)
if(!desired_amount || desired_amount < 1)
return
uplink_handler.purchase_raw_tc(ui.user, desired_amount, parent)
if("lock")
if(!lockable)
return TRUE
lock_uplink()
if("renegotiate_objectives")
uplink_handler.replace_objectives?.Invoke()
SStgui.update_uis(src)
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(LOWER_TEXT(new_ring_text)) != trim(LOWER_TEXT(unlock_code)))
if(trim(LOWER_TEXT(new_ring_text)) == trim(LOWER_TEXT(failsafe_code)))
failsafe(user)
return COMPONENT_STOP_RINGTONE_CHANGE
return
locked = FALSE
if(ismob(user))
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(LOWER_TEXT(message), LOWER_TEXT(unlock_code)))
if(failsafe_code && findtext(LOWER_TEXT(message), LOWER_TEXT(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))
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))
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
/datum/component/uplink/proc/failsafe(atom/source)
if(!parent)
return
var/turf/T = get_turf(parent)
if(!T)
return
var/user_deets = "an uplink failsafe explosion has been triggered"
if(ismob(source))
user_deets = "[ADMIN_LOOKUPFLW(source)] has triggered an uplink failsafe explosion"
source.log_message("triggered an uplink failsafe explosion. Uplink owner: [key_name(owner)].", LOG_ATTACK)
else if(istype(source, /obj/item/circuit_component))
var/obj/item/circuit_component/circuit = source
user_deets = "[circuit.parent.get_creator_admin()] has triggered an uplink failsafe explosion"
else
source?.log_message("somehow triggered an uplink failsafe explosion. Uplink owner: [key_name(owner)].", LOG_ATTACK)
message_admins("[user_deets] at [AREACOORD(T)] The owner of the uplink was [ADMIN_LOOKUPFLW(owner)].")
explosion(parent, devastation_range = 1, heavy_impact_range = 2, light_impact_range = 3)
qdel(parent) //Alternatively could brick the uplink.
#undef PEN_ROTATIONS