Removes Secondary & Final Objectives from Traitors (#89466)

## About The Pull Request


![image](https://github.com/user-attachments/assets/cb0bffb3-33be-4fb1-baec-a4d0f4a4cd57)
Pre-discussed with @Watermelon914, this PR removes Secondary & Final
Objectives from all Traitors, rather than just midround ones. It also
removes all of the surrounding supporting code.
Randomly assigned Primary Objectives still exist, I just used the
ability to rewrite mine to take the screenshot.

In terms of final objectives, the surrounding items that were available
still exist but don't necessarily have sources.
If anyone has good ideas for readding these in some other form it can be
done in future PRs.

It also allows all traitors to buy the Contractor kit, previously
limited to midround traitors which lacked secondary objectives, because
now all traitors lack secondary objectives.

This essentially limits all traitors to a maximum of 20 TC (16 if they
spawn with an uplink implant). Currently I don't foresee that they
strictly need any additional way of gaining TC during a round as 20 is
quite sufficient, but it may take some time to adjust and get used to it
after such a long time of having access to more. If we need to adjust
the starting value or add a slow drip of more points over time or
something, that can be done in followup PRs.

This also removes the ability to recreate your uplink added by my
beautiful wife in #74315
This was part of the progression traitor design document, but ultimately
probably a bad idea as it essentially made traitors impossible to
properly disarm. You will once more just need to carefully protect your
uplink.

**This does not remove the threat/progression system**. 
Like midround traitors, all Reputation requirements on gear are now
simple timelocks, most of which will have elapsed by the time 30 minutes
have passed.

**Finally** this PR also adds Romerol to the traitor uplink for 25 TC
and 30 minutes of reputation, as a treat (and because I removed the
final objective that previously granted it).

## Why It's Good For The Game

We've tried this system for a long time (3 years last month!) and while
I think it had a lot of promise, enabled some cool moments, and also
solved several of the problems it set out to solve, overall I think some
of the behaviours it has encouraged in players have been overall
negative for the game.
While the _game systems_ are fine, even quite fun and cool (especially
final objectives) I am of the opinion that having them in the game
creates a net negative purely in the way that they react with players'
_brains_, creating incentives towards behaviour we don't actually want
people to pursue.

While it's hard-to-impossible to prove any of this with hard data, there
has been a prevailing feeling for some time among many (though certainly
not all) people that the simple fact of _having_ a constant drip-feed of
objective available to players leads directly to less interesting
antagonist play. While certainly nobody is _forced_ to do secondary
objectives you are directly and quite strongly rewarded for doing so,
doing so efficiently, and doing so in a way which makes sure that nobody
(alive) sees you do it. This leads to a tendency to play defensively and
try to maximise the number of tasks you can complete in one round, which
also has a knock-on effect of generally minimising the number of people
you attempt to interact with in a round (unless you are killing them).

Even people who _intend_ on doing some more interesting gimmick can fall
into this trap, as "having more tools" is always useful for anyone who
is intending on any kind of plan at all, but then executing on the
secondary objectives again incentivises you to lay low, not interact
with anyone, be efficient, and then reduces the time you are spending
doing the thing that's your actual plan for the round. Removing the
ever-present temptation to fish for extra TC leaves "doing whatever your
actual plan is" as the sole thing to optimise.

Final Objectives too have created unfortunate psychological effects
between crewsided players and other antagonists. Because of the _threat_
(no matter how remote, Final Objectives have always been tuned to be
appropriately rare) that leaving any antagonist alone will cause them to
snowball by acquiring more power, it starts to feel foolish to respond
to any threat with less than the maximum possible level of force even if
they seem relatively innocuous in the moment. This even has an effect on
other non-progression antagonists, as traitors are the most common
antagonist type and how people treat them is going to be their default
level of reaction to most other station threats.

While there has always been the promise of expanding the system with
novel and exciting objectives that leverage appearing mid-round to do
something unique, we've taken very little advantage of that over time.
Most objectives we have added that didn't boil down to "kill someone,
with a twist" have been somewhat unsuccessful, serving either as ways to
get yourself arrested and killed for no reason or ways to get free
telecrystals by doing something the crew don't really care about
stopping you from doing. The option still exists to add more roundstart
objectives to traitors, if someone suddenly has a great idea that would
fit in this space.

The ideal outcome of making this change is a slight relaxation of crew
attitude towards feeling like their only option after catching an
antagonist that isn't sandbagging is to permanently remove them from the
round (although it's fine to do this still in many scenarios), and a
broadening of traitorous activity which is not purely focused on
collecting as many checkboxes as possible and might give people more
time to roleplay with other players, not worrying that this time could
have been more efficiently spent pursuing a different secondary goal.
I don't anticipate or desire that this will prevent traitors from
killing anyone (or even stop them from killing people they don't have a
specific objective to kill), I just want to remove the FOMO from
people's minds.

Also this gives us something to talk about at the coder townhall meeting
on the 22nd.

## Changelog

🆑
del: Misplaced or stolen traitor uplinks can no longer be recreated
using a radio code and special device, guard yours carefully or buy a
backup implant.
del: Roundstart traitors can no longer take on additional objectives in
order to earn additional Telecrystals and fast-forward any unlock timers
on items. They also cannot earn the ability to complete a Final
Objective.
balance: Roundstart traitors can now buy the Contractor Kit from their
traitor uplink, rather than only midround traitors.
add: Traitors can buy Romerol for 25 TC, after 30 minutes of time has
passed in a round.
/🆑
This commit is contained in:
Jacquerel
2025-03-12 16:00:35 -04:00
committed by Roxy
parent ed4ea0423d
commit 434e4e435e
66 changed files with 249 additions and 5346 deletions
@@ -246,7 +246,6 @@
/turf/open/floor/mineral/plastitanium/red,
/area/ruin/syndielab)
"oA" = (
/obj/structure/syndicate_uplink_beacon,
/turf/open/floor/iron/dark/textured,
/area/ruin/syndielab)
"oY" = (
@@ -267,10 +266,6 @@
pixel_x = -5;
pixel_y = 8
},
/obj/item/traitor_bug{
pixel_y = 6;
pixel_x = 6
},
/turf/open/floor/mineral/plastitanium/red,
/area/ruin/syndielab)
"qy" = (
@@ -102,7 +102,6 @@
"kp" = (
/obj/structure/safe,
/obj/item/stamp/syndicate,
/obj/item/traitor_bug,
/obj/item/eyesnatcher,
/obj/item/stack/spacecash/c1000,
/turf/open/floor/plating,
-1
View File
@@ -39176,7 +39176,6 @@
"ncb" = (
/obj/effect/decal/cleanable/dirt,
/obj/structure/table/wood,
/obj/item/paper/calling_card,
/obj/structure/alien/resin/membrane,
/obj/structure/alien/weeds,
/obj/structure/alien/weeds,
-8
View File
@@ -104,14 +104,6 @@
/obj/item/binoculars{
pixel_y = 10
},
/obj/item/traitor_bug{
pixel_y = 6;
pixel_x = 6
},
/obj/item/traitor_bug{
pixel_y = 3;
pixel_x = -5
},
/obj/item/storage/box/zipties{
pixel_y = -4;
pixel_x = 4
@@ -1,30 +1,5 @@
/// Called when the hack_comm_console objective is completed.
#define COMSIG_GLOB_TRAITOR_OBJECTIVE_COMPLETED "!traitor_objective_completed"
/// 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"
#define COMPONENT_TRAITOR_OBJECTIVE_ABORT_GENERATION (1<<0)
/// Called whenever the traitor objective is completed
#define COMSIG_TRAITOR_OBJECTIVE_COMPLETED "traitor_objective_completed"
/// Called whenever the traitor objective is failed
#define COMSIG_TRAITOR_OBJECTIVE_FAILED "traitor_objective_failed"
/// Called when a traitor bug is planted in an area
#define COMSIG_TRAITOR_BUG_PLANTED_GROUND "traitor_bug_planted_area"
/// Called when a traitor bug is planted
#define COMSIG_TRAITOR_BUG_PLANTED_OBJECT "traitor_bug_planted_object"
/// Called before a traitor bug is planted, where it can still be overrided
#define COMSIG_TRAITOR_BUG_PRE_PLANTED_OBJECT "traitor_bug_planted_pre_object"
#define COMPONENT_FORCE_PLACEMENT (1<<0)
#define COMPONENT_FORCE_FAIL_PLACEMENT (1<<1)
/// Called when a machine a traitor has booby trapped triggers its payload
#define COMSIG_TRAITOR_MACHINE_TRAP_TRIGGERED "traitor_machine_trap_triggered"
/// Called when a device a traitor has planted effects someone's mood. Pass the mind of the viewer.
#define COMSIG_DEMORALISING_EVENT "traitor_demoralise_event"
+2 -5
View File
@@ -9,13 +9,10 @@
/// This item is purchasable to clown ops
#define UPLINK_CLOWN_OPS (1 << 2)
/// This item is purchasable to infiltrators (midround traitors)
#define UPLINK_INFILTRATORS (1 << 3)
/// Can be randomly given to spies for their bounties
#define UPLINK_SPY (1 << 4)
#define UPLINK_SPY (1 << 3)
#define UPLINK_LONE_OP (1 << 5)
#define UPLINK_LONE_OP (1 << 4)
/// A blanket define for an item being purchasable by all types of nukie
#define UPLINK_ALL_SYNDIE_OPS (UPLINK_NUKE_OPS | UPLINK_LONE_OP | UPLINK_CLOWN_OPS)
@@ -73,11 +73,6 @@
integer = FALSE
min_val = 0.01
/// Determines how many potential objectives a traitor can have.
/datum/config_entry/number/maximum_potential_objectives
default = 6
min_val = 1
/datum/config_entry/number/changeling_scaling_coeff //how much does the amount of players get divided by to determine changelings
default = 6
integer = FALSE
@@ -60,7 +60,7 @@
/datum/dynamic_ruleset/latejoin/infiltrator
name = "Syndicate Infiltrator"
antag_datum = /datum/antagonist/traitor/infiltrator
antag_datum = /datum/antagonist/traitor
antag_flag = ROLE_SYNDICATE_INFILTRATOR
antag_flag_override = ROLE_TRAITOR
protected_roles = list(
@@ -238,7 +238,7 @@
/datum/dynamic_ruleset/midround/from_living/autotraitor
name = "Syndicate Sleeper Agent"
midround_ruleset_style = MIDROUND_RULESET_STYLE_LIGHT
antag_datum = /datum/antagonist/traitor/infiltrator/sleeper_agent
antag_datum = /datum/antagonist/traitor
antag_flag = ROLE_SLEEPER_AGENT
antag_flag_override = ROLE_TRAITOR
protected_roles = list(
@@ -276,7 +276,7 @@
var/mob/M = pick(candidates)
assigned += M
candidates -= M
var/datum/antagonist/traitor/infiltrator/sleeper_agent/newTraitor = new
var/datum/antagonist/traitor/newTraitor = new
M.mind.add_antag_datum(newTraitor)
message_admins("[ADMIN_LOOKUPFLW(M)] was selected by the [name] ruleset and has been made into a midround traitor.")
log_dynamic("[key_name(M)] was selected by the [name] ruleset and has been made into a midround traitor.")
-50
View File
@@ -9,15 +9,6 @@ SUBSYSTEM_DEF(traitor)
/// A list of all uplink items
var/list/uplink_items = list()
/// File to load configurations from.
var/configuration_path = "config/traitor_objective.json"
/// Global configuration data that gets applied to each objective when it is created.
/// Basic objective format
/// '/datum/traitor_objective/path/to/objective': {
/// "global_progression_influence_intensity": 0
/// }
var/configuration_data = list()
/// The coefficient multiplied by the current_global_progression for new joining traitors to calculate their progression
var/newjoin_progression_coeff = 1
/// The current progression that all traitors should be at in the round
@@ -29,31 +20,10 @@ SUBSYSTEM_DEF(traitor)
var/list/datum/uplink_handler/uplink_handlers = list()
/// The current scaling per minute of progression. Has a maximum value of 1 MINUTES.
var/current_progression_scaling = 1 MINUTES
/// Used to handle the probability of getting an objective.
var/datum/traitor_category_handler/category_handler
/// The current debug handler for objectives. Used for debugging objectives
var/datum/traitor_objective_debug/traitor_debug_panel
/// Used by the debug menu, decides whether newly created objectives should generate progression and telecrystals. Do not modify for non-debug purposes.
var/generate_objectives = TRUE
/// Objectives that have been completed by type. Used for limiting objectives.
var/list/taken_objectives_by_type = list()
/// A list of all existing objectives by type
var/list/all_objectives_by_type = list()
/datum/controller/subsystem/traitor/Initialize()
category_handler = new()
traitor_debug_panel = new(category_handler)
for(var/theft_item in subtypesof(/datum/objective_item/steal))
new theft_item
if(fexists(configuration_path))
var/list/data = json_decode(file2text(file(configuration_path)))
for(var/typepath in data)
var/actual_typepath = text2path(typepath)
if(!actual_typepath)
log_world("[configuration_path] has an invalid type ([typepath]) that doesn't exist in the codebase! Please correct or remove [typepath]")
configuration_data[actual_typepath] = data[typepath]
return SS_INIT_SUCCESS
/datum/controller/subsystem/traitor/fire(resumed)
@@ -83,7 +53,6 @@ SUBSYSTEM_DEF(traitor)
var/amount_to_give = progression_scaling_delta + (progression_scaling_delta * deviance)
amount_to_give = clamp(amount_to_give, 0, progression_scaling_delta * 2)
handler.progression_points += amount_to_give
handler.update_objectives()
handler.on_update()
/datum/controller/subsystem/traitor/proc/register_uplink_handler(datum/uplink_handler/uplink_handler)
@@ -97,22 +66,3 @@ SUBSYSTEM_DEF(traitor)
/datum/controller/subsystem/traitor/proc/uplink_handler_deleted(datum/uplink_handler/uplink_handler)
SIGNAL_HANDLER
uplink_handlers -= uplink_handler
/datum/controller/subsystem/traitor/proc/on_objective_taken(datum/traitor_objective/objective)
if(!istype(objective))
return
add_objective_to_list(objective, taken_objectives_by_type)
/datum/controller/subsystem/traitor/proc/get_taken_count(datum/traitor_objective/objective_type)
return length(taken_objectives_by_type[objective_type])
/datum/controller/subsystem/traitor/proc/add_objective_to_list(datum/traitor_objective/objective, list/objective_list)
var/datum/traitor_objective/current_type = objective.type
while(current_type != /datum/traitor_objective)
if(!objective_list[current_type])
objective_list[current_type] = list(objective)
else
objective_list[current_type] += objective
current_type = type2parent(current_type)
@@ -61,20 +61,6 @@
)
category = CAT_STRUCTURE
/datum/crafting_recipe/syndicate_uplink_beacon
name = "Syndicate Uplink Beacon"
result = /obj/structure/syndicate_uplink_beacon
tool_behaviors = list(TOOL_SCREWDRIVER)
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
crafting_flags = CRAFT_CHECK_DENSITY | CRAFT_MUST_BE_LEARNED
/datum/crafting_recipe/manucrate
name = "Manufacturing Storage Unit"
result = /obj/machinery/power/manufacturing/storagebox
+1 -90
View File
@@ -66,8 +66,6 @@
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
@@ -81,7 +79,6 @@
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
@@ -89,7 +86,6 @@
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
@@ -100,19 +96,6 @@
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)
/datum/component/uplink/InheritComponent(datum/component/uplink/uplink)
lockable |= uplink.lockable
active |= uplink.active
@@ -186,12 +169,9 @@
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.primary_objectives)
var/list/primary_objectives = list()
for(var/datum/objective/task as anything in uplink_handler.primary_objectives)
@@ -204,25 +184,6 @@
primary_objectives += list(task_data)
data["primary_objectives"] = primary_objectives
if(uplink_handler.has_objectives)
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["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()
@@ -263,7 +224,6 @@
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
@@ -308,47 +268,9 @@
if("renegotiate_objectives")
uplink_handler.replace_objectives?.Invoke()
SStgui.update_uis(src)
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
@@ -494,17 +416,6 @@
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(atom/source)
if(!parent)
return
-55
View File
@@ -408,41 +408,6 @@
message_admins("[key_name_admin(usr)] has unemag'ed [ai]'s Cyborgs.")
log_admin("[key_name(usr)] has unemag'ed [ai]'s Cyborgs.")
else if(href_list["edit_obj_tc"])
var/datum/traitor_objective/objective = locate(href_list["edit_obj_tc"])
if(!istype(objective))
return
var/telecrystal = input("Set new telecrystal reward for [objective.name]","Syndicate uplink", objective.telecrystal_reward) as null | num
if(isnull(telecrystal))
return
objective.telecrystal_reward = telecrystal
message_admins("[key_name_admin(usr)] changed [objective]'s telecrystal reward count to [telecrystal].")
log_admin("[key_name(usr)] changed [objective]'s telecrystal reward count to [telecrystal].")
else if(href_list["edit_obj_pr"])
var/datum/traitor_objective/objective = locate(href_list["edit_obj_pr"])
if(!istype(objective))
return
var/progression = input("Set new progression reward for [objective.name]","Syndicate uplink", objective.progression_reward) as null | num
if(isnull(progression))
return
objective.progression_reward = progression
message_admins("[key_name_admin(usr)] changed [objective]'s progression reward count to [progression].")
log_admin("[key_name(usr)] changed [objective]'s progression reward count to [progression].")
else if(href_list["fail_objective"])
var/datum/traitor_objective/objective = locate(href_list["fail_objective"])
if(!istype(objective))
return
var/performed = objective.objective_state == OBJECTIVE_STATE_INACTIVE? "skipped" : "failed"
message_admins("[key_name_admin(usr)] forcefully [performed] [objective].")
log_admin("[key_name(usr)] forcefully [performed] [objective].")
objective.fail_objective()
else if(href_list["succeed_objective"])
var/datum/traitor_objective/objective = locate(href_list["succeed_objective"])
if(!istype(objective))
return
message_admins("[key_name_admin(usr)] forcefully succeeded [objective].")
log_admin("[key_name(usr)] forcefully succeeded [objective].")
objective.succeed_objective()
else if (href_list["common"])
switch(href_list["common"])
if("undress")
@@ -478,26 +443,6 @@
uplink.uplink_handler.progression_points = progression
message_admins("[key_name_admin(usr)] changed [current]'s progression point count to [progression].")
log_admin("[key_name(usr)] changed [current]'s progression point count to [progression].")
uplink.uplink_handler.update_objectives()
uplink.uplink_handler.generate_objectives()
if("give_objective")
if(!check_rights(R_FUN))
return
var/datum/component/uplink/uplink = find_syndicate_uplink()
if(!uplink || !uplink.uplink_handler)
return
var/list/all_objectives = subtypesof(/datum/traitor_objective)
var/objective_typepath = tgui_input_list(usr, "Select objective", "Select objective", all_objectives)
if(!objective_typepath)
return
var/datum/traitor_objective/objective = uplink.uplink_handler.try_add_objective(objective_typepath, force = TRUE)
if(objective)
message_admins("[key_name_admin(usr)] gave [current] a traitor objective ([objective_typepath]).")
log_admin("[key_name(usr)] gave [current] a traitor objective ([objective_typepath]).")
else
to_chat(usr, span_warning("Failed to generate the objective!"))
message_admins("[key_name_admin(usr)] failed to give [current] a traitor objective ([objective_typepath]).")
log_admin("[key_name(usr)] failed to give [current] a traitor objective ([objective_typepath]).")
if("uplink")
var/datum/antagonist/traitor/traitor_datum = has_antag_datum(/datum/antagonist/traitor)
if(!give_uplink(antag_datum = traitor_datum || null))
+48
View File
@@ -1,7 +1,55 @@
GLOBAL_DATUM_INIT(steal_item_handler, /datum/objective_item_handler, new())
/proc/add_item_to_steal(source, type)
GLOB.steal_item_handler.objectives_by_path[type] += source
return type
/// Holds references to information about all of the items you might need to steal for objectives
/datum/objective_item_handler
var/list/list/objectives_by_path
var/generated_items = FALSE
/datum/objective_item_handler/New()
. = ..()
objectives_by_path = list()
for(var/datum/objective_item/item as anything in subtypesof(/datum/objective_item))
objectives_by_path[initial(item.targetitem)] = list()
RegisterSignal(SSatoms, COMSIG_SUBSYSTEM_POST_INITIALIZE, PROC_REF(save_items))
RegisterSignal(SSdcs, COMSIG_GLOB_NEW_ITEM, PROC_REF(new_item_created))
/datum/objective_item_handler/proc/new_item_created(datum/source, obj/item/item)
SIGNAL_HANDLER
if(HAS_TRAIT(item, TRAIT_ITEM_OBJECTIVE_BLOCKED))
return
if(!generated_items)
item.add_stealing_item_objective()
return
var/typepath = item.add_stealing_item_objective()
if(typepath != null)
register_item(item, typepath)
/// Registers all items that are potentially stealable and removes ones that aren't.
/// We still need to do things this way because on mapload, items may not be on the station until everything has finished loading.
/datum/objective_item_handler/proc/save_items()
SIGNAL_HANDLER
for(var/obj/item/typepath as anything in objectives_by_path)
var/list/obj_by_path_cache = objectives_by_path[typepath].Copy()
for(var/obj/item/object as anything in obj_by_path_cache)
register_item(object, typepath)
generated_items = TRUE
/datum/objective_item_handler/proc/register_item(atom/object, typepath)
var/turf/place = get_turf(object)
if(!place || !is_station_level(place.z))
objectives_by_path[typepath] -= object
return
RegisterSignal(object, COMSIG_QDELETING, PROC_REF(remove_item))
/datum/objective_item_handler/proc/remove_item(atom/source)
SIGNAL_HANDLER
for(var/typepath in objectives_by_path)
objectives_by_path[typepath] -= source
//Contains the target item datums for Steal objectives.
/datum/objective_item
/// How the item is described in the objective
@@ -1,114 +0,0 @@
/// 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 receiving 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), hidden = TRUE))
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, hidden = TRUE))
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
-4
View File
@@ -199,14 +199,10 @@ GLOBAL_VAR(antag_prototypes)
var/uplink_info = "<i><b>Uplink</b></i>:"
var/datum/component/uplink/U = find_syndicate_uplink()
if(U)
if(!U.uplink_handler.has_objectives)
uplink_info += "<a href='byond://?src=[REF(src)];common=takeuplink'>take</a>"
if (check_rights(R_FUN, 0))
uplink_info += ", <a href='byond://?src=[REF(src)];common=crystals'>[U.uplink_handler.telecrystals]</a> TC"
if(U.uplink_handler.has_progression)
uplink_info += ", <a href='byond://?src=[REF(src)];common=progression'>[U.uplink_handler.progression_points]</a> PR"
if(U.uplink_handler.has_objectives)
uplink_info += ", <a href='byond://?src=[REF(src)];common=give_objective'>Force Give Objective</a>"
else
uplink_info += ", [U.uplink_handler.telecrystals] TC"
if(U.uplink_handler.has_progression)
-1
View File
@@ -765,7 +765,6 @@ ADMIN_VERB(secrets, R_NONE, "Secrets", "Abuse harder than you ever have before w
assign_admin_objective_and_antag(player, antag_datum)
var/datum/uplink_handler/uplink = antag_datum.uplink_handler
uplink.has_progression = FALSE
uplink.has_objectives = FALSE
if(ROLE_CHANGELING)
var/datum/antagonist/changeling/antag_datum = new
antag_datum.give_objectives = keep_generic_objecives
@@ -1,58 +0,0 @@
# Progression Traitor Balance Guide
This guide will explain how the values for progression traitor works, how to balance progression traitors and what you should NOT do when balancing.
This guide will only explain progression values.
## Definitions
- Progression points OR Progression - A currency that controls what uplink items a player can purchase and what objectives they have accessible to them. Gained passively or by completing objectives and has diminishing returns as it strays from the expected progression
- Expected Progression - A global value that increments by a value of 1 minute every minute, representing the 'time' that a player should be at if they had not completed any objectives.
- Objectives - An activity or job that a player can take for rewards such as TC and progression points.
- Player - The user(s) that are playing as the antagonist in this new system.
- Expected deviance - The amount of deviance that can be expected from the minimum and maximum progressions. Usually calculated by `progression_scaling_deviance` + `progression_scaling_deviance` * `global_progression_deviance_required` (explained further down)
## How it works
This section will explain how the entire balance system works. This is an overview of the entire system.
### Progression
Progression points is passively given to a player, and are represented as minutes (or time values) in code. The round has its own 'expected progression', which is the progression value that you'd normally have if you hadn't completed any objectives whatsoever. This is the baseline progression that all players will be at unless they're a latejoiner, and it acts as the basis for determining how much progression points a player should get over time and the cost of objectives for a specific player, if they deviate too much from this value. The idea is that they will slowly drift back towards the expected progression if they do nothing and it becomes harder for them to progress as they deviate further from the expected progression. The amount that is passively given can also vary depending on how many players there are, so that at lower populations, expected progression rises more slowly.
### Objectives
Objectives are worth a certain amount of progression points, determined by the code. However, this can be scaled to be less if the player taking them is ahead of the expected progression. This scales exponentially, so that as a player deviates further from the expected progression, the reward diminishes exponentially, up to a reduced value of 90%. The similar thing happens in the opposite direction, with people who are lower than the expected progression getting more progression than usual for completing objectives.
## How to balance
### The traitor subsystem
- `newjoin_progression_coeff` - The coefficient multiplied by the expected progression for new joining traitors to calculate their starting progression, so that they don't start from scratch
- `progression_scaling_deviance` - The value that the entire system revolves around. This determines how your disadvantages are calculated, if you stray from the expected progression. Having a progression value that is `progression_scaling_deviance` minutes off of the expected progression means that you won't get any progression at all, and if objectives were configured to suit this, you'd have the highest reduction you can possibly get. From the expected progression to this value, it scales linearly and it also works in the opposite direction.
- `current_progression_scaling` - Defined at compile time, this determines how fast expected progression scales. So if you have it set to 0.5 MINUTES, it'll take twice as long to unlock uplink items and new objectives.
- `CONFIG:TRAITOR_IDEAL_PLAYER_COUNT` - The ideal player count before expected progression stops increasing. If the living player list gets below this value, the current progression scaling will be multiplied by player_count/traitor_ideal_player_count. In essence, this makes it so that progression scales more slowly when there isn't a lot of people alive.
If you want to balance how fast the system progresses, you should look at modifying `current_progression_scaling`. If you want to balance how far someone should be allowed to deviate, you should look at modifying `progression-scaling-deviance`
### Objectives
- `progression_minimum` - The minimum number of progression points required before this objective can show up as a potential objective
- `progression_maximum` - The maximum number of progression points before this objective stops showing up as a potential objective, used to prevent roundstart objectives from showing up during the late game.
- `progression_reward` - The progression reward you get from completing an objective. This is the base value, and can also be a two element list of numbers if you want it to be random. This value is then scaled depending on whether a player is ahead or behind the expected progression
- `global_progression_influence_intensity` - Determines how influential expected progression will affect the progression reward of this objective. Set to 0 to disable.
- `global_progression_deviance_required` - Determines how much deviance is required before the scaling kicks in, to give objectives more leeway so that at the `progression_scaling_deviance`, it doesn't scale to 90% immediately.
- `progression_cost_coeff_deviance` - This determines the randomness of the progression reward, to prevent all of the scaling from looking the same. Becomes a lot less significant as the scaling variable gets closer to 1.
If you want to balance the expected timeframe an objective should be available, you should look at changing the `progression_minimum` or `progression_maximum`. If you want to balance how much objectives reward, you may want to look at modifying `progression_reward`. If you want to look at balancing the cost of an objective depending on the expected progression, you may want to look at `global_progression_influence_intensity`. If you want to look at decreasing or increasing the deviance allowed before objectives become worthless progression-wise, you may want to look at modifying `global_progression_deviance_required`
### Uplink Items
- `progression_minimum` - The minimum number of progression points required to purchase this uplink item.
## What NOT to do when balancing
### Overcompensate
You do not want to overcompensate variables such as `progression_minimum` and `progression_maximum`. Such values need to be an accurate representation of roughly around the time a player should unlock the objective or uplink item. progression_scaling_deviance is supposed to represents the limit that a casual player can be at before it becomes significantly harder for them to progress throughout. You should expect people to be within `progression_scaling_deviance` + `progression_scaling_deviance` * `global_progression_deviance_required`. (Assuming `progression_scaling_deviance` is 20 minutes and `progression_scaling_deviance_required` is 0.5, 20 + 0.5 * 20 = 30; this gives us a value of 30 minutes). This is the expected deviance.
### Reward large amounts of progression points
Progression points are passively gained, so rewarding large amounts of progression points will let people bypass the scaling as they'll immediately jump to an absurd value. A good rule of thumb is to always keep the reward within or below the expected deviance.
@@ -1,109 +0,0 @@
ADMIN_VERB(debug_traitor_objectives, R_DEBUG, "Debug Traitor Objectives", "Verify functionality of traitor goals.", ADMIN_CATEGORY_DEBUG)
SStraitor.traitor_debug_panel?.ui_interact(user.mob)
/datum/traitor_objective_debug
var/list/all_objectives
/datum/traitor_objective_debug/New(datum/traitor_category_handler/category_handler)
. = ..()
all_objectives = list()
for(var/datum/traitor_objective_category/category as anything in category_handler.all_categories)
var/list/generated_list = list()
var/list/current_list = category.objectives
for(var/value in category.objectives)
if(islist(value))
generated_list += list(list(
"objectives" = recursive_list_generate(value),
"weight" = current_list[value]
))
else
generated_list += list(generate_objective_data(value, current_list[value]))
all_objectives += list(list(
"name" = category.name,
"objectives" = generated_list,
"weight" = category.weight,
))
/datum/traitor_objective_debug/proc/recursive_list_generate(list/to_check)
var/list/generated_list = list()
for(var/value in to_check)
if(islist(value))
generated_list += list(list(
"objectives" = recursive_list_generate(value),
"weight" = to_check[value]
))
else
generated_list += list(generate_objective_data(value, to_check[value]))
return generated_list
/datum/traitor_objective_debug/proc/generate_objective_data(datum/traitor_objective/objective_type, weight)
// Need to set this to false before we create the new objective to prevent init from fucking it up
SStraitor.generate_objectives = FALSE
var/datum/traitor_objective/objective = new objective_type()
var/list/return_data = list(
"name" = objective.name,
"description" = objective.description,
"progression_minimum" = objective.progression_minimum,
"progression_maximum" = objective.progression_maximum,
"global_progression" = objective.global_progression_deviance_required,
"global_progression_limit_coeff" = objective.global_progression_limit_coeff,
"global_progression_influence_intensity" = objective.global_progression_influence_intensity,
"progression_reward" = objective.progression_reward,
"telecrystal_reward" = objective.telecrystal_reward,
"telecrystal_penalty" = objective.telecrystal_penalty,
"weight" = weight,
"type" = objective.type,
)
qdel(objective)
SStraitor.generate_objectives = TRUE
return return_data
/datum/traitor_objective_debug/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "TraitorObjectiveDebug")
ui.open()
/datum/traitor_objective_debug/ui_data(mob/user)
var/list/data = list()
data["current_progression"] = SStraitor.current_global_progression
var/list/handlers = SStraitor.uplink_handlers
var/list/handler_data = list()
for(var/datum/uplink_handler/handler as anything in handlers)
var/total_progression_from_objectives = 0
for(var/datum/traitor_objective/objective as anything in handler.completed_objectives)
if(objective.objective_state != OBJECTIVE_STATE_COMPLETED)
continue
total_progression_from_objectives += objective.progression_reward
handler_data += list(list(
"player" = handler.owner?.key,
"progression_points" = handler.progression_points,
"total_progression_from_objectives" = total_progression_from_objectives
))
data["player_data"] = handler_data
return data
/datum/traitor_objective_debug/ui_static_data(mob/user)
var/list/data = list()
data["objective_data"] = all_objectives
data["progression_scaling_deviance"] = SStraitor.progression_scaling_deviance
return data
/datum/traitor_objective_debug/ui_state(mob/user)
return GLOB.admin_state
/datum/traitor_objective_debug/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
. = ..()
if(.)
return
switch(action)
if("set_current_expected_progression")
SStraitor.current_global_progression = text2num(params["new_expected_progression"])
return TRUE
if("generate_json")
var/temp_file = file("data/TraitorObjectiveDownloadTempFile")
fdel(temp_file)
WRITE_FILE(temp_file, all_objectives)
DIRECT_OUTPUT(ui.user, ftp(temp_file, "TraitorObjectiveData.json"))
return TRUE
@@ -1,54 +0,0 @@
/// Helper component that registers signals on an object
/// This is not necessary to use and gives little control over the conditions
/datum/component/traitor_objective_register
dupe_mode = COMPONENT_DUPE_ALLOWED
/// The target to apply the succeed/fail signals onto
var/datum/target
/// Signals to listen out for to automatically succeed the objective
var/succeed_signals
/// Signals to listen out for to automatically fail the objective.
var/fail_signals
/// Whether failing has a penalty
var/penalty = 0
/datum/component/traitor_objective_register/Initialize(datum/target, succeed_signals, fail_signals, penalty)
. = ..()
if(!istype(parent, /datum/traitor_objective))
return COMPONENT_INCOMPATIBLE
src.target = target
src.succeed_signals = succeed_signals
src.fail_signals = fail_signals
src.penalty = penalty
/datum/component/traitor_objective_register/RegisterWithParent()
if(succeed_signals)
RegisterSignals(target, succeed_signals, PROC_REF(on_success))
if(fail_signals)
RegisterSignals(target, fail_signals, PROC_REF(on_fail))
RegisterSignals(parent, list(COMSIG_TRAITOR_OBJECTIVE_COMPLETED, COMSIG_TRAITOR_OBJECTIVE_FAILED), PROC_REF(delete_self))
/datum/component/traitor_objective_register/UnregisterFromParent()
if(target)
if(succeed_signals)
UnregisterSignal(target, succeed_signals)
if(fail_signals)
UnregisterSignal(target, fail_signals)
UnregisterSignal(parent, list(
COMSIG_TRAITOR_OBJECTIVE_COMPLETED,
COMSIG_TRAITOR_OBJECTIVE_FAILED
))
/datum/component/traitor_objective_register/proc/on_fail(datum/traitor_objective/source)
SIGNAL_HANDLER
var/datum/traitor_objective/objective = parent
objective.fail_objective(penalty)
/datum/component/traitor_objective_register/proc/on_success()
SIGNAL_HANDLER
var/datum/traitor_objective/objective = parent
objective.succeed_objective()
/datum/component/traitor_objective_register/proc/delete_self()
SIGNAL_HANDLER
qdel(src)
@@ -1,41 +0,0 @@
/// Helper component to track events on
/datum/component/traitor_objective_limit_per_time
dupe_mode = COMPONENT_DUPE_HIGHLANDER
/// The maximum time that an objective will be considered for. Set to -1 to accept any time.
var/time_period = 0
/// The maximum amount of objectives that can be active or recently active at one time
var/maximum_objectives = 0
/// The typepath which we check for
var/typepath
/datum/component/traitor_objective_limit_per_time/Initialize(typepath, time_period, maximum_objectives)
. = ..()
if(!istype(parent, /datum/traitor_objective))
return COMPONENT_INCOMPATIBLE
src.time_period = time_period
src.maximum_objectives = maximum_objectives
src.typepath = typepath
if(!typepath)
src.typepath = parent.type
/datum/component/traitor_objective_limit_per_time/RegisterWithParent()
RegisterSignal(parent, COMSIG_TRAITOR_OBJECTIVE_PRE_GENERATE, PROC_REF(handle_generate))
/datum/component/traitor_objective_limit_per_time/UnregisterFromParent()
UnregisterSignal(parent, COMSIG_TRAITOR_OBJECTIVE_PRE_GENERATE)
/datum/component/traitor_objective_limit_per_time/proc/handle_generate(datum/traitor_objective/source, datum/mind/owner, list/potential_duplicates)
SIGNAL_HANDLER
var/datum/uplink_handler/handler = source.handler
if(!handler)
return
var/count = 0
for(var/datum/traitor_objective/objective as anything in handler.potential_duplicate_objectives[typepath])
if(time_period != -1 && objective.objective_state != OBJECTIVE_STATE_INACTIVE && (world.time - objective.time_of_completion) > time_period)
continue
count++
if(count >= maximum_objectives)
return COMPONENT_TRAITOR_OBJECTIVE_ABORT_GENERATION
@@ -1,40 +0,0 @@
/// Helper component to track events on
/datum/component/traitor_objective_mind_tracker
dupe_mode = COMPONENT_DUPE_ALLOWED
/// The target to track
var/datum/mind/target
/// Signals to listen out for mapped to procs to call
var/list/signals
/// Current registered target
var/mob/current_registered_target
/datum/component/traitor_objective_mind_tracker/Initialize(datum/target, signals)
. = ..()
if(!istype(parent, /datum/traitor_objective))
return COMPONENT_INCOMPATIBLE
src.target = target
src.signals = signals
/datum/component/traitor_objective_mind_tracker/RegisterWithParent()
RegisterSignal(target, COMSIG_MIND_TRANSFERRED, PROC_REF(handle_mind_transferred))
RegisterSignal(target, COMSIG_QDELETING, PROC_REF(delete_self))
RegisterSignals(parent, list(COMSIG_TRAITOR_OBJECTIVE_COMPLETED, COMSIG_TRAITOR_OBJECTIVE_FAILED), PROC_REF(delete_self))
handle_mind_transferred(target)
/datum/component/traitor_objective_mind_tracker/UnregisterFromParent()
UnregisterSignal(target, COMSIG_MIND_TRANSFERRED)
if(target.current)
parent.UnregisterSignal(target.current, signals)
/datum/component/traitor_objective_mind_tracker/proc/handle_mind_transferred(datum/source, mob/previous_body)
SIGNAL_HANDLER
if(current_registered_target)
parent.UnregisterSignal(current_registered_target, signals)
for(var/signal in signals)
parent.RegisterSignal(target.current, signal, signals[signal])
/datum/component/traitor_objective_mind_tracker/proc/delete_self()
SIGNAL_HANDLER
qdel(src)
@@ -23,15 +23,9 @@
var/uplink_flag_given = UPLINK_TRAITORS
var/give_objectives = TRUE
/// Whether to give secondary objectives to the traitor, which aren't necessary but can be completed for a progression and TC boost.
var/give_secondary_objectives = TRUE
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
@@ -53,17 +47,6 @@
///the final objective the traitor has to accomplish, be it escaping, hijacking, or just martyrdom.
var/datum/objective/ending_objective
/datum/antagonist/traitor/infiltrator
// Used to denote traitors who have joined midround and therefore have no access to secondary objectives.
// Progression elements are best left to the roundstart antagonists
// There will still be a timelock on uplink items
name = "\improper Infiltrator"
give_secondary_objectives = FALSE
uplink_flag_given = UPLINK_INFILTRATORS
/datum/antagonist/traitor/infiltrator/sleeper_agent
name = "\improper Syndicate Sleeper Agent"
/datum/antagonist/traitor/New(give_objectives = TRUE)
. = ..()
src.give_objectives = give_objectives
@@ -73,7 +56,6 @@
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)
@@ -87,10 +69,6 @@
uplink_handler.has_progression = TRUE
SStraitor.register_uplink_handler(uplink_handler)
if(give_secondary_objectives)
uplink_handler.has_objectives = TRUE
uplink_handler.generate_objectives()
uplink_handler.can_replace_objectives = CALLBACK(src, PROC_REF(can_change_objectives))
uplink_handler.replace_objectives = CALLBACK(src, PROC_REF(submit_player_objective))
@@ -114,77 +92,20 @@
pick_employer()
owner.teach_crafting_recipe(/datum/crafting_recipe/syndicate_uplink_beacon)
return ..()
/datum/antagonist/traitor/on_removal()
if(!isnull(uplink_handler))
uplink_handler.has_objectives = FALSE
uplink_handler.can_replace_objectives = null
uplink_handler.replace_objectives = null
owner.take_uplink()
owner.special_role = null
owner.forget_crafting_recipe(/datum/crafting_recipe/syndicate_uplink_beacon)
return ..()
/datum/antagonist/traitor/proc/traitor_objective_to_html(datum/traitor_objective/to_display)
var/string = "[to_display.name]"
if(to_display.objective_state == OBJECTIVE_STATE_ACTIVE || to_display.objective_state == OBJECTIVE_STATE_INACTIVE)
string += " <a href='byond://?src=[REF(owner)];edit_obj_tc=[REF(to_display)]'>[to_display.telecrystal_reward] TC</a>"
string += " <a href='byond://?src=[REF(owner)];edit_obj_pr=[REF(to_display)]'>[to_display.progression_reward] PR</a>"
else
string += ", [to_display.telecrystal_reward] TC"
string += ", [to_display.progression_reward] PR"
if(to_display.objective_state == OBJECTIVE_STATE_ACTIVE && !istype(to_display, /datum/traitor_objective/ultimate))
string += " <a href='byond://?src=[REF(owner)];fail_objective=[REF(to_display)]'>Fail this objective</a>"
string += " <a href='byond://?src=[REF(owner)];succeed_objective=[REF(to_display)]'>Succeed this objective</a>"
if(to_display.objective_state == OBJECTIVE_STATE_INACTIVE)
string += " <a href='byond://?src=[REF(owner)];fail_objective=[REF(to_display)]'>Dispose of this objective</a>"
if(to_display.skipped)
string += " - <b>Skipped</b>"
else if(to_display.objective_state == OBJECTIVE_STATE_FAILED)
string += " - <b><font color='red'>Failed</font></b>"
else if(to_display.objective_state == OBJECTIVE_STATE_INVALID)
string += " - <b>Invalidated</b>"
else if(to_display.objective_state == OBJECTIVE_STATE_COMPLETED)
string += " - <b><font color='green'>Succeeded</font></b>"
return string
/datum/antagonist/traitor/antag_panel_objectives()
var/result = ..()
if(!uplink_handler)
return result
result += "<i><b>Traitor specific objectives</b></i><br>"
result += "<i><b>Concluded Objectives</b></i>:<br>"
for(var/datum/traitor_objective/objective as anything in uplink_handler.completed_objectives)
result += "[traitor_objective_to_html(objective)]<br>"
if(!length(uplink_handler.completed_objectives))
result += "EMPTY<br>"
result += "<i><b>Ongoing Objectives</b></i>:<br>"
for(var/datum/traitor_objective/objective as anything in uplink_handler.active_objectives)
result += "[traitor_objective_to_html(objective)]<br>"
if(!length(uplink_handler.active_objectives))
result += "EMPTY<br>"
result += "<i><b>Potential Objectives</b></i>:<br>"
for(var/datum/traitor_objective/objective as anything in uplink_handler.potential_objectives)
result += "[traitor_objective_to_html(objective)]<br>"
if(!length(uplink_handler.potential_objectives))
result += "EMPTY<br>"
result += "<a href='byond://?src=[REF(owner)];common=give_objective'>Force add objective</a><br>"
return result
/// Returns true if we're allowed to assign ourselves a new objective
/datum/antagonist/traitor/proc/can_change_objectives()
return can_assign_self_objectives
/// 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/proc/pick_employer()
if(!employer)
var/faction = prob(75) ? FLAVOR_FACTION_SYNDICATE : FLAVOR_FACTION_NANOTRASEN
@@ -300,8 +221,6 @@
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"]
@@ -340,9 +259,6 @@
traitor_won = FALSE
objectives_text += "<br><B>Objective #[count]</B>: [objective.explanation_text] [objective.get_roundend_success_suffix()]"
count++
if(uplink_handler.final_objective)
objectives_text += "<br>[span_greentext("[traitor_won ? "Additionally" : "However"], the final objective \"[uplink_handler.final_objective]\" was completed!")]"
traitor_won = TRUE
result += "<br>[owner.name] <B>[traitor_flavor["roundend_report"]]</B>"
@@ -1,68 +0,0 @@
/// The traitor category handler. This is where the probability of all objectives are managed.
/datum/traitor_category_handler
var/list/datum/traitor_objective_category/all_categories = list()
/datum/traitor_category_handler/New()
. = ..()
for(var/type in subtypesof(/datum/traitor_objective_category))
var/datum/traitor_objective_category/category = new type()
if(length(category.objectives))
all_categories += category
else
// Category should just get autoGC'd here if they don't have any length, this may not be necessary
qdel(category)
/datum/traitor_category_handler/proc/objective_valid(datum/traitor_objective/objective_path, progression_points)
if(initial(objective_path.abstract_type) == objective_path)
return FALSE
if(progression_points < initial(objective_path.progression_minimum))
return FALSE
if(progression_points > initial(objective_path.progression_maximum))
return FALSE
return TRUE
/datum/traitor_category_handler/proc/get_possible_objectives(progression_points)
var/list/valid_objectives = list()
for(var/datum/traitor_objective_category/category as anything in all_categories)
var/list/category_list = list()
for(var/value in category.objectives)
if(islist(value))
var/list/objective_category = filter_invalid_objective_list(value, progression_points)
if(!length(objective_category))
continue
category_list[objective_category] = category.objectives[value]
else
if(!objective_valid(value, progression_points))
continue
category_list[value] = category.objectives[value]
if(!length(category_list))
continue
valid_objectives[category_list] = category.weight
return valid_objectives
/datum/traitor_category_handler/proc/filter_invalid_objective_list(list/objectives, progression_points)
var/list/filtered_objectives = list()
for(var/value in objectives)
if(islist(value))
var/list/result = filter_invalid_objective_list(value, progression_points)
if(!length(result))
continue
filtered_objectives[result] = objectives[value]
else
if(!objective_valid(value, progression_points))
continue
filtered_objectives[value] = objectives[value]
return filtered_objectives
/// The objective category.
/// Used to group up entire objectives into 1 weight objects to prevent having a
/// higher chance of getting an objective due to an increased number of different objective subtypes.
/// These are nothing but informational holders and will have no other purpose.
/datum/traitor_objective_category
/// Name of the category, unused but may help in the future
var/name = "generic category"
/// Assoc list of objectives by type mapped to their weight. Can also contain lists of objectives mapped to weight
var/list/objectives = list()
/// The weight of the category. How likely this category is to be chosen.
var/weight = OBJECTIVE_WEIGHT_DEFAULT
@@ -1,33 +0,0 @@
/**
* The point of this datum is to act as a means to group target player objectives
* Not all 'target player' objectives have to be under this subtype, it's only used if you don't want duplicates among the current
* children types under this type.
*/
/datum/traitor_objective/target_player
abstract_type = /datum/traitor_objective/target_player
progression_minimum = 30 MINUTES
// The code below is for limiting how often you can get this objective. You will get this objective at a maximum of maximum_objectives_in_period every objective_period
/// The objective period at which we consider if it is an 'objective'. Set to 0 to accept all objectives.
var/objective_period = 15 MINUTES
/// The maximum number of objectives we can get within this period.
var/maximum_objectives_in_period = 4
/// The target that we need to target.
var/mob/living/target
/datum/traitor_objective/target_player/Destroy(force)
set_target(null)
return ..()
/datum/traitor_objective/target_player/proc/set_target(mob/living/new_target)
if(target)
UnregisterSignal(target, COMSIG_QDELETING)
target = new_target
if(target)
RegisterSignal(target, COMSIG_QDELETING, PROC_REF(target_deleted))
/datum/traitor_objective/target_player/proc/target_deleted(datum/source)
SIGNAL_HANDLER
set_target(null)
@@ -1,205 +0,0 @@
/datum/traitor_objective_category/assassinate_kidnap
name = "Assassination/Kidnap"
objectives = list(
list(
/datum/traitor_objective/target_player/assassinate/calling_card = 1,
/datum/traitor_objective/target_player/assassinate/calling_card/heads_of_staff = 1,
) = 1,
list(
list(
/datum/traitor_objective/target_player/kidnapping/common = 20,
/datum/traitor_objective/target_player/kidnapping/common/assistant = 1,
) = 4,
/datum/traitor_objective/target_player/kidnapping/uncommon = 3,
/datum/traitor_objective/target_player/kidnapping/rare = 2,
/datum/traitor_objective/target_player/kidnapping/captain = 1
) = 1,
)
/datum/traitor_objective/target_player/assassinate
name = "Assassinate %TARGET% the %JOB TITLE%"
description = "Simply kill your target to accomplish this objective."
abstract_type = /datum/traitor_objective/target_player/assassinate
progression_minimum = 30 MINUTES
/**
* Makes the objective only set heads as targets when true, and block them from being targets when false.
* This also blocks the objective from generating UNTIL the un-heads_of_staff version (WHICH SHOULD BE A DIRECT PARENT) is completed.
* example: calling card objective, you kill someone, you unlock the chance to roll a head of staff target version of calling card.
*/
var/heads_of_staff = FALSE
duplicate_type = /datum/traitor_objective/target_player
/datum/traitor_objective/target_player/assassinate/supported_configuration_changes()
. = ..()
. += NAMEOF(src, objective_period)
. += NAMEOF(src, maximum_objectives_in_period)
/datum/traitor_objective/target_player/assassinate/calling_card
name = "Assassinate %TARGET% the %JOB TITLE%, and plant a calling card"
description = "Kill your target and plant a calling card in the pockets of your victim. If your calling card gets destroyed before you are able to plant it, this objective will fail."
progression_reward = 2 MINUTES
telecrystal_reward = list(1, 2)
var/obj/item/paper/calling_card/card
/datum/traitor_objective/target_player/assassinate/calling_card/heads_of_staff
progression_reward = 4 MINUTES
telecrystal_reward = list(2, 3)
heads_of_staff = TRUE
/datum/traitor_objective/target_player/assassinate/calling_card/generate_ui_buttons(mob/user)
var/list/buttons = list()
if(!card)
buttons += add_ui_button("", "Pressing this will materialize a calling card, which you must plant to succeed.", "paper-plane", "summon_card")
return buttons
/datum/traitor_objective/target_player/assassinate/calling_card/ui_perform_action(mob/living/user, action)
. = ..()
switch(action)
if("summon_card")
if(card)
return
card = new(user.drop_location())
user.put_in_hands(card)
card.balloon_alert(user, "the card materializes in your hand")
RegisterSignal(card, COMSIG_ITEM_EQUIPPED, PROC_REF(on_card_planted))
AddComponent(/datum/component/traitor_objective_register, card, \
succeed_signals = null, \
fail_signals = list(COMSIG_QDELETING), \
penalty = TRUE)
/datum/traitor_objective/target_player/assassinate/calling_card/proc/on_card_planted(datum/source, mob/living/equipper, slot)
SIGNAL_HANDLER
if(equipper != target)
return //your target please
if(equipper.stat != DEAD)
return //kill them please
if(!(slot & (ITEM_SLOT_LPOCKET|ITEM_SLOT_RPOCKET)))
return //in their pockets please
succeed_objective()
/datum/traitor_objective/target_player/assassinate/calling_card/ungenerate_objective()
. = ..() //unsets kill target
if(card)
UnregisterSignal(card, COMSIG_ITEM_EQUIPPED)
card = null
/datum/traitor_objective/target_player/assassinate/calling_card/target_deleted()
//you cannot plant anything on someone who is gone gone, so even if this happens after you're still liable to fail
fail_objective(penalty_cost = telecrystal_penalty)
/datum/traitor_objective/target_player/assassinate/New(datum/uplink_handler/handler)
. = ..()
AddComponent(/datum/component/traitor_objective_limit_per_time, \
/datum/traitor_objective/target_player, \
time_period = objective_period, \
maximum_objectives = maximum_objectives_in_period \
)
/datum/traitor_objective/target_player/assassinate/generate_objective(datum/mind/generating_for, list/possible_duplicates)
var/list/already_targeting = list() //List of minds we're already targeting. The possible_duplicates is a list of objectives, so let's not mix things
for(var/datum/objective/task as anything in handler.primary_objectives)
if(!istype(task.target, /datum/mind))
continue
already_targeting += task.target //Removing primary objective kill targets from the list
var/parent_type = type2parent(type)
//don't roll head of staff types if you haven't completed the normal version
if(heads_of_staff && !handler.get_completion_count(parent_type))
// Locked if they don't have any of the risky bug room objective completed
return FALSE
var/list/possible_targets = list()
var/try_target_late_joiners = FALSE
if(generating_for.late_joiner)
try_target_late_joiners = TRUE
for(var/datum/mind/possible_target as anything in get_crewmember_minds())
if(possible_target in already_targeting)
continue
var/target_area = get_area(possible_target.current)
if(possible_target == generating_for)
continue
if(!ishuman(possible_target.current))
continue
if(possible_target.current.stat == DEAD)
continue
var/datum/antagonist/traitor/traitor = possible_target.has_antag_datum(/datum/antagonist/traitor)
if(traitor && traitor.uplink_handler.telecrystals >= 0)
continue
if(!HAS_TRAIT(SSstation, STATION_TRAIT_LATE_ARRIVALS) && istype(target_area, /area/shuttle/arrival))
continue
//removes heads of staff from being targets from non heads of staff assassinations, and vice versa
if(heads_of_staff)
if(!(possible_target.assigned_role.job_flags & JOB_HEAD_OF_STAFF))
continue
else
if((possible_target.assigned_role.job_flags & JOB_HEAD_OF_STAFF))
continue
possible_targets += possible_target
for(var/datum/traitor_objective/target_player/objective as anything in possible_duplicates)
possible_targets -= objective.target
if(try_target_late_joiners)
var/list/all_possible_targets = possible_targets.Copy()
for(var/datum/mind/possible_target as anything in all_possible_targets)
if(!possible_target.late_joiner)
possible_targets -= possible_target
if(!possible_targets.len)
possible_targets = all_possible_targets
special_target_filter(possible_targets)
if(!possible_targets.len)
return FALSE //MISSION FAILED, WE'LL GET EM NEXT TIME
var/datum/mind/target_mind = pick(possible_targets)
set_target(target_mind.current)
replace_in_name("%TARGET%", target.real_name)
replace_in_name("%JOB TITLE%", target_mind.assigned_role.title)
RegisterSignal(target, COMSIG_LIVING_DEATH, PROC_REF(on_target_death))
return TRUE
/datum/traitor_objective/target_player/assassinate/ungenerate_objective()
UnregisterSignal(target, COMSIG_LIVING_DEATH)
set_target(null)
///proc for checking for special states that invalidate a target
/datum/traitor_objective/target_player/assassinate/proc/special_target_filter(list/possible_targets)
return
/datum/traitor_objective/target_player/assassinate/target_deleted()
if(objective_state == OBJECTIVE_STATE_INACTIVE)
//don't take an objective target of someone who is already obliterated
fail_objective()
return ..()
/datum/traitor_objective/target_player/assassinate/proc/on_target_death()
SIGNAL_HANDLER
if(objective_state == OBJECTIVE_STATE_INACTIVE)
//don't take an objective target of someone who is already dead
fail_objective()
/obj/item/paper/calling_card
name = "calling card"
icon_state = "syndicate_calling_card"
color = "#ff5050"
show_written_words = FALSE
default_raw_text = {"
<b>**Death to Nanotrasen.**</b><br><br>
Only through the inviolable cooperation of corporations known as The Syndicate, can Nanotrasen and its autocratic tyrants be silenced.
The outcries of Nanotrasen's employees are squelched by the suffocating iron grip of their leaders. If you read this, and understand
why we fight, then you need only to look where Nanotrasen doesn't want you to find us to join our cause. Any number of our companies
may be fighting with your interests in mind.<br><br>
<b>SELF:</b> They fight for the protection and freedom of silicon life all across the galaxy.<br><br>
<b>Tiger Cooperative:</b> They fight for religious freedom and their righteous concoctions.<br><br>
<b>Waffle Corporation:</b> They fight for the return of healthy corporate competition, snuffed out by Nanotrasen's monopoly.<br><br>
<b>Animal Rights Consortium:</b> They fight for nature and the right for all biological life to exist.
"}
@@ -1,129 +0,0 @@
/datum/traitor_objective_category/demoralise
name = "Demoralise Crew"
objectives = list(
/datum/traitor_objective/target_player/assault = 1,
/datum/traitor_objective/destroy_item/demoralise = 1,
)
weight = OBJECTIVE_WEIGHT_UNLIKELY
/datum/traitor_objective/target_player/assault
name = "Assault %TARGET% the %JOB TITLE%"
description = "%TARGET% has been identified as a potential future agent. \
Pick a fight and give them a good beating. \
%COUNT% hits should reduce their morale and have them questioning their loyalties. \
Try not to kill them just yet, we may want to recruit them in the future."
abstract_type = /datum/traitor_objective/target_player
duplicate_type = /datum/traitor_objective/target_player
progression_minimum = 0 MINUTES
progression_maximum = 30 MINUTES
progression_reward = list(4 MINUTES, 8 MINUTES)
telecrystal_reward = list(0, 1)
/// Min attacks required to pass the objective. Picked at random between this and max.
var/min_attacks_required = 2
/// Max attacks required to pass the objective. Picked at random between this and min.
var/max_attacks_required = 5
/// The random number picked for the number of required attacks to pass this objective.
var/attacks_required = 0
/// Total number of successful attacks recorded.
var/attacks_inflicted = 0
/datum/traitor_objective/target_player/assault/on_objective_taken(mob/user)
. = ..()
target.AddElement(/datum/element/relay_attackers)
RegisterSignal(target, COMSIG_ATOM_WAS_ATTACKED, PROC_REF(on_attacked))
/datum/traitor_objective/target_player/assault/proc/on_attacked(mob/source, mob/living/attacker, attack_flags)
SIGNAL_HANDLER
// Only care about attacks from the objective's owner.
if(attacker != handler.owner.current)
return
// We want some sort of damaging attack to trigger this, rather than shoves and non-lethals.
if(!(attack_flags & ATTACKER_DAMAGING_ATTACK))
return
attacks_inflicted++
if(attacks_inflicted == attacks_required)
succeed_objective()
/datum/traitor_objective/target_player/assault/ungenerate_objective()
UnregisterSignal(target, COMSIG_ATOM_WAS_ATTACKED)
UnregisterSignal(target, COMSIG_LIVING_DEATH)
set_target(null)
/datum/traitor_objective/target_player/assault/generate_objective(datum/mind/generating_for, list/possible_duplicates)
var/list/already_targeting = list() //List of minds we're already targeting. The possible_duplicates is a list of objectives, so let's not mix things
for(var/datum/objective/task as anything in handler.primary_objectives)
if(!istype(task.target, /datum/mind))
continue
already_targeting += task.target //Removing primary objective kill targets from the list
var/list/possible_targets = list()
for(var/datum/mind/possible_target as anything in get_crewmember_minds())
if(possible_target in already_targeting)
continue
if(possible_target == generating_for)
continue
if(!ishuman(possible_target.current))
continue
if(possible_target.current.stat == DEAD)
continue
if(possible_target.has_antag_datum(/datum/antagonist/traitor))
continue
possible_targets += possible_target
for(var/datum/traitor_objective/target_player/objective as anything in possible_duplicates)
possible_targets -= objective.target?.mind
if(generating_for.late_joiner)
var/list/all_possible_targets = possible_targets.Copy()
for(var/datum/mind/possible_target as anything in all_possible_targets)
if(!possible_target.late_joiner)
possible_targets -= possible_target
if(!possible_targets.len)
possible_targets = all_possible_targets
if(!possible_targets.len)
return FALSE
var/datum/mind/target_mind = pick(possible_targets)
set_target(target_mind.current)
replace_in_name("%TARGET%", target.real_name)
replace_in_name("%JOB TITLE%", target_mind.assigned_role.title)
attacks_required = rand(min_attacks_required, max_attacks_required)
replace_in_name("%COUNT%", attacks_required)
RegisterSignal(target, COMSIG_LIVING_DEATH, PROC_REF(on_target_death))
return TRUE
/datum/traitor_objective/target_player/assault/generate_ui_buttons(mob/user)
var/list/buttons = list()
if(attacks_required > attacks_inflicted)
buttons += add_ui_button("[attacks_required - attacks_inflicted]", "This tells you how many more times you have to attack the target player to succeed.", "hand-rock-o", "none")
return buttons
/datum/traitor_objective/target_player/assault/target_deleted()
//don't take an objective target of someone who is already obliterated
fail_objective()
return ..()
/datum/traitor_objective/target_player/assault/proc/on_target_death()
SIGNAL_HANDLER
//don't take an objective target of someone who is already dead
fail_objective()
@@ -1,153 +0,0 @@
/datum/traitor_objective_category/destroy_heirloom
name = "Destroy Heirloom"
objectives = list(
list(
// There's about 16 jobs in common, so assistant has a 1/21 chance of getting chosen.
/datum/traitor_objective/destroy_heirloom/common = 20,
/datum/traitor_objective/destroy_heirloom/common/assistant = 1,
) = 4,
/datum/traitor_objective/destroy_heirloom/uncommon = 3,
/datum/traitor_objective/destroy_heirloom/rare = 2,
/datum/traitor_objective/destroy_heirloom/captain = 1
)
/datum/traitor_objective/destroy_heirloom
name = "Destroy %ITEM%, the family heirloom that belongs to %TARGET% the %JOB TITLE%"
description = "%TARGET% has been on our shitlist for a while and we want to show them we mean business. Find their %ITEM% and destroy it."
abstract_type = /datum/traitor_objective/destroy_heirloom
/// The jobs that this objective is targeting.
var/list/target_jobs
/// the item we need to destroy
var/obj/item/target_item
/// the owner of the item we need to destroy
var/datum/mind/target_mind
// The code below is for limiting how often you can get this objective. You will get this objective at a maximum of maximum_objectives_in_period every objective_period
/// The objective period at which we consider if it is an 'objective'. Set to 0 to accept all objectives.
var/objective_period = 10 MINUTES
/// The maximum number of objectives that can be taken in this period.
var/maximum_objectives_in_period = 2
duplicate_type = /datum/traitor_objective/destroy_heirloom
/datum/traitor_objective/destroy_heirloom/common
/// 30 minutes in, syndicate won't care about common heirlooms anymore
progression_minimum = 0 MINUTES
progression_maximum = 30 MINUTES
progression_reward = list(8 MINUTES, 12 MINUTES)
telecrystal_reward = list(1, 2)
target_jobs = list(
// Medical
/datum/job/doctor,
/datum/job/paramedic,
/datum/job/psychologist,
/datum/job/chemist,
/datum/job/coroner,
// Service
/datum/job/clown,
/datum/job/botanist,
/datum/job/janitor,
/datum/job/mime,
/datum/job/lawyer,
// Cargo
/datum/job/cargo_technician,
// Science
/datum/job/geneticist,
/datum/job/scientist,
/datum/job/roboticist,
// Engineering
/datum/job/station_engineer,
/datum/job/atmospheric_technician,
)
/// This is only for assistants, because the syndies are a lot less likely to give a shit about what an assistant does, so they're a lot less likely to appear
/datum/traitor_objective/destroy_heirloom/common/assistant
target_jobs = list(
/datum/job/assistant
)
/datum/traitor_objective/destroy_heirloom/uncommon
/// 45 minutes in, syndicate won't care about uncommon heirlooms anymore
progression_minimum = 0 MINUTES
progression_maximum = 45 MINUTES
progression_reward = list(8 MINUTES, 12 MINUTES)
telecrystal_reward = list(1, 2)
target_jobs = list(
// Cargo
/datum/job/bitrunner,
/datum/job/shaft_miner,
// Service
/datum/job/chaplain,
/datum/job/bartender,
/datum/job/cook,
/datum/job/curator,
)
/datum/traitor_objective/destroy_heirloom/rare
progression_minimum = 15 MINUTES
/// 60 minutes in, syndicate won't care about rare heirlooms anymore
progression_maximum = 60 MINUTES
progression_reward = list(10 MINUTES, 14 MINUTES)
telecrystal_reward = list(2, 3)
target_jobs = list(
// Security
/datum/job/security_officer,
/datum/job/warden,
/datum/job/detective,
// Heads of staff
/datum/job/head_of_personnel,
/datum/job/chief_medical_officer,
/datum/job/research_director,
/datum/job/quartermaster,
/datum/job/chief_engineer,
)
/datum/traitor_objective/destroy_heirloom/captain
progression_minimum = 30 MINUTES
progression_reward = list(10 MINUTES, 14 MINUTES)
telecrystal_reward = 4
target_jobs = list(
/datum/job/head_of_security,
/datum/job/captain
)
/datum/traitor_objective/destroy_heirloom/New(datum/uplink_handler/handler)
. = ..()
AddComponent(/datum/component/traitor_objective_limit_per_time, \
/datum/traitor_objective/destroy_heirloom, \
time_period = objective_period, \
maximum_objectives = maximum_objectives_in_period \
)
/datum/traitor_objective/destroy_heirloom/generate_objective(datum/mind/generating_for, list/possible_duplicates)
var/list/possible_targets = list()
for(var/datum/mind/possible_target as anything in get_crewmember_minds())
if(possible_target == generating_for)
continue
if(!ishuman(possible_target.current))
continue
var/datum/quirk/item_quirk/family_heirloom/quirk = locate() in possible_target.current.quirks
if(!quirk || !quirk.heirloom.resolve())
continue
if(!(possible_target.assigned_role.type in target_jobs))
continue
possible_targets += possible_target
for(var/datum/traitor_objective/destroy_heirloom/objective as anything in possible_duplicates)
possible_targets -= objective.target_mind
if(!length(possible_targets))
return FALSE
target_mind = pick(possible_targets)
AddComponent(/datum/component/traitor_objective_register, target_mind.current, fail_signals = list(COMSIG_QDELETING))
var/datum/quirk/item_quirk/family_heirloom/quirk = locate() in target_mind.current.quirks
target_item = quirk.heirloom.resolve()
AddComponent(/datum/component/traitor_objective_register, target_item, succeed_signals = list(COMSIG_QDELETING))
replace_in_name("%TARGET%", target_mind.name)
replace_in_name("%JOB TITLE%", target_mind.assigned_role.title)
replace_in_name("%ITEM%", target_item.name)
return TRUE
/datum/traitor_objective/destroy_heirloom/ungenerate_objective()
target_item = null
target_mind = null
@@ -1,112 +0,0 @@
/datum/traitor_objective/destroy_item
name = "Steal %ITEM% and destroy it"
description = "Find %ITEM% and destroy it using any means necessary. We can't allow the crew to have %ITEM% as it conflicts with our interests. %METHOD%"
var/list/possible_items = list()
/// The current target item that we are stealing.
var/datum/objective_item/steal/target_item
/// Any special equipment that may be needed
var/list/special_equipment
/// Items that are currently tracked and will succeed this objective when destroyed.
var/list/tracked_items = list()
abstract_type = /datum/traitor_objective/destroy_item
/datum/traitor_objective/destroy_item/low_risk
progression_minimum = 10 MINUTES
progression_maximum = 35 MINUTES
progression_reward = list(5 MINUTES, 10 MINUTES)
telecrystal_reward = 1
possible_items = list(
/datum/objective_item/steal/traitor/bartender_shotgun,
/datum/objective_item/steal/traitor/fireaxe,
/datum/objective_item/steal/traitor/nullrod,
/datum/objective_item/steal/traitor/big_crowbar,
)
/datum/traitor_objective/destroy_item/very_risky
progression_minimum = 40 MINUTES
progression_reward = 15 MINUTES
telecrystal_reward = list(6, 9)
possible_items = list(
/datum/objective_item/steal/blackbox,
)
/// Super early-game destroy objective intended to be items easily tided that the crew tends to value.
/datum/traitor_objective/destroy_item/demoralise
description = "Find %ITEM% and destroy it using any means necessary. \
We believe this luxury item is important for crew morale. \
Destruction of this item will help our recruitment efforts."
progression_minimum = 0 MINUTES
progression_maximum = 10 MINUTES
progression_reward = list(4 MINUTES, 8 MINUTES)
telecrystal_reward = list(0, 1)
possible_items = list(
/datum/objective_item/steal/traitor/rpd,
/datum/objective_item/steal/traitor/space_law,
/datum/objective_item/steal/traitor/granted_stamp,
/datum/objective_item/steal/traitor/denied_stamp,
/datum/objective_item/steal/traitor/lizard_plush,
/datum/objective_item/steal/traitor/moth_plush,
/datum/objective_item/steal/traitor/insuls,
)
/datum/traitor_objective/destroy_item/generate_objective(datum/mind/generating_for, list/possible_duplicates)
for(var/datum/traitor_objective/destroy_item/objective as anything in possible_duplicates)
possible_items -= objective.target_item.type
while(length(possible_items))
var/datum/objective_item/steal/target = pick_n_take(possible_items)
target = new target()
if(!target.valid_objective_for(list(generating_for), require_owner = TRUE))
qdel(target)
continue
target_item = target
break
if(!target_item)
return FALSE
if(target_item.exists_on_map)
var/list/items = GLOB.steal_item_handler.objectives_by_path[target_item.targetitem]
for(var/obj/item/item as anything in items)
AddComponent(/datum/component/traitor_objective_register, item, succeed_signals = list(COMSIG_QDELETING))
tracked_items += item
if(length(target_item.special_equipment))
special_equipment = target_item.special_equipment
replace_in_name("%ITEM%", target_item.name)
if(target_item.destruction_method != null)
replace_in_name("%METHOD%", target_item.destruction_method)
else
replace_in_name("%METHOD%", "This item can be destroyed normally, such as by using a recycler, found in disposals.")
AddComponent(/datum/component/traitor_objective_mind_tracker, generating_for, \
signals = list(COMSIG_MOB_EQUIPPED_ITEM = PROC_REF(on_item_pickup)))
return TRUE
/datum/traitor_objective/destroy_item/generate_ui_buttons(mob/user)
var/list/buttons = list()
if(special_equipment)
buttons += add_ui_button("", "Pressing this will summon any extra special equipment you may need for the mission.", "tools", "summon_gear")
return buttons
/datum/traitor_objective/destroy_item/ui_perform_action(mob/living/user, action)
. = ..()
switch(action)
if("summon_gear")
if(!special_equipment)
return
for(var/item in special_equipment)
var/obj/item/new_item = new item(user.drop_location())
user.put_in_hands(new_item)
user.balloon_alert(user, "the equipment materializes in your hand")
special_equipment = null
/datum/traitor_objective/destroy_item/proc/on_item_pickup(datum/source, obj/item/item, slot)
SIGNAL_HANDLER
if(istype(item, target_item.targetitem) && !(item in tracked_items))
AddComponent(/datum/component/traitor_objective_register, item, succeed_signals = list(COMSIG_QDELETING))
tracked_items += item
/datum/traitor_objective/destroy_item/ungenerate_objective()
tracked_items.Cut()
return ..()
@@ -1,240 +0,0 @@
/datum/traitor_objective_category/eyesnatching
name = "Eyesnatching"
objectives = list(
/datum/traitor_objective/target_player/eyesnatching = 1,
/datum/traitor_objective/target_player/eyesnatching/heads = 1,
)
weight = OBJECTIVE_WEIGHT_UNLIKELY
/datum/traitor_objective/target_player/eyesnatching
name = "Steal the eyes of %TARGET% the %JOB TITLE%"
description = "%TARGET% messed with the wrong people. Steal their eyes to teach them a lesson. You will be provided an experimental eyesnatcher device to aid you in your mission."
progression_minimum = 10 MINUTES
progression_reward = list(4 MINUTES, 8 MINUTES)
telecrystal_reward = list(1, 2)
/// If we're targeting heads of staff or not
var/heads_of_staff = FALSE
/// Have we already spawned an eyesnatcher
var/spawned_eyesnatcher = FALSE
duplicate_type = /datum/traitor_objective/target_player
/datum/traitor_objective/target_player/eyesnatching/supported_configuration_changes()
. = ..()
. += NAMEOF(src, objective_period)
. += NAMEOF(src, maximum_objectives_in_period)
/datum/traitor_objective/target_player/eyesnatching/New(datum/uplink_handler/handler)
. = ..()
AddComponent(/datum/component/traitor_objective_limit_per_time, \
/datum/traitor_objective/target_player, \
time_period = objective_period, \
maximum_objectives = maximum_objectives_in_period \
)
/datum/traitor_objective/target_player/eyesnatching/heads
progression_reward = list(6 MINUTES, 12 MINUTES)
telecrystal_reward = list(2, 3)
heads_of_staff = TRUE
/datum/traitor_objective/target_player/eyesnatching/generate_objective(datum/mind/generating_for, list/possible_duplicates)
var/list/already_targeting = list() //List of minds we're already targeting. The possible_duplicates is a list of objectives, so let's not mix things
for(var/datum/objective/task as anything in handler.primary_objectives)
if(!istype(task.target, /datum/mind))
continue
already_targeting += task.target //Removing primary objective kill targets from the list
var/list/possible_targets = list()
var/try_target_late_joiners = FALSE
if(generating_for.late_joiner)
try_target_late_joiners = TRUE
for(var/datum/mind/possible_target as anything in get_crewmember_minds())
if(possible_target == generating_for)
continue
if(possible_target in already_targeting)
continue
if(!ishuman(possible_target.current))
continue
if(possible_target.current.stat == DEAD)
continue
if(possible_target.has_antag_datum(/datum/antagonist/traitor))
continue
if(!possible_target.assigned_role)
continue
if(heads_of_staff)
if(!(possible_target.assigned_role.job_flags & JOB_HEAD_OF_STAFF))
continue
else
if(possible_target.assigned_role.job_flags & JOB_HEAD_OF_STAFF)
continue
var/mob/living/carbon/human/targets_current = possible_target.current
if(!targets_current.get_organ_by_type(/obj/item/organ/eyes))
continue
possible_targets += possible_target
for(var/datum/traitor_objective/target_player/objective as anything in possible_duplicates)
possible_targets -= objective.target?.mind
if(try_target_late_joiners)
var/list/all_possible_targets = possible_targets.Copy()
for(var/datum/mind/possible_target as anything in all_possible_targets)
if(!possible_target.late_joiner)
possible_targets -= possible_target
if(!possible_targets.len)
possible_targets = all_possible_targets
if(!possible_targets.len)
return FALSE //MISSION FAILED, WE'LL GET EM NEXT TIME
var/datum/mind/target_mind = pick(possible_targets)
set_target(target_mind.current)
replace_in_name("%TARGET%", target_mind.name)
replace_in_name("%JOB TITLE%", target_mind.assigned_role.title)
RegisterSignal(target, COMSIG_CARBON_LOSE_ORGAN, PROC_REF(check_eye_removal))
AddComponent(/datum/component/traitor_objective_register, target, fail_signals = list(COMSIG_QDELETING))
return TRUE
/datum/traitor_objective/target_player/eyesnatching/proc/check_eye_removal(datum/source, obj/item/organ/eyes/removed)
SIGNAL_HANDLER
if(!istype(removed))
return
succeed_objective()
/datum/traitor_objective/target_player/eyesnatching/generate_ui_buttons(mob/user)
var/list/buttons = list()
if(!spawned_eyesnatcher)
buttons += add_ui_button("", "Pressing this will materialize an eyesnatcher, which can be used on incapacitaded or restrained targets to forcefully remove their eyes.", "syringe", "eyesnatcher")
return buttons
/datum/traitor_objective/target_player/eyesnatching/ui_perform_action(mob/living/user, action)
. = ..()
switch(action)
if("eyesnatcher")
if(spawned_eyesnatcher)
return
spawned_eyesnatcher = TRUE
var/obj/item/eyesnatcher/eyesnatcher = new(user.drop_location())
user.put_in_hands(eyesnatcher)
eyesnatcher.balloon_alert(user, "the snatcher materializes in your hand")
/obj/item/eyesnatcher
name = "portable eyeball extractor"
desc = "An overly complicated device that can pierce target's skull and extract their eyeballs if enough brute force is applied."
icon = 'icons/obj/medical/surgery_tools.dmi'
icon_state = "eyesnatcher"
base_icon_state = "eyesnatcher"
inhand_icon_state = "hypo"
lefthand_file = 'icons/mob/inhands/equipment/medical_lefthand.dmi'
righthand_file = 'icons/mob/inhands/equipment/medical_righthand.dmi'
throwforce = 0
w_class = WEIGHT_CLASS_SMALL
throw_speed = 3
throw_range = 5
///Whether it's been used to steal a pair of eyes already.
var/used = FALSE
/obj/item/eyesnatcher/update_icon_state()
. = ..()
icon_state = "[base_icon_state][used ? "-used" : ""]"
/obj/item/eyesnatcher/attack(mob/living/carbon/human/target, mob/living/user, params)
if(used || !istype(target) || !target.Adjacent(user)) //Works only once, no TK use
return ..()
var/obj/item/organ/eyes/eyeballies = target.get_organ_slot(ORGAN_SLOT_EYES)
var/obj/item/bodypart/head/head = target.get_bodypart(BODY_ZONE_HEAD)
if(!head || !eyeballies || target.is_eyes_covered())
return ..()
var/eye_snatch_enthusiasm = 5 SECONDS
if(HAS_MIND_TRAIT(user, TRAIT_MORBID))
eye_snatch_enthusiasm *= 0.7
user.do_attack_animation(target, used_item = src)
target.visible_message(
span_warning("[user] presses [src] against [target]'s skull!"),
span_userdanger("[user] presses [src] against your skull!"))
if(!do_after(user, eye_snatch_enthusiasm, target = target, extra_checks = CALLBACK(src, PROC_REF(eyeballs_exist), eyeballies, head, target)))
return
to_chat(target, span_userdanger("You feel something forcing its way into your skull!"))
balloon_alert(user, "applying pressure...")
if(!do_after(user, eye_snatch_enthusiasm, target = target, extra_checks = CALLBACK(src, PROC_REF(eyeballs_exist), eyeballies, head, target)))
return
var/min_wound = head.get_wound_threshold_of_wound_type(WOUND_BLUNT, WOUND_SEVERITY_SEVERE, return_value_if_no_wound = 30, wound_source = src)
var/max_wound = head.get_wound_threshold_of_wound_type(WOUND_BLUNT, WOUND_SEVERITY_CRITICAL, return_value_if_no_wound = 50, wound_source = src)
target.apply_damage(20, BRUTE, BODY_ZONE_HEAD, wound_bonus = rand(min_wound, max_wound + 10), attacking_item = src)
target.visible_message(
span_danger("[src] pierces through [target]'s skull, horribly mutilating their eyes!"),
span_userdanger("Something penetrates your skull, horribly mutilating your eyes! Holy fuck!"),
span_hear("You hear a sickening sound of metal piercing flesh!")
)
eyeballies.apply_organ_damage(eyeballies.maxHealth)
target.emote("scream")
playsound(target, 'sound/effects/wounds/crackandbleed.ogg', 100)
log_combat(user, target, "cracked the skull of (eye snatching)", src)
if(!do_after(user, eye_snatch_enthusiasm, target = target, extra_checks = CALLBACK(src, PROC_REF(eyeballs_exist), eyeballies, head, target)))
return
if(!target.is_blind())
to_chat(target, span_userdanger("You suddenly go blind!"))
if(prob(1))
to_chat(target, span_notice("At least you got a new pirate-y look out of it..."))
var/obj/item/clothing/glasses/eyepatch/new_patch = new(target.loc)
target.equip_to_slot_if_possible(new_patch, ITEM_SLOT_EYES, disable_warning = TRUE)
to_chat(user, span_notice("You successfully extract [target]'s eyeballs."))
playsound(target, 'sound/items/handling/surgery/retractor2.ogg', 100, TRUE)
playsound(target, 'sound/effects/pop.ogg', 100, TRAIT_MUTE)
eyeballies.Remove(target)
eyeballies.forceMove(get_turf(target))
notify_ghosts(
"[target] has just had their eyes snatched!",
source = target,
header = "Ouch!",
)
target.emote("scream")
if(prob(20))
target.emote("cry")
used = TRUE
update_appearance(UPDATE_ICON)
/obj/item/eyesnatcher/examine(mob/user)
. = ..()
if(used)
. += span_notice("It has been used up.")
/obj/item/eyesnatcher/proc/eyeballs_exist(obj/item/organ/eyes/eyeballies, obj/item/bodypart/head/head, mob/living/carbon/human/target)
if(!eyeballies || QDELETED(eyeballies))
return FALSE
if(!head || QDELETED(head))
return FALSE
if(eyeballies.owner != target)
return FALSE
var/obj/item/organ/eyes/eyes = target.get_organ_slot(ORGAN_SLOT_EYES)
//got different eyes or doesn't own the head... somehow
if(head.owner != target || eyes != eyeballies)
return FALSE
return TRUE
@@ -1,43 +0,0 @@
/datum/traitor_objective/ultimate/battle_royale
name = "Implant crewmembers with a subtle implant, then make them fight to the death on pay-per-view TV."
description = "Go to %AREA%, and receive the Royale Broadcast Kit. \
Use the contained implant on station personnel to subtly implant them with a micro-explosive. \
Once you have at least six contestants, use the contained remote to start a timer and begin broadcasting live. \
If more than one contestant remains alive after ten minutes, all of the implants will detonate."
///Area type the objective owner must be in to receive the tools.
var/area/kit_spawn_area
///Whether the kit was sent already.
var/equipped = FALSE
/datum/traitor_objective/ultimate/battle_royale/generate_objective(datum/mind/generating_for, list/possible_duplicates)
var/list/possible_areas = GLOB.the_station_areas.Copy()
for(var/area/possible_area as anything in possible_areas)
if(ispath(possible_area, /area/station/hallway) || ispath(possible_area, /area/station/security))
possible_areas -= possible_area
if(length(possible_areas) == 0)
return FALSE
kit_spawn_area = pick(possible_areas)
replace_in_name("%AREA%", initial(kit_spawn_area.name))
return TRUE
/datum/traitor_objective/ultimate/battle_royale/generate_ui_buttons(mob/user)
var/list/buttons = list()
if(!equipped)
buttons += add_ui_button("", "Pressing this will call down a pod with the Royale Broadcast kit.", "biohazard", "deliver_kit")
return buttons
/datum/traitor_objective/ultimate/battle_royale/ui_perform_action(mob/living/user, action)
. = ..()
if(action != "deliver_kit" || equipped)
return
var/area/delivery_area = get_area(user)
if(delivery_area.type != kit_spawn_area)
to_chat(user, span_warning("You must be in [initial(kit_spawn_area.name)] to receive the Royale Broadcast kit."))
return
equipped = TRUE
podspawn(list(
"target" = get_turf(user),
"style" = /datum/pod_style/syndicate,
"spawn" = /obj/item/storage/box/syndie_kit/battle_royale,
))
@@ -1,49 +0,0 @@
/datum/traitor_objective/ultimate/battlecruiser
name = "Reveal Station Coordinates to nearby Syndicate Battlecruiser"
description = "Use a special upload card on a communications console to send the coordinates \
of the station to a nearby Battlecruiser. You may want to make your syndicate status known to \
the battlecruiser crew when they arrive - their goal will be to destroy the station."
/// Checks whether we have sent the card to the traitor yet.
var/sent_accesscard = FALSE
/// Battlecruiser team that we get assigned to
var/datum/team/battlecruiser/team
/datum/traitor_objective/ultimate/battlecruiser/generate_objective(datum/mind/generating_for, list/possible_duplicates)
// There's no empty space to load a battlecruiser in...
if(SSmapping.is_planetary())
return FALSE
return TRUE
/datum/traitor_objective/ultimate/battlecruiser/on_objective_taken(mob/user)
. = ..()
team = new()
var/obj/machinery/nuclearbomb/selfdestruct/nuke = locate() in SSmachines.get_machines_by_type(/obj/machinery/nuclearbomb/selfdestruct)
if(nuke.r_code == NUKE_CODE_UNSET)
nuke.r_code = random_nukecode()
team.nuke = nuke
team.update_objectives()
handler.owner.add_antag_datum(/datum/antagonist/battlecruiser/ally, team)
/datum/traitor_objective/ultimate/battlecruiser/generate_ui_buttons(mob/user)
var/list/buttons = list()
if(!sent_accesscard)
buttons += add_ui_button("", "Pressing this will materialize an upload card, which you can use on a communication console to contact the fleet.", "phone", "card")
return buttons
/datum/traitor_objective/ultimate/battlecruiser/ui_perform_action(mob/living/user, action)
. = ..()
switch(action)
if("card")
if(sent_accesscard)
return
sent_accesscard = TRUE
var/obj/item/card/emag/battlecruiser/emag_card = new()
emag_card.team = team
podspawn(list(
"target" = get_turf(user),
"style" = /datum/pod_style/syndicate,
"spawn" = emag_card,
))
@@ -1,43 +0,0 @@
/datum/traitor_objective_category/final_objective
name = "Final Objective"
objectives = list(
/datum/traitor_objective/ultimate/battlecruiser = 1,
/datum/traitor_objective/ultimate/battle_royale = 1,
/datum/traitor_objective/ultimate/dark_matteor = 1,
/datum/traitor_objective/ultimate/infect_ai = 1,
/datum/traitor_objective/ultimate/romerol = 1,
/datum/traitor_objective/ultimate/supermatter_cascade = 1,
/datum/traitor_objective/ultimate/no_escape = 1,
)
weight = 100
/datum/traitor_objective/ultimate
abstract_type = /datum/traitor_objective/ultimate
progression_minimum = 140 MINUTES
needs_reward = FALSE
var/progression_points_in_objectives = 20 MINUTES
/// Determines if this final objective can be taken. Should be put into every final objective's generate function.
/datum/traitor_objective/ultimate/can_generate_objective(generating_for, list/possible_duplicates)
if(handler.get_completion_progression(/datum/traitor_objective) < progression_points_in_objectives)
return FALSE
if(SStraitor.get_taken_count(type) > 0) // Prevents multiple people from ever getting the same final objective.
return FALSE
if(length(possible_duplicates) > 0)
return FALSE
return TRUE
/datum/traitor_objective/ultimate/on_objective_taken(mob/user)
. = ..()
handler.maximum_potential_objectives = 0
for(var/datum/traitor_objective/objective as anything in handler.potential_objectives)
if(objective == src)
continue
objective.fail_objective()
user.playsound_local(get_turf(user), 'sound/music/antag/traitor/final_objective.ogg', vol = 100, vary = FALSE, channel = CHANNEL_TRAITOR)
handler.final_objective = name
/datum/traitor_objective/ultimate/uplink_ui_data(mob/user)
. = ..()
.["final_objective"] = TRUE
@@ -1,56 +0,0 @@
/datum/traitor_objective/ultimate/infect_ai
name = "Infect the station AI with an experimental virus."
description = "Infect the station AI with an experimental virus. Go to %AREA% to receive an infected law upload board \
and use it on the AI core or a law upload console."
///area type the objective owner must be in to receive the law upload module
var/area/board_area_pickup
///checker on whether we have sent the law upload module
var/sent_board = FALSE
/datum/traitor_objective/ultimate/infect_ai/can_generate_objective(generating_for, list/possible_duplicates)
. = ..()
if(!.)
return FALSE
for(var/mob/living/silicon/ai/ai in GLOB.ai_list)
if(ai.stat == DEAD || ai.mind?.has_antag_datum(/datum/antagonist/malf_ai) || !is_station_level(ai.z))
continue
return TRUE
return FALSE
/datum/traitor_objective/ultimate/infect_ai/generate_objective(datum/mind/generating_for, list/possible_duplicates)
var/list/possible_areas = GLOB.the_station_areas.Copy()
for(var/area/possible_area as anything in possible_areas)
//remove areas too close to the destination, too obvious for our poor shmuck, or just unfair
if(istype(possible_area, /area/station/hallway) || istype(possible_area, /area/station/security))
possible_areas -= possible_area
if(!length(possible_areas))
return FALSE
board_area_pickup = pick(possible_areas)
replace_in_name("%AREA%", initial(board_area_pickup.name))
return TRUE
/datum/traitor_objective/ultimate/infect_ai/generate_ui_buttons(mob/user)
var/list/buttons = list()
if(!sent_board)
buttons += add_ui_button("", "Pressing this will call down a pod with an infected law upload board.", "wifi", "upload_board")
return buttons
/datum/traitor_objective/ultimate/infect_ai/ui_perform_action(mob/living/user, action)
. = ..()
switch(action)
if("upload_board")
if(sent_board)
return
var/area/delivery_area = get_area(user)
if(delivery_area.type != board_area_pickup)
to_chat(user, span_warning("You must be in [initial(board_area_pickup.name)] to receive the infected law upload board."))
return
sent_board = TRUE
podspawn(list(
"target" = get_turf(user),
"style" = /datum/pod_style/syndicate,
"spawn" = /obj/item/ai_module/malf,
))
@@ -1,48 +0,0 @@
/datum/traitor_objective/ultimate/no_escape
name = "Attach a beacon to the escape shuttle that will attract a singularity to consume everything."
description = "Go to %AREA%, and receive the smuggled beacon. Set up the beacon anywhere on the shuttle, \
and charge it using an inducer then, IT COMES. Warning: The singularity will consume all in it's path, you included."
///area type the objective owner must be in to receive the satellites
var/area/beacon_spawn_area_type
///checker on whether we have sent the beacon yet
var/sent_beacon = FALSE
/datum/traitor_objective/ultimate/no_escape/generate_objective(datum/mind/generating_for, list/possible_duplicates)
var/list/possible_areas = GLOB.the_station_areas.Copy()
for(var/area/possible_area as anything in possible_areas)
if(!ispath(possible_area, /area/station/maintenance/solars) && !ispath(possible_area, /area/station/solars))
possible_areas -= possible_area
if(length(possible_areas) == 0)
return FALSE
beacon_spawn_area_type = pick(possible_areas)
replace_in_name("%AREA%", initial(beacon_spawn_area_type.name))
return TRUE
/datum/traitor_objective/ultimate/no_escape/generate_ui_buttons(mob/user)
var/list/buttons = list()
if(!sent_beacon)
buttons += add_ui_button("", "Pressing this will call down a pod with the smuggled beacon.", "beacon", "beacon")
return buttons
/datum/traitor_objective/ultimate/no_escape/ui_perform_action(mob/living/user, action)
. = ..()
switch(action)
if("beacon")
if(sent_beacon)
return
var/area/delivery_area = get_area(user)
if(delivery_area.type != beacon_spawn_area_type)
to_chat(user, span_warning("You must be in [initial(beacon_spawn_area_type.name)] to receive the smuggled beacon."))
return
sent_beacon = TRUE
podspawn(list(
"target" = get_turf(user),
"style" = /datum/pod_style/syndicate,
"spawn" = list(
/obj/item/sbeacondrop/no_escape,
/obj/item/inducer/syndicate,
/obj/item/wrench
)
))
@@ -1,83 +0,0 @@
/datum/traitor_objective/ultimate/dark_matteor
name = "Summon a dark matter singularity to consume the station."
description = "Go to %AREA%, and receive the smuggled satellites + emag. Set up and emag the satellites, \
after enough have been recalibrated by the emag, IT COMES. Warning: The dark matter singularity will hunt all creatures, you included."
//this is a prototype so this progression is for all basic level kill objectives
///area type the objective owner must be in to receive the satellites
var/area/satellites_spawnarea_type
///checker on whether we have sent the satellites yet.
var/sent_satellites = FALSE
/datum/traitor_objective/ultimate/dark_matteor/can_generate_objective(generating_for, list/possible_duplicates)
. = ..()
if(!.)
return FALSE
if(SSmapping.is_planetary())
return FALSE //meteors can't spawn on planets
return TRUE
/datum/traitor_objective/ultimate/dark_matteor/generate_objective(datum/mind/generating_for, list/possible_duplicates)
var/list/possible_areas = GLOB.the_station_areas.Copy()
for(var/area/possible_area as anything in possible_areas)
if(!ispath(possible_area, /area/station/maintenance/solars) && !ispath(possible_area, /area/station/solars))
possible_areas -= possible_area
if(length(possible_areas) == 0)
return FALSE
satellites_spawnarea_type = pick(possible_areas)
replace_in_name("%AREA%", initial(satellites_spawnarea_type.name))
return TRUE
/datum/traitor_objective/ultimate/dark_matteor/generate_ui_buttons(mob/user)
var/list/buttons = list()
if(!sent_satellites)
buttons += add_ui_button("", "Pressing this will call down a pod with the smuggled satellites.", "satellite", "satellite")
return buttons
/datum/traitor_objective/ultimate/dark_matteor/ui_perform_action(mob/living/user, action)
. = ..()
switch(action)
if("satellite")
if(sent_satellites)
return
var/area/delivery_area = get_area(user)
if(delivery_area.type != satellites_spawnarea_type)
to_chat(user, span_warning("You must be in [initial(satellites_spawnarea_type.name)] to receive the smuggled satellites."))
return
sent_satellites = TRUE
podspawn(list(
"target" = get_turf(user),
"style" = /datum/pod_style/syndicate,
"spawn" = /obj/structure/closet/crate/engineering/smuggled_meteor_shields,
))
/obj/structure/closet/crate/engineering/smuggled_meteor_shields
/obj/structure/closet/crate/engineering/smuggled_meteor_shields/PopulateContents()
..()
for(var/i in 1 to 11)
new /obj/machinery/satellite/meteor_shield(src)
new /obj/item/card/emag/meteor_shield_recalibrator(src)
new /obj/item/paper/dark_matteor_summoning(src)
/obj/item/paper/dark_matteor_summoning
name = "notes - dark matter meteor summoning"
default_raw_text = {"
Summoning a dark matter meteor.<br>
<br>
<br>
Operative, this crate contains 10+1 spare meteor shield satellites stolen from NTs supply lines. Your mission is to
deploy them in space near the station and recalibrate them with the provided emag. Be careful: you need a 30 second
cooldown between each hack, and NT will detect your interference after seven recalibrations. That means you
have at least 5 minutes of work and 1 minute of resistance.<br>
<br>
This is a high-risk operation. Youll need backup, fortification, and determination. The reward?
A spectacular dark matter singularity that will wipe out the station.<br>
<br>
<b>**Death to Nanotrasen.**</b>
"}
/obj/item/card/emag/meteor_shield_recalibrator
name = "cryptographic satellite recalibrator"
desc = "It's a cryptographic sequencer that has been tuned to recalibrate meteor shields quicker and with less risk of frying them."
@@ -1,46 +0,0 @@
/datum/traitor_objective/ultimate/romerol
name = "Spread the experimental bioterror agent Romerol by calling a droppod down at %AREA%"
description = "Go to %AREA%, and receive the bioterror agent. Spread it to the crew, \
and watch then raise from the dead as mindless killing machines. Warning: The undead will attack you too."
//this is a prototype so this progression is for all basic level kill objectives
///area type the objective owner must be in to receive the romerol
var/area/romerol_spawnarea_type
///checker on whether we have sent the romerol yet.
var/sent_romerol = FALSE
/datum/traitor_objective/ultimate/romerol/generate_objective(datum/mind/generating_for, list/possible_duplicates)
var/list/possible_areas = GLOB.the_station_areas.Copy()
for(var/area/possible_area as anything in possible_areas)
//remove areas too close to the destination, too obvious for our poor shmuck, or just unfair
if(ispath(possible_area, /area/station/hallway) || ispath(possible_area, /area/station/security))
possible_areas -= possible_area
if(length(possible_areas) == 0)
return FALSE
romerol_spawnarea_type = pick(possible_areas)
replace_in_name("%AREA%", initial(romerol_spawnarea_type.name))
return TRUE
/datum/traitor_objective/ultimate/romerol/generate_ui_buttons(mob/user)
var/list/buttons = list()
if(!sent_romerol)
buttons += add_ui_button("", "Pressing this will call down a pod with the biohazard kit.", "biohazard", "romerol")
return buttons
/datum/traitor_objective/ultimate/romerol/ui_perform_action(mob/living/user, action)
. = ..()
switch(action)
if("romerol")
if(sent_romerol)
return
var/area/delivery_area = get_area(user)
if(delivery_area.type != romerol_spawnarea_type)
to_chat(user, span_warning("You must be in [initial(romerol_spawnarea_type.name)] to receive the bioterror agent."))
return
sent_romerol = TRUE
podspawn(list(
"target" = get_turf(user),
"style" = /datum/pod_style/syndicate,
"spawn" = /obj/item/storage/box/syndie_kit/romerol,
))
@@ -1,57 +0,0 @@
/datum/traitor_objective/ultimate/supermatter_cascade
name = "Destroy the station by causing a crystallizing resonance cascade"
description = "Destroy the station by causing a supermatter cascade. Go to %AREA% to retrieve the destabilizing crystal \
and use it on the supermatter."
///area type the objective owner must be in to receive the destabilizing crystal
var/area/dest_crystal_area_pickup
///checker on whether we have sent the crystal yet.
var/sent_crystal = FALSE
/datum/traitor_objective/ultimate/supermatter_cascade/can_generate_objective(generating_for, list/possible_duplicates)
. = ..()
if(!.)
return FALSE
if(isnull(GLOB.main_supermatter_engine))
return FALSE
var/obj/machinery/power/supermatter_crystal/engine/crystal = locate() in GLOB.main_supermatter_engine
if(!is_station_level(crystal.z) && !is_mining_level(crystal.z))
return FALSE
return TRUE
/datum/traitor_objective/ultimate/supermatter_cascade/generate_objective(datum/mind/generating_for, list/possible_duplicates)
var/list/possible_areas = GLOB.the_station_areas.Copy()
for(var/area/possible_area as anything in possible_areas)
//remove areas too close to the destination, too obvious for our poor shmuck, or just unfair
if(ispath(possible_area, /area/station/hallway) || ispath(possible_area, /area/station/security))
possible_areas -= possible_area
if(length(possible_areas) == 0)
return FALSE
dest_crystal_area_pickup = pick(possible_areas)
replace_in_name("%AREA%", initial(dest_crystal_area_pickup.name))
return TRUE
/datum/traitor_objective/ultimate/supermatter_cascade/generate_ui_buttons(mob/user)
var/list/buttons = list()
if(!sent_crystal)
buttons += add_ui_button("", "Pressing this will call down a pod with the supermatter cascade kit.", "biohazard", "destabilizing_crystal")
return buttons
/datum/traitor_objective/ultimate/supermatter_cascade/ui_perform_action(mob/living/user, action)
. = ..()
switch(action)
if("destabilizing_crystal")
if(sent_crystal)
return
var/area/delivery_area = get_area(user)
if(delivery_area.type != dest_crystal_area_pickup)
to_chat(user, span_warning("You must be in [initial(dest_crystal_area_pickup.name)] to receive the supermatter cascade kit."))
return
sent_crystal = TRUE
podspawn(list(
"target" = get_turf(user),
"style" = /datum/pod_style/syndicate,
"spawn" = /obj/item/destabilizing_crystal,
))
@@ -1,55 +0,0 @@
/datum/traitor_objective_category/hack_comm_console
name = "Hack Communication Console"
objectives = list(
/datum/traitor_objective/hack_comm_console = 1,
)
/datum/traitor_objective/hack_comm_console
name = "Hack a communication console to summon an unknown threat to the station"
description = "Right click on a communication console to begin the hacking process. Once started, the AI will know that you are hacking a communication console, so be ready to run or have yourself disguised to prevent being caught. This objective will invalidate itself if another traitor completes it first."
progression_minimum = 60 MINUTES
progression_reward = list(30 MINUTES, 40 MINUTES)
telecrystal_reward = list(7, 12)
var/progression_objectives_minimum = 20 MINUTES
/datum/traitor_objective/hack_comm_console/can_generate_objective(datum/mind/generating_for, list/possible_duplicates)
if(length(possible_duplicates) > 0)
return FALSE
if(SStraitor.get_taken_count(/datum/traitor_objective/hack_comm_console) > 0)
return FALSE
if(handler.get_completion_progression(/datum/traitor_objective) < progression_objectives_minimum)
return FALSE
return TRUE
/datum/traitor_objective/hack_comm_console/generate_objective(datum/mind/generating_for, list/possible_duplicates)
AddComponent(/datum/component/traitor_objective_mind_tracker, generating_for, \
signals = list(COMSIG_LIVING_UNARMED_ATTACK = PROC_REF(on_unarmed_attack)))
RegisterSignal(SSdcs, COMSIG_GLOB_TRAITOR_OBJECTIVE_COMPLETED, PROC_REF(on_global_obj_completed))
return TRUE
/datum/traitor_objective/hack_comm_console/ungenerate_objective()
UnregisterSignal(SSdcs, COMSIG_GLOB_TRAITOR_OBJECTIVE_COMPLETED)
/datum/traitor_objective/hack_comm_console/proc/on_global_obj_completed(datum/source, datum/traitor_objective/objective)
SIGNAL_HANDLER
if(istype(objective, /datum/traitor_objective/hack_comm_console))
fail_objective()
/datum/traitor_objective/hack_comm_console/proc/on_unarmed_attack(mob/user, obj/machinery/computer/communications/target, proximity_flag, modifiers)
SIGNAL_HANDLER
if(!proximity_flag)
return
if(!modifiers[RIGHT_CLICK])
return
if(!istype(target))
return
INVOKE_ASYNC(src, PROC_REF(begin_hack), user, target)
return COMPONENT_CANCEL_ATTACK_CHAIN
/datum/traitor_objective/hack_comm_console/proc/begin_hack(mob/user, obj/machinery/computer/communications/target)
if(!target.try_hack_console(user))
return
succeed_objective()
@@ -1,176 +0,0 @@
/datum/traitor_objective_category/infect
name = "Infect with Disease"
objectives = list(
/datum/traitor_objective/target_player/infect = 1,
)
/datum/traitor_objective/target_player/infect
name = "Infect %TARGET% the %JOB TITLE%"
description = "Infect your target with the experimental Hereditary Manifold Sickness."
progression_minimum = 30 MINUTES
progression_reward = list(8 MINUTES, 14 MINUTES)
telecrystal_reward = 1
duplicate_type = /datum/traitor_objective/target_player/infect
/// if TRUE, can only target heads of staff
/// if FALSE, CANNOT target heads of staff
var/heads_of_staff = FALSE
/// if TRUE, the injector item has been bestowed upon the player
var/injector_given = FALSE
/datum/traitor_objective/target_player/infect/supported_configuration_changes()
. = ..()
. += NAMEOF(src, objective_period)
. += NAMEOF(src, maximum_objectives_in_period)
/datum/traitor_objective/target_player/infect/can_generate_objective(generating_for, list/possible_duplicates)
if(length(possible_duplicates) > 0)
return FALSE
return ..()
/datum/traitor_objective/target_player/infect/generate_ui_buttons(mob/user)
var/list/buttons = list()
if(!injector_given)
buttons += add_ui_button("", "Pressing this will materialize a EHMS autoinjector into your hand, which you must inject into the target to succeed.", "syringe", "summon_pen")
return buttons
/datum/traitor_objective/target_player/infect/ui_perform_action(mob/living/user, action)
. = ..()
switch(action)
if("summon_pen")
if(injector_given)
return
injector_given = TRUE
var/obj/item/reagent_containers/hypospray/medipen/manifoldinjector/ehms = new(user.drop_location())
user.put_in_hands(ehms)
ehms.balloon_alert(user, "the injector materializes in your hand")
RegisterSignal(ehms, COMSIG_EHMS_INJECTOR_INJECTED, PROC_REF(on_injected))
AddComponent(/datum/component/traitor_objective_register, ehms, \
succeed_signals = null, \
fail_signals = list(COMSIG_QDELETING), \
penalty = TRUE)
/datum/traitor_objective/target_player/infect/proc/on_injected(datum/source, mob/living/user, mob/living/injected)
SIGNAL_HANDLER
if(injected != target)
fail_objective()
return
if(injected == target)
succeed_objective()
return
/datum/traitor_objective/target_player/infect/generate_objective(datum/mind/generating_for, list/possible_duplicates)
var/list/already_targeting = list() //List of minds we're already targeting. The possible_duplicates is a list of objectives, so let's not mix things
for(var/datum/objective/task as anything in handler.primary_objectives)
if(!istype(task.target, /datum/mind))
continue
already_targeting += task.target //Removing primary objective kill targets from the list
var/parent_type = type2parent(type)
//don't roll head of staff types if you haven't completed the normal version
if(heads_of_staff && !handler.get_completion_count(parent_type))
// Locked if they don't have any of the risky bug room objective completed
return FALSE
var/list/possible_targets = list()
var/try_target_late_joiners = FALSE
if(generating_for.late_joiner)
try_target_late_joiners = TRUE
for(var/datum/mind/possible_target as anything in get_crewmember_minds())
if(possible_target in already_targeting)
continue
var/target_area = get_area(possible_target.current)
if(possible_target == generating_for)
continue
if(!ishuman(possible_target.current))
continue
if(possible_target.current.stat == DEAD)
continue
var/datum/antagonist/traitor/traitor = possible_target.has_antag_datum(/datum/antagonist/traitor)
if(traitor && traitor.uplink_handler.telecrystals >= 0)
continue
var/mob/living/carbon/human/targets_current = possible_target.current
var/datum/disease/chronic_illness/illness = locate() in targets_current.diseases
if(illness)
continue
if(!HAS_TRAIT(SSstation, STATION_TRAIT_LATE_ARRIVALS) && istype(target_area, /area/shuttle/arrival))
continue
//removes heads of staff from being targets from non heads of staff assassinations, and vice versa
if(heads_of_staff)
if(!(possible_target.assigned_role.job_flags & JOB_HEAD_OF_STAFF))
continue
else
if((possible_target.assigned_role.job_flags & JOB_HEAD_OF_STAFF))
continue
possible_targets += possible_target
for(var/datum/traitor_objective/target_player/objective as anything in possible_duplicates)
possible_targets -= objective.target
if(try_target_late_joiners)
var/list/all_possible_targets = possible_targets.Copy()
for(var/datum/mind/possible_target as anything in all_possible_targets)
if(!possible_target.late_joiner)
possible_targets -= possible_target
if(!possible_targets.len)
possible_targets = all_possible_targets
special_target_filter(possible_targets)
if(!possible_targets.len)
return FALSE //MISSION FAILED, WE'LL GET EM NEXT TIME
var/datum/mind/target_mind = pick(possible_targets)
set_target(target_mind.current)
replace_in_name("%TARGET%", target.real_name)
replace_in_name("%JOB TITLE%", target_mind.assigned_role.title)
RegisterSignal(target, COMSIG_LIVING_DEATH, PROC_REF(on_target_death))
return TRUE
/datum/traitor_objective/target_player/infect/ungenerate_objective()
UnregisterSignal(target, COMSIG_LIVING_DEATH)
set_target(null)
///proc for checking for special states that invalidate a target
/datum/traitor_objective/target_player/infect/proc/special_target_filter(list/possible_targets)
return
/datum/traitor_objective/target_player/infect/target_deleted()
if(objective_state == OBJECTIVE_STATE_INACTIVE)
//don't take an objective target of someone who is already obliterated
fail_objective()
return ..()
/datum/traitor_objective/target_player/infect/proc/on_target_death()
SIGNAL_HANDLER
if(objective_state == OBJECTIVE_STATE_INACTIVE)
//don't take an objective target of someone who is already dead
fail_objective()
/obj/item/reagent_containers/hypospray/medipen/manifoldinjector
name = "EHMS autoinjector"
desc = "Experimental Hereditary Manifold Sickness autoinjector."
icon_state = "tbpen"
inhand_icon_state = "tbpen"
base_icon_state = "tbpen"
volume = 30
amount_per_transfer_from_this = 30
list_reagents = list(/datum/reagent/medicine/sansufentanyl = 20)
stealthy = TRUE
//Was the injector used on someone yet?
var/used = FALSE
/obj/item/reagent_containers/hypospray/medipen/manifoldinjector/attack(mob/living/affected_mob, mob/living/carbon/human/user)
if(used)
return ..()
to_chat(affected_mob, span_warning("You feel someone try to inject you with something."))
balloon_alert(user, "injecting...")
log_combat(user, affected_mob, "attempted to inject", src)
if(!do_after(user, 1.5 SECONDS, hidden = TRUE))
balloon_alert(user, "interrupted!")
return
var/datum/disease/chronic_illness/hms = new /datum/disease/chronic_illness()
affected_mob.ForceContractDisease(hms)
used = TRUE
inject(affected_mob, user)
SEND_SIGNAL(src, COMSIG_EHMS_INJECTOR_INJECTED, user, affected_mob)
@@ -1,300 +0,0 @@
/datum/traitor_objective/target_player/kidnapping
name = "Kidnap %TARGET% the %JOB TITLE% and deliver them to %AREA%"
description = "%TARGET% holds extremely important information regarding secret NT projects - and you'll need to kidnap and deliver them to %AREA%, where our transport pod will be waiting. \
If %TARGET% is delivered alive, you will be rewarded with an additional %TC% telecrystals."
abstract_type = /datum/traitor_objective/target_player/kidnapping
/// The jobs that this objective is targeting.
var/list/target_jobs
/// Area that the target needs to be delivered to
var/area/dropoff_area
/// Have we called the pod yet?
var/pod_called = FALSE
/// How much TC do we get from sending the target alive
var/alive_bonus = 0
/// All stripped targets belongings (weakrefs)
var/list/target_belongings = list()
/// The ID of the stoppable timer for returning the captured crew
var/list/victim_timerid
duplicate_type = /datum/traitor_objective/target_player
/datum/traitor_objective/target_player/kidnapping/supported_configuration_changes()
. = ..()
. += NAMEOF(src, objective_period)
. += NAMEOF(src, maximum_objectives_in_period)
/datum/traitor_objective/target_player/kidnapping/New(datum/uplink_handler/handler)
. = ..()
AddComponent(/datum/component/traitor_objective_limit_per_time, \
/datum/traitor_objective/target_player, \
time_period = objective_period, \
maximum_objectives = maximum_objectives_in_period \
)
/datum/traitor_objective/target_player/kidnapping/common
progression_minimum = 0 MINUTES
progression_maximum = 30 MINUTES
progression_reward = list(2 MINUTES, 4 MINUTES)
telecrystal_reward = list(1, 2)
target_jobs = list(
// Cargo
/datum/job/cargo_technician,
// Engineering
/datum/job/atmospheric_technician,
/datum/job/station_engineer,
// Medical
/datum/job/chemist,
/datum/job/doctor,
/datum/job/psychologist,
/datum/job/coroner,
// Science
/datum/job/geneticist,
/datum/job/roboticist,
/datum/job/scientist,
// Service
/datum/job/bartender,
/datum/job/botanist,
/datum/job/chaplain,
/datum/job/clown,
/datum/job/curator,
/datum/job/janitor,
/datum/job/lawyer,
/datum/job/mime,
)
alive_bonus = 2
/datum/traitor_objective/target_player/kidnapping/common/assistant
progression_minimum = 0 MINUTES
progression_maximum = 15 MINUTES
target_jobs = list(
/datum/job/assistant
)
/datum/traitor_objective/target_player/kidnapping/uncommon //Hard to fish out targets
progression_minimum = 0 MINUTES
progression_maximum = 45 MINUTES
progression_reward = list(4 MINUTES, 8 MINUTES)
telecrystal_reward = list(1, 2)
target_jobs = list(
// Cargo
/datum/job/bitrunner,
/datum/job/shaft_miner,
// Medical
/datum/job/paramedic,
// Service
/datum/job/cook,
)
alive_bonus = 3
/datum/traitor_objective/target_player/kidnapping/rare
progression_minimum = 15 MINUTES
progression_maximum = 60 MINUTES
progression_reward = list(8 MINUTES, 12 MINUTES)
telecrystal_reward = list(2, 3)
target_jobs = list(
// Heads of staff
/datum/job/chief_engineer,
/datum/job/chief_medical_officer,
/datum/job/head_of_personnel,
/datum/job/research_director,
/datum/job/quartermaster,
// Security
/datum/job/detective,
/datum/job/security_officer,
/datum/job/warden,
)
alive_bonus = 4
/datum/traitor_objective/target_player/kidnapping/captain
progression_minimum = 30 MINUTES
progression_reward = list(12 MINUTES, 16 MINUTES)
telecrystal_reward = list(2, 3)
target_jobs = list(
/datum/job/captain,
/datum/job/head_of_security,
)
alive_bonus = 5
/datum/traitor_objective/target_player/kidnapping/generate_objective(datum/mind/generating_for, list/possible_duplicates)
var/list/already_targeting = list() //List of minds we're already targeting. The possible_duplicates is a list of objectives, so let's not mix things
for(var/datum/objective/task as anything in handler.primary_objectives)
if(!istype(task.target, /datum/mind))
continue
already_targeting += task.target //Removing primary objective kill targets from the list
var/list/possible_targets = list()
for(var/datum/mind/possible_target as anything in get_crewmember_minds())
if(possible_target == generating_for)
continue
if(possible_target in already_targeting)
continue
if(!ishuman(possible_target.current))
continue
if(possible_target.current.stat == DEAD)
continue
if(HAS_TRAIT(possible_target, TRAIT_HAS_BEEN_KIDNAPPED))
continue
if(possible_target.has_antag_datum(/datum/antagonist/traitor))
continue
if(!(possible_target.assigned_role.type in target_jobs))
continue
possible_targets += possible_target
for(var/datum/traitor_objective/target_player/objective as anything in possible_duplicates)
if(!objective.target) //the old objective was already completed.
continue
possible_targets -= objective.target.mind
if(!length(possible_targets))
return FALSE
var/datum/mind/target_mind = pick(possible_targets)
set_target(target_mind.current)
AddComponent(/datum/component/traitor_objective_register, target, fail_signals = list(COMSIG_QDELETING))
var/list/possible_areas = GLOB.the_station_areas.Copy()
for(var/area/possible_area as anything in possible_areas)
if(ispath(possible_area, /area/station/hallway) || ispath(possible_area, /area/station/security) || initial(possible_area.outdoors))
possible_areas -= possible_area
dropoff_area = pick(possible_areas)
replace_in_name("%TARGET%", target_mind.name)
replace_in_name("%JOB TITLE%", target_mind.assigned_role.title)
replace_in_name("%AREA%", initial(dropoff_area.name))
replace_in_name("%TC%", alive_bonus)
return TRUE
/datum/traitor_objective/target_player/kidnapping/ungenerate_objective()
set_target(null)
dropoff_area = null
/datum/traitor_objective/target_player/kidnapping/on_objective_taken(mob/user)
. = ..()
INVOKE_ASYNC(src, PROC_REF(generate_holding_area))
/datum/traitor_objective/target_player/kidnapping/proc/generate_holding_area()
// Let's load in the holding facility ahead of time
// even if they fail the objective it's better to get done now rather than later
SSmapping.lazy_load_template(LAZY_TEMPLATE_KEY_NINJA_HOLDING_FACILITY)
/datum/traitor_objective/target_player/kidnapping/generate_ui_buttons(mob/user)
var/list/buttons = list()
if(!pod_called)
buttons += add_ui_button("Call Extraction Pod", "Pressing this will call down an extraction pod.", "rocket", "call_pod")
return buttons
/datum/traitor_objective/target_player/kidnapping/ui_perform_action(mob/living/user, action)
. = ..()
switch(action)
if("call_pod")
if(pod_called)
return
var/area/user_area = get_area(user)
var/area/target_area = get_area(target)
if(user_area.type != dropoff_area)
to_chat(user, span_warning("You must be in [initial(dropoff_area.name)] to call the extraction pod."))
return
if(target_area.type != dropoff_area)
to_chat(user, span_warning("[target.real_name] must be in [initial(dropoff_area.name)] for you to call the extraction pod."))
return
call_pod(user)
/datum/traitor_objective/target_player/kidnapping/proc/call_pod(mob/living/user)
pod_called = TRUE
var/obj/structure/closet/supplypod/extractionpod/new_pod = new()
RegisterSignal(new_pod, COMSIG_ATOM_ENTERED, PROC_REF(enter_check))
new /obj/effect/pod_landingzone(get_turf(user), new_pod)
/datum/traitor_objective/target_player/kidnapping/proc/enter_check(obj/structure/closet/supplypod/extractionpod/source, entered_atom)
SIGNAL_HANDLER
if(!istype(source))
CRASH("Kidnapping objective's enter_check called with source being not an extraction pod: [source ? source.type : "N/A"]")
if(!ishuman(entered_atom))
return
var/mob/living/carbon/human/sent_mob = entered_atom
for(var/obj/item/belonging in sent_mob.gather_belongings(FALSE, FALSE))
if(belonging == sent_mob.get_item_by_slot(ITEM_SLOT_ICLOTHING) || belonging == sent_mob.get_item_by_slot(ITEM_SLOT_FEET))
continue
var/unequipped = sent_mob.temporarilyRemoveItemFromInventory(belonging)
if (!unequipped)
continue
belonging.moveToNullspace()
target_belongings.Add(WEAKREF(belonging))
var/datum/market_item/hostage/market_item = sent_mob.process_capture(rand(1000, 3000))
RegisterSignal(market_item, COMSIG_MARKET_ITEM_SPAWNED, PROC_REF(on_victim_shipped))
addtimer(CALLBACK(src, PROC_REF(handle_target), sent_mob), 1.5 SECONDS)
if(sent_mob != target)
fail_objective(penalty_cost = telecrystal_penalty)
source.startExitSequence(source)
return
if(sent_mob.stat != DEAD)
telecrystal_reward += alive_bonus
succeed_objective()
source.startExitSequence(source)
/datum/traitor_objective/target_player/kidnapping/proc/handle_target(mob/living/carbon/human/sent_mob)
victim_timerid = addtimer(CALLBACK(src, PROC_REF(return_target), sent_mob), COME_BACK_FROM_CAPTURE_TIME, TIMER_STOPPABLE)
if(sent_mob.stat == DEAD)
return
sent_mob.flash_act()
sent_mob.adjust_confusion(10 SECONDS)
sent_mob.adjust_dizzy(10 SECONDS)
sent_mob.set_eye_blur_if_lower(100 SECONDS)
sent_mob.dna.species.give_important_for_life(sent_mob) // so plasmamen do not get left for dead
to_chat(sent_mob, span_hypnophrase("A million voices echo in your head... <i>\"Your mind held many valuable secrets - \
we thank you for providing them. Your value is expended, and you will be ransomed back to your station. We always get paid, \
so it's only a matter of time before we ship you back...\"</i>"))
/datum/traitor_objective/target_player/kidnapping/proc/return_target(mob/living/carbon/human/sent_mob)
if(!sent_mob || QDELETED(sent_mob)) //suicided and qdeleted themselves
return
var/obj/structure/closet/supplypod/back_to_station/return_pod = new()
return_pod.return_from_capture(sent_mob)
returnal_side_effects(return_pod, sent_mob)
/datum/traitor_objective/target_player/kidnapping/proc/on_victim_shipped(datum/market_item/source, obj/item/market_uplink/uplink, shipping_method, turf/shipping_loc)
SIGNAL_HANDLER
deltimer(victim_timerid)
returnal_side_effects(shipping_loc, source.item)
/datum/traitor_objective/target_player/kidnapping/proc/returnal_side_effects(atom/dropoff_location, mob/living/carbon/human/sent_mob)
for(var/obj/item/belonging in sent_mob.gather_belongings())
if(belonging == sent_mob.get_item_by_slot(ITEM_SLOT_ICLOTHING) || belonging == sent_mob.get_item_by_slot(ITEM_SLOT_FEET))
continue
sent_mob.dropItemToGround(belonging) // No souvenirs, except shoes and t-shirts
for(var/datum/weakref/belonging_ref in target_belongings)
var/obj/item/belonging = belonging_ref.resolve()
if(!belonging)
continue
belonging.forceMove(dropoff_location)
sent_mob.flash_act()
sent_mob.adjust_confusion(10 SECONDS)
sent_mob.adjust_dizzy(10 SECONDS)
sent_mob.set_eye_blur_if_lower(100 SECONDS)
sent_mob.dna.species.give_important_for_life(sent_mob) // so plasmamen do not get left for dead
@@ -1,97 +0,0 @@
/datum/traitor_objective_category/kill_pet
name = "Kill Pet"
objectives = list(
/datum/traitor_objective/kill_pet/high_risk = 1,
list(
/datum/traitor_objective/kill_pet = 2,
/datum/traitor_objective/kill_pet/medium_risk = 1,
) = 4,
)
/datum/traitor_objective/kill_pet
name = "Kill the %DEPARTMENT HEAD%'s beloved %PET%"
description = "The %DEPARTMENT HEAD% has particularly annoyed us by sending us spam emails and we want their %PET% dead to show them what happens when they cross us. "
progression_minimum = 0 MINUTES
telecrystal_reward = list(1, 2)
progression_reward = list(3 MINUTES, 6 MINUTES)
/// Possible heads mapped to their pet type. Can be a list of possible pets
var/list/possible_heads = list(
JOB_HEAD_OF_PERSONNEL = list(
/mob/living/basic/pet/dog/corgi/ian,
/mob/living/basic/pet/dog/corgi/puppy/ian
),
JOB_CAPTAIN = /mob/living/basic/pet/fox/renault,
JOB_CHIEF_MEDICAL_OFFICER = /mob/living/basic/pet/cat/runtime,
JOB_CHIEF_ENGINEER = /mob/living/basic/parrot/poly,
JOB_QUARTERMASTER = list(
/mob/living/basic/gorilla/cargorilla,
/mob/living/basic/sloth/citrus,
/mob/living/basic/sloth/paperwork,
)
)
/// The head that we are targeting
var/datum/job/target
/// Whether or not we only take from the traitor's own department head or not.
var/limited_to_department_head = TRUE
/// The actual pet that needs to be killed
var/mob/living/target_pet
duplicate_type = /datum/traitor_objective/kill_pet
/datum/traitor_objective/kill_pet/medium_risk
progression_minimum = 10 MINUTES
progression_reward = list(5 MINUTES, 8 MINUTES)
limited_to_department_head = FALSE
/datum/traitor_objective/kill_pet/high_risk
progression_minimum = 25 MINUTES
progression_reward = list(14 MINUTES, 18 MINUTES)
telecrystal_reward = list(2, 3)
limited_to_department_head = FALSE
possible_heads = list(
JOB_HEAD_OF_SECURITY = list(
/mob/living/basic/carp/pet/lia,
/mob/living/basic/spider/giant/sgt_araneus,
/mob/living/basic/bear/snow/misha,
),
JOB_WARDEN = list(
/mob/living/basic/pet/dog/pug/mcgriff
)
)
/datum/traitor_objective/kill_pet/generate_objective(datum/mind/generating_for, list/possible_duplicates)
var/datum/job/role = generating_for.assigned_role
for(var/datum/traitor_objective/kill_pet/objective as anything in possible_duplicates)
possible_heads -= objective.target.title
if(limited_to_department_head)
possible_heads = possible_heads & role.department_head
possible_heads -= role.title
if(!length(possible_heads))
return FALSE
target = SSjob.name_occupations[pick(possible_heads)]
var/pet_type = possible_heads[target.title]
if(islist(pet_type))
for(var/type in pet_type)
target_pet = locate(type) in GLOB.mob_living_list
if(target_pet)
break
else
target_pet = locate(pet_type) in GLOB.mob_living_list
if(!target_pet)
return FALSE
if(target_pet.stat == DEAD)
return FALSE
AddComponent(/datum/component/traitor_objective_register, target_pet, \
succeed_signals = list(COMSIG_QDELETING, COMSIG_LIVING_DEATH))
replace_in_name("%DEPARTMENT HEAD%", target.title)
replace_in_name("%PET%", target_pet.name)
return TRUE
/datum/traitor_objective/kill_pet/ungenerate_objective()
if(target_pet)
UnregisterSignal(target_pet, list(COMSIG_QDELETING, COMSIG_LIVING_DEATH))
target_pet = null
@@ -1,282 +0,0 @@
/datum/traitor_objective_category/locate_weakpoint
name = "Locate And Destroy Weakpoint"
objectives = list(
/datum/traitor_objective/locate_weakpoint = 1,
)
weight = OBJECTIVE_WEIGHT_UNLIKELY
/datum/traitor_objective/locate_weakpoint
name = "Triangulate station's structural weakpoint and detonate an explosive charge nearby."
description = "You will be given a handheld device that you'll need to use in %AREA1% and %AREA2% in order to triangulate the station's structural weakpoint and detonate an explosive charge there. Warning: Once you start scanning either one of the areas, station's AI will be alerted."
progression_minimum = 45 MINUTES
progression_reward = list(15 MINUTES, 20 MINUTES)
telecrystal_reward = list(3, 5)
var/progression_objectives_minimum = 20 MINUTES
/// Have we sent a weakpoint locator yet?
var/locator_sent = FALSE
/// Have we sent a bomb yet?
var/bomb_sent = FALSE
/// Have we located the weakpoint yet?
var/weakpoint_found = FALSE
/// Areas that need to be scanned
var/list/area/scan_areas
/// Weakpoint where the bomb should be planted
var/area/weakpoint_area
/datum/traitor_objective/locate_weakpoint/can_generate_objective(datum/mind/generating_for, list/possible_duplicates)
if(length(possible_duplicates) > 0)
return FALSE
if(handler.get_completion_progression(/datum/traitor_objective) < progression_objectives_minimum)
return FALSE
if(SStraitor.get_taken_count(/datum/traitor_objective/locate_weakpoint) > 0)
return FALSE
return TRUE
/datum/traitor_objective/locate_weakpoint/generate_objective(datum/mind/generating_for, list/possible_duplicates)
scan_areas = list()
/// List of high-security areas that we pick required ones from
var/list/allowed_areas = typecacheof(list(/area/station/command,
/area/station/comms,
/area/station/engineering,
/area/station/science,
/area/station/security,
))
var/list/blacklisted_areas = typecacheof(list(/area/station/engineering/hallway,
/area/station/engineering/lobby,
/area/station/engineering/storage,
/area/station/science/lobby,
/area/station/science/ordnance/bomb,
/area/station/security/prison,
))
var/list/possible_areas = GLOB.the_station_areas.Copy()
for(var/area/possible_area as anything in possible_areas)
if(!is_type_in_typecache(possible_area, allowed_areas) || initial(possible_area.outdoors) || is_type_in_typecache(possible_area, blacklisted_areas))
possible_areas -= possible_area
for(var/i in 1 to 2)
scan_areas[pick_n_take(possible_areas)] = TRUE
weakpoint_area = pick_n_take(possible_areas)
var/area/scan_area1 = scan_areas[1]
var/area/scan_area2 = scan_areas[2]
replace_in_name("%AREA1%", initial(scan_area1.name))
replace_in_name("%AREA2%", initial(scan_area2.name))
RegisterSignal(SSdcs, COMSIG_GLOB_TRAITOR_OBJECTIVE_COMPLETED, PROC_REF(on_global_obj_completed))
return TRUE
/datum/traitor_objective/locate_weakpoint/ungenerate_objective()
UnregisterSignal(SSdcs, COMSIG_GLOB_TRAITOR_OBJECTIVE_COMPLETED)
/datum/traitor_objective/locate_weakpoint/on_objective_taken(mob/user)
. = ..()
// We don't want multiple people being able to take weakpoint objectives if they get one available at the same time
for(var/datum/traitor_objective/locate_weakpoint/other_objective as anything in SStraitor.all_objectives_by_type[/datum/traitor_objective/locate_weakpoint])
if(other_objective != src)
other_objective.fail_objective()
/datum/traitor_objective/locate_weakpoint/proc/on_global_obj_completed(datum/source, datum/traitor_objective/objective)
SIGNAL_HANDLER
if(istype(objective, /datum/traitor_objective/locate_weakpoint))
fail_objective()
/datum/traitor_objective/locate_weakpoint/generate_ui_buttons(mob/user)
var/list/buttons = list()
if(!locator_sent)
buttons += add_ui_button("", "Pressing this will materialize a weakpoint locator in your hand.", "globe", "locator")
if(weakpoint_found && !bomb_sent)
buttons += add_ui_button("", "Pressing this will materialize an ES8 explosive charge in your hand.", "bomb", "shatter_charge")
return buttons
/datum/traitor_objective/locate_weakpoint/ui_perform_action(mob/living/user, action)
. = ..()
switch(action)
if("locator")
if(locator_sent)
return
locator_sent = TRUE
var/obj/item/weakpoint_locator/locator = new(user.drop_location(), src)
user.put_in_hands(locator)
locator.balloon_alert(user, "the weakpoint locator materializes in your hand")
if("shatter_charge")
if(bomb_sent)
return
bomb_sent = TRUE
var/obj/item/grenade/c4/es8/bomb = new(user.drop_location(), src)
user.put_in_hands(bomb)
bomb.balloon_alert(user, "the ES8 charge materializes in your hand")
/datum/traitor_objective/locate_weakpoint/proc/weakpoint_located()
description = "Structural weakpoint has been located in %AREA%. Detonate an ES8 explosive charge there to create a shockwave that will severely damage the station."
replace_in_name("%AREA%", initial(weakpoint_area.name))
weakpoint_found = TRUE
/datum/traitor_objective/locate_weakpoint/proc/create_shockwave(center_x, center_y, center_z)
var/turf/epicenter = locate(center_x, center_y, center_z)
var/lowpop = (length(GLOB.clients) <= CONFIG_GET(number/minimal_access_threshold))
if(lowpop)
explosion(epicenter, devastation_range = 2, heavy_impact_range = 4, light_impact_range = 6, explosion_cause = src)
else
explosion(epicenter, devastation_range = 3, heavy_impact_range = 6, light_impact_range = 9, explosion_cause = src)
priority_announce(
"Attention crew, it appears that a high-power explosive charge has been detonated in your station's weakpoint, causing severe structural damage.",
"[command_name()] High-Priority Update"
)
succeed_objective()
/obj/item/weakpoint_locator
name = "structural weakpoint locator"
desc = "A device that can triangulate station's structural weakpoint. It has to be used in %AREA1% and %AREA2% in order to triangulate the weakpoint. Warning: station's AI will be notified as soon as the process starts!"
icon = 'icons/obj/antags/syndicate_tools.dmi'
icon_state = "weakpoint_locator"
inhand_icon_state = "weakpoint_locator"
lefthand_file = 'icons/mob/inhands/items/devices_lefthand.dmi'
righthand_file = 'icons/mob/inhands/items/devices_righthand.dmi'
throwforce = 0
w_class = WEIGHT_CLASS_SMALL
throw_speed = 3
throw_range = 5
var/datum/weakref/objective_weakref
/obj/item/weakpoint_locator/Initialize(mapload, datum/traitor_objective/locate_weakpoint/objective)
. = ..()
objective_weakref = WEAKREF(objective)
if(!objective)
return
var/area/area1 = objective.scan_areas[1]
var/area/area2 = objective.scan_areas[2]
desc = replacetext(desc, "%AREA1%", initial(area1.name))
desc = replacetext(desc, "%AREA2%", initial(area2.name))
/obj/item/weakpoint_locator/Destroy(force)
objective_weakref = null
return ..()
/obj/item/weakpoint_locator/attack_self(mob/living/user, modifiers)
. = ..()
if(!istype(user) || loc != user || !user.mind) //No TK cheese
return
var/datum/traitor_objective/locate_weakpoint/objective = objective_weakref.resolve()
if(!objective || objective.objective_state == OBJECTIVE_STATE_INACTIVE)
to_chat(user, span_warning("Your time to use [src] has not come yet."))
return
if(objective.handler.owner != user.mind)
to_chat(user, span_warning("You have zero clue how to use [src]."))
return
var/area/user_area = get_area(user)
if(!(user_area.type in objective.scan_areas))
balloon_alert(user, "invalid area!")
playsound(user, 'sound/machines/buzz/buzz-sigh.ogg', 30, TRUE)
return
if(!objective.scan_areas[user_area.type])
balloon_alert(user, "already scanned here!")
playsound(user, 'sound/machines/buzz/buzz-sigh.ogg', 30, TRUE)
return
user.visible_message(span_danger("[user] presses a few buttons on [src] and it starts ominously beeping!"), span_notice("You activate [src] and start scanning the area. Do not exit [get_area_name(user, TRUE)] until the scan finishes!"))
playsound(user, 'sound/machines/beep/triple_beep.ogg', 30, TRUE)
var/alertstr = span_userdanger("Network Alert: Station network probing attempt detected[user_area?" in [get_area_name(user, TRUE)]":". Unable to pinpoint location"].")
for(var/mob/living/silicon/ai/ai_player in GLOB.player_list)
to_chat(ai_player, alertstr)
if(!do_after(user, 30 SECONDS, src, IGNORE_USER_LOC_CHANGE | IGNORE_TARGET_LOC_CHANGE | IGNORE_HELD_ITEM | IGNORE_INCAPACITATED | IGNORE_SLOWDOWNS, extra_checks = CALLBACK(src, PROC_REF(scan_checks), user, user_area, objective), hidden = TRUE))
playsound(user, 'sound/machines/buzz/buzz-sigh.ogg', 30, TRUE)
return
playsound(user, 'sound/machines/ding.ogg', 100, TRUE)
objective.scan_areas[user_area.type] = FALSE
for(var/area/scan_area as anything in objective.scan_areas)
if(objective.scan_areas[scan_area])
say("Next scanning location is [initial(scan_area.name)]")
return
to_chat(user, span_notice("Scan finished. Structural weakpoint located in [initial(objective.weakpoint_area.name)]."))
objective.weakpoint_located()
/obj/item/weakpoint_locator/proc/scan_checks(mob/living/user, area/user_area, datum/traitor_objective/locate_weakpoint/parent_objective)
if(get_area(user) != user_area)
return FALSE
if(parent_objective.objective_state != OBJECTIVE_STATE_ACTIVE)
return FALSE
var/atom/current_loc = loc
while(!isturf(current_loc) && !ismob(current_loc))
current_loc = current_loc.loc
if(current_loc != user)
return FALSE
return TRUE
/obj/item/grenade/c4/es8
name = "ES8 explosive charge"
desc = "A high-power explosive charge designed to create a shockwave in a structural weakpoint of the station."
icon_state = "plasticx40"
inhand_icon_state = "plasticx4"
worn_icon_state = "x4"
boom_sizes = list(3, 6, 9)
/// Weakref to user's objective
var/datum/weakref/objective_weakref
/obj/item/grenade/c4/es8/Initialize(mapload, objective)
. = ..()
objective_weakref = WEAKREF(objective)
/obj/item/grenade/c4/es8/Destroy()
objective_weakref = null
return ..()
/obj/item/grenade/c4/es8/plant_c4(atom/bomb_target, mob/living/user)
if(!IS_TRAITOR(user))
to_chat(user, span_warning("You can't seem to find a way to detonate the charge."))
return FALSE
var/datum/traitor_objective/locate_weakpoint/objective = objective_weakref.resolve()
if(!objective || objective.objective_state == OBJECTIVE_STATE_INACTIVE || objective.handler.owner != user.mind)
to_chat(user, span_warning("You don't think it would be wise to use [src]."))
return FALSE
var/area/target_area = get_area(bomb_target)
if (target_area.type != objective.weakpoint_area)
to_chat(user, span_warning("[src] can only be detonated in [initial(objective.weakpoint_area.name)]."))
return FALSE
if(!isfloorturf(bomb_target) && !iswallturf(bomb_target))
to_chat(user, span_warning("[src] can only be planted on a wall or the floor!"))
return FALSE
return ..()
/obj/item/grenade/c4/es8/detonate(mob/living/lanced_by)
var/area/target_area = get_area(target)
var/datum/traitor_objective/locate_weakpoint/objective = objective_weakref.resolve()
if(!objective)
return
if (target_area.type != objective.weakpoint_area)
var/obj/item/grenade/c4/es8/new_bomb = new(target.drop_location())
new_bomb.balloon_alert_to_viewers("invalid location!")
target.cut_overlay(plastic_overlay, TRUE)
qdel(src)
return
objective.create_shockwave(target.x, target.y, target.z)
return ..()
@@ -1,239 +0,0 @@
/// Datum which manages references to things we are instructed to destroy
GLOBAL_DATUM_INIT(objective_machine_handler, /datum/objective_target_machine_handler, new())
/// Marks a machine as a possible traitor sabotage target
/proc/add_sabotage_machine(source, typepath)
LAZYADD(GLOB.objective_machine_handler.machine_instances_by_path[typepath], source)
return typepath
/// Traitor objective to destroy a machine the crew cares about
/datum/traitor_objective_category/sabotage_machinery
name = "Sabotage Worksite"
objectives = list(
/datum/traitor_objective/sabotage_machinery/trap = 1,
/datum/traitor_objective/sabotage_machinery/destroy = 1,
)
/datum/traitor_objective/sabotage_machinery
name = "Sabotage the %MACHINE%"
description = "Abstract objective holder which shouldn't appear in your uplink."
abstract_type = /datum/traitor_objective/sabotage_machinery
/// The maximum amount of this type of objective a traitor can have, set to 0 for no limit.
var/maximum_allowed = 0
/// The possible target machinery and the jobs tied to each one.
var/list/applicable_jobs = list()
/// The chosen job. Used to check for duplicates
var/chosen_job
/datum/traitor_objective/sabotage_machinery/can_generate_objective(datum/mind/generating_for, list/possible_duplicates)
if(!maximum_allowed)
return TRUE
if(length(possible_duplicates) >= maximum_allowed)
return FALSE
return TRUE
/datum/traitor_objective/sabotage_machinery/generate_objective(datum/mind/generating_for, list/possible_duplicates)
var/list/possible_jobs = applicable_jobs.Copy()
for(var/datum/traitor_objective/sabotage_machinery/objective as anything in possible_duplicates)
possible_jobs -= objective.chosen_job
for(var/available_job in possible_jobs)
var/job_machine_path = possible_jobs[available_job]
if (!length(GLOB.objective_machine_handler.machine_instances_by_path[job_machine_path]))
possible_jobs -= available_job
if(!length(possible_jobs))
return FALSE
chosen_job = pick(possible_jobs)
var/list/obj/machinery/possible_machines = GLOB.objective_machine_handler.machine_instances_by_path[possible_jobs[chosen_job]]
for(var/obj/machinery/machine as anything in possible_machines)
prepare_machine(machine)
replace_in_name("%JOB%", LOWER_TEXT(chosen_job))
replace_in_name("%MACHINE%", possible_machines[1].name)
return TRUE
/// Marks a given machine as our target
/datum/traitor_objective/sabotage_machinery/proc/prepare_machine(obj/machinery/machine)
AddComponent(/datum/component/traitor_objective_register, machine, succeed_signals = list(COMSIG_QDELETING))
// Destroy machines which are in annoying locations, are annoying when destroyed, and aren't directly interacted with
/datum/traitor_objective/sabotage_machinery/destroy
name = "Destroy the %MACHINE%"
description = "Destroy the %MACHINE% to cause disarray and disrupt the operations of the %JOB%'s department."
progression_reward = list(5 MINUTES, 10 MINUTES)
telecrystal_reward = list(3, 4)
progression_minimum = 15 MINUTES
applicable_jobs = list(
JOB_STATION_ENGINEER = /obj/machinery/telecomms/hub,
JOB_SCIENTIST = /obj/machinery/rnd/server,
)
// Rig machines which are in public locations to explode when interacted with
/datum/traitor_objective/sabotage_machinery/trap
name = "Sabotage the %MACHINE%"
description = "Destroy the %MACHINE% to cause disarray and disrupt the operations of the %JOB%'s department. If you can get another crew member to destroy the machine using the provided booby trap, you will be rewarded with an additional %PROGRESSION% reputation and %TC% telecrystals."
progression_reward = list(2 MINUTES, 4 MINUTES)
telecrystal_reward = 0 // Only from completing the bonus objective
progression_minimum = 0 MINUTES
progression_maximum = 10 MINUTES
maximum_allowed = 2
applicable_jobs = list(
JOB_CHIEF_ENGINEER = /obj/machinery/rnd/production/protolathe/department/engineering,
JOB_CHIEF_MEDICAL_OFFICER = /obj/machinery/rnd/production/techfab/department/medical,
JOB_HEAD_OF_PERSONNEL = /obj/machinery/rnd/production/techfab/department/service,
JOB_QUARTERMASTER = /obj/machinery/rnd/production/techfab/department/cargo,
JOB_RESEARCH_DIRECTOR = /obj/machinery/rnd/production/protolathe/department/science,
JOB_SHAFT_MINER = /obj/machinery/mineral/ore_redemption,
)
/// Bonus reward to grant if you booby trap successfully
var/bonus_tc = 2
/// Bonus progression to grant if you booby trap successfully
var/bonus_progression = 5 MINUTES
/// Have we given out a traitor trap item?
var/traitor_trapper_given = FALSE
/datum/traitor_objective/sabotage_machinery/trap/generate_objective(datum/mind/generating_for, list/possible_duplicates)
. = ..()
if (!.)
return FALSE
replace_in_name("%TC%", bonus_tc)
replace_in_name("%PROGRESSION%", DISPLAY_PROGRESSION(bonus_progression))
return TRUE
/datum/traitor_objective/sabotage_machinery/trap/prepare_machine(obj/machinery/machine)
RegisterSignal(machine, COMSIG_TRAITOR_MACHINE_TRAP_TRIGGERED, PROC_REF(sabotage_success))
return ..()
/// Called when you successfully proc the booby trap, gives a bonus reward
/datum/traitor_objective/sabotage_machinery/trap/proc/sabotage_success(obj/machinery/machine)
progression_reward += bonus_progression
telecrystal_reward += bonus_tc
succeed_objective()
/datum/traitor_objective/sabotage_machinery/trap/generate_ui_buttons(mob/user)
var/list/buttons = list()
if(!traitor_trapper_given)
buttons += add_ui_button("", "Pressing this will materialize an explosive trap in your hand, which you can conceal within the target machine", "wifi", "summon_gear")
return buttons
/datum/traitor_objective/sabotage_machinery/trap/ui_perform_action(mob/living/user, action)
. = ..()
switch(action)
if("summon_gear")
if(traitor_trapper_given)
return
traitor_trapper_given = TRUE
var/obj/item/traitor_machine_trapper/tool = new(user.drop_location())
user.put_in_hands(tool)
tool.balloon_alert(user, "a booby trap materializes in your hand")
tool.target_machine_path = applicable_jobs[chosen_job]
/// Item which you use on a machine to cause it to explode next time someone interacts with it
/obj/item/traitor_machine_trapper
name = "suspicious device"
desc = "It looks dangerous."
icon = 'icons/obj/weapons/grenade.dmi'
icon_state = "boobytrap"
/// Light explosion range, to hurt the person using the machine
var/explosion_range = 3
/// The type of object on which this can be planted on.
var/obj/machinery/target_machine_path
/// The time it takes to deploy the bomb.
var/deploy_time = 10 SECONDS
/obj/item/traitor_machine_trapper/examine(mob/user)
. = ..()
if(!IS_TRAITOR(user))
return
if(target_machine_path)
. += span_notice("This device must be placed by <b>clicking on a [initial(target_machine_path.name)]</b> with it. It can be removed with a screwdriver.")
. += span_notice("Remember, you may leave behind fingerprints on the device. Wear <b>gloves</b> when handling it to be safe!")
/obj/item/traitor_machine_trapper/pre_attack(atom/target, mob/living/user, params)
. = ..()
if (. || !istype(target, target_machine_path))
return
balloon_alert(user, "planting device...")
if(!do_after(user, delay = deploy_time, target = src, interaction_key = DOAFTER_SOURCE_PLANTING_DEVICE, hidden = TRUE))
return TRUE
target.AddComponent(\
/datum/component/interaction_booby_trap,\
additional_triggers = list(COMSIG_ORM_COLLECTED_ORE),\
on_triggered_callback = CALLBACK(src, PROC_REF(on_triggered)),\
on_defused_callback = CALLBACK(src, PROC_REF(on_defused)),\
)
RegisterSignal(target, COMSIG_QDELETING, GLOBAL_PROC_REF(qdel), src)
moveToNullspace()
return TRUE
/// Called when applied trap is triggered, mark success
/obj/item/traitor_machine_trapper/proc/on_triggered(atom/machine)
SEND_SIGNAL(machine, COMSIG_TRAITOR_MACHINE_TRAP_TRIGGERED)
qdel(src)
/// Called when applied trap has been defused, retrieve this item from nullspace
/obj/item/traitor_machine_trapper/proc/on_defused(atom/machine, mob/defuser, obj/item/tool)
UnregisterSignal(machine, COMSIG_QDELETING)
playsound(machine, 'sound/effects/structure_stress/pop3.ogg', 100, vary = TRUE)
forceMove(get_turf(machine))
visible_message(span_warning("A [src] falls out from the [machine]!"))
/// Datum which manages references to things we are instructed to destroy
/datum/objective_target_machine_handler
/// Existing instances of machines organised by typepath
var/list/machine_instances_by_path = list()
/datum/objective_target_machine_handler/New()
. = ..()
RegisterSignal(SSdcs, COMSIG_GLOB_NEW_MACHINE, PROC_REF(on_machine_created))
RegisterSignal(SSatoms, COMSIG_SUBSYSTEM_POST_INITIALIZE, PROC_REF(finalise_valid_targets))
/// Adds a newly created machine to our list of machines, if we need it
/datum/objective_target_machine_handler/proc/on_machine_created(datum/source, obj/machinery/new_machine)
SIGNAL_HANDLER
new_machine.add_as_sabotage_target()
/// Confirm that everything added to the list is a valid target, then prevent new targets from being added
/datum/objective_target_machine_handler/proc/finalise_valid_targets()
SIGNAL_HANDLER
for (var/machine_type in machine_instances_by_path)
for (var/obj/machinery/machine as anything in machine_instances_by_path[machine_type])
var/turf/place = get_turf(machine)
if(!place || !is_station_level(place.z))
machine_instances_by_path[machine_type] -= machine
continue
RegisterSignal(machine, COMSIG_QDELETING, PROC_REF(machine_destroyed))
UnregisterSignal(SSdcs, COMSIG_GLOB_NEW_MACHINE)
/datum/objective_target_machine_handler/proc/machine_destroyed(atom/machine)
SIGNAL_HANDLER
// Sadly can't do a direct typepath association because of some map helper subtypes
for (var/machine_type in machine_instances_by_path)
machine_instances_by_path[machine_type] -= machine
// Mark valid machines as targets, add a new entry here if you add a new potential target
/obj/machinery/telecomms/hub/add_as_sabotage_target()
return add_sabotage_machine(src, /obj/machinery/telecomms/hub) // Not always our specific type because of map helper subtypes
/obj/machinery/rnd/server/add_as_sabotage_target()
return add_sabotage_machine(src, type)
/obj/machinery/rnd/production/protolathe/department/add_as_sabotage_target()
return add_sabotage_machine(src, type)
/obj/machinery/rnd/production/techfab/department/add_as_sabotage_target()
return add_sabotage_machine(src, type)
/obj/machinery/mineral/ore_redemption/add_as_sabotage_target()
return add_sabotage_machine(src, type)
@@ -1,322 +0,0 @@
/datum/traitor_objective_category/steal_item
name = "Steal Item"
objectives = list(
list(
/datum/traitor_objective/steal_item/low_risk = 1,
/datum/traitor_objective/destroy_item/low_risk = 1,
) = 1,
/datum/traitor_objective/steal_item/somewhat_risky = 1,
list(
/datum/traitor_objective/destroy_item/very_risky = 1,
/datum/traitor_objective/steal_item/very_risky = 1,
) = 1,
/datum/traitor_objective/steal_item/most_risky = 1
)
GLOBAL_DATUM_INIT(steal_item_handler, /datum/objective_item_handler, new())
/datum/objective_item_handler
var/list/list/objectives_by_path
var/generated_items = FALSE
/datum/objective_item_handler/New()
. = ..()
objectives_by_path = list()
for(var/datum/objective_item/item as anything in subtypesof(/datum/objective_item))
objectives_by_path[initial(item.targetitem)] = list()
RegisterSignal(SSatoms, COMSIG_SUBSYSTEM_POST_INITIALIZE, PROC_REF(save_items))
RegisterSignal(SSdcs, COMSIG_GLOB_NEW_ITEM, PROC_REF(new_item_created))
/datum/objective_item_handler/proc/new_item_created(datum/source, obj/item/item)
SIGNAL_HANDLER
if(HAS_TRAIT(item, TRAIT_ITEM_OBJECTIVE_BLOCKED))
return
if(!generated_items)
item.add_stealing_item_objective()
return
var/typepath = item.add_stealing_item_objective()
if(typepath != null)
register_item(item, typepath)
/// Registers all items that are potentially stealable and removes ones that aren't.
/// We still need to do things this way because on mapload, items may not be on the station until everything has finished loading.
/datum/objective_item_handler/proc/save_items()
SIGNAL_HANDLER
for(var/obj/item/typepath as anything in objectives_by_path)
var/list/obj_by_path_cache = objectives_by_path[typepath].Copy()
for(var/obj/item/object as anything in obj_by_path_cache)
register_item(object, typepath)
generated_items = TRUE
/datum/objective_item_handler/proc/register_item(atom/object, typepath)
var/turf/place = get_turf(object)
if(!place || !is_station_level(place.z))
objectives_by_path[typepath] -= object
return
RegisterSignal(object, COMSIG_QDELETING, PROC_REF(remove_item))
/datum/objective_item_handler/proc/remove_item(atom/source)
SIGNAL_HANDLER
for(var/typepath in objectives_by_path)
objectives_by_path[typepath] -= source
/datum/traitor_objective/steal_item
name = "Steal %ITEM% and place a schematics scanner on it."
description = "Use the button below to materialize the schematic scanner within your hand, where you'll then be able to place it on the item. Additionally, you can keep it near you and let it scan for %TIME% minutes, and you will be rewarded with %PROGRESSION% reputation and %TC% telecrystals."
progression_minimum = 20 MINUTES
var/list/possible_items = list()
/// The current target item that we are stealing.
var/datum/objective_item/steal/target_item
/// A list of 2 elements, which contain the range that the time will be in. Represented in minutes.
var/hold_time_required = list(5, 15)
/// The current time fulfilled around the item
var/time_fulfilled = 0
/// The maximum distance between the bug and the objective taker for time to count as fulfilled
var/max_distance = 4
/// The bug that will be put onto the item
var/obj/item/traitor_bug/bug
/// Any special equipment that may be needed
var/list/special_equipment
/// Telecrystal reward increase per unit of time.
var/minutes_per_telecrystal = 3
/// Extra TC given for holding the item for the required duration of time.
var/extra_tc = 0
/// Extra progression given for holding the item for the required duration of time.
var/extra_progression = 0
abstract_type = /datum/traitor_objective/steal_item
/datum/traitor_objective/steal_item/low_risk
progression_minimum = 10 MINUTES
progression_maximum = 35 MINUTES
progression_reward = list(5 MINUTES, 10 MINUTES)
telecrystal_reward = 0
minutes_per_telecrystal = 6
possible_items = list(
/datum/objective_item/steal/traitor/cargo_budget,
/datum/objective_item/steal/traitor/clown_shoes,
/datum/objective_item/steal/traitor/lawyers_badge,
/datum/objective_item/steal/traitor/chef_moustache,
/datum/objective_item/steal/traitor/pka,
)
/datum/traitor_objective/steal_item/somewhat_risky
progression_minimum = 20 MINUTES
progression_maximum = 50 MINUTES
progression_reward = 10 MINUTES
telecrystal_reward = 2
possible_items = list(
/datum/objective_item/steal/traitor/chief_engineer_belt
)
/datum/traitor_objective/steal_item/very_risky
progression_minimum = 30 MINUTES
progression_reward = 15 MINUTES
telecrystal_reward = 3
possible_items = list(
/datum/objective_item/steal/traitor/det_revolver,
)
/datum/traitor_objective/steal_item/most_risky
progression_minimum = 50 MINUTES
progression_reward = 20 MINUTES
telecrystal_reward = 5
possible_items = list(
/datum/objective_item/steal/traitor/captain_modsuit,
/datum/objective_item/steal/traitor/captain_spare,
)
/datum/traitor_objective/steal_item/most_risky/generate_objective(datum/mind/generating_for, list/possible_duplicates)
if(!handler.get_completion_count(/datum/traitor_objective/steal_item/very_risky))
return FALSE
return ..()
/datum/traitor_objective/steal_item/generate_objective(datum/mind/generating_for, list/possible_duplicates)
for(var/datum/traitor_objective/steal_item/objective as anything in possible_duplicates)
possible_items -= objective.target_item.type
while(length(possible_items))
var/datum/objective_item/steal/target = pick_n_take(possible_items)
target = new target()
if(!target.valid_objective_for(list(generating_for), require_owner = TRUE))
qdel(target)
continue
target_item = target
break
if(!target_item)
return FALSE
if(length(target_item.special_equipment))
special_equipment = target_item.special_equipment
hold_time_required = rand(hold_time_required[1], hold_time_required[2])
extra_progression += hold_time_required * (1 MINUTES)
extra_tc += round(hold_time_required / max(minutes_per_telecrystal, 0.1))
replace_in_name("%ITEM%", target_item.name)
replace_in_name("%TIME%", hold_time_required)
replace_in_name("%TC%", extra_tc)
replace_in_name("%PROGRESSION%", DISPLAY_PROGRESSION(extra_progression))
return TRUE
/datum/traitor_objective/steal_item/ungenerate_objective()
STOP_PROCESSING(SSprocessing, src)
if(bug)
UnregisterSignal(bug, list(COMSIG_TRAITOR_BUG_PLANTED_OBJECT, COMSIG_TRAITOR_BUG_PRE_PLANTED_OBJECT))
bug = null
/datum/traitor_objective/steal_item/generate_ui_buttons(mob/user)
var/list/buttons = list()
if(special_equipment)
buttons += add_ui_button("", "Pressing this will summon any extra special equipment you may need for the mission.", "tools", "summon_gear")
if(!bug)
buttons += add_ui_button("", "Pressing this will materialize a scanner in your hand, which you can place on the target item", "wifi", "summon_bug")
else if(bug.planted_on)
buttons += add_ui_button("[DisplayTimeText(time_fulfilled)]", "This tells you how much time you have spent around the target item after the scanner has been planted.", "clock", "none")
buttons += add_ui_button("Skip Time", "Pressing this will succeed the mission. You will not get the extra TC and progression.", "forward", "cash_out")
return buttons
/datum/traitor_objective/steal_item/ui_perform_action(mob/living/user, action)
. = ..()
switch(action)
if("summon_bug")
if(bug)
return
bug = new(user.drop_location())
user.put_in_hands(bug)
bug.balloon_alert(user, "the scanner materializes in your hand")
bug.target_object_type = target_item.targetitem
AddComponent(/datum/component/traitor_objective_register, bug, \
fail_signals = list(COMSIG_QDELETING), \
penalty = telecrystal_penalty)
RegisterSignal(bug, COMSIG_TRAITOR_BUG_PLANTED_OBJECT, PROC_REF(on_bug_planted))
RegisterSignal(bug, COMSIG_TRAITOR_BUG_PRE_PLANTED_OBJECT, PROC_REF(handle_special_case))
if("summon_gear")
if(!special_equipment)
return
for(var/item in special_equipment)
var/obj/item/new_item = new item(user.drop_location())
user.put_in_hands(new_item)
user.balloon_alert(user, "the equipment materializes in your hand")
special_equipment = null
if("cash_out")
if(!bug.planted_on)
return
succeed_objective()
/datum/traitor_objective/steal_item/process(seconds_per_tick)
var/mob/owner = handler.owner?.current
if(objective_state != OBJECTIVE_STATE_ACTIVE || !bug.planted_on)
return PROCESS_KILL
if(!owner)
fail_objective()
return PROCESS_KILL
if(get_dist(get_turf(owner), get_turf(bug)) > max_distance)
return
time_fulfilled += seconds_per_tick * (1 SECONDS)
if(time_fulfilled >= hold_time_required * (1 MINUTES))
progression_reward += extra_progression
telecrystal_reward += extra_tc
succeed_objective()
return PROCESS_KILL
handler.on_update()
/datum/traitor_objective/steal_item/proc/handle_special_case(obj/item/source, obj/item/target)
SIGNAL_HANDLER
if(HAS_TRAIT(target, TRAIT_ITEM_OBJECTIVE_BLOCKED))
return COMPONENT_FORCE_FAIL_PLACEMENT
if(istype(target, target_item.targetitem))
if(!target_item.check_special_completion(target))
return COMPONENT_FORCE_FAIL_PLACEMENT
return
var/found = FALSE
for(var/typepath in target_item.valid_containers)
if(istype(target, typepath))
found = TRUE
break
if(!found)
return
var/found_item = locate(target_item.targetitem) in target
if(!found_item || !target_item.check_special_completion(found_item))
return COMPONENT_FORCE_FAIL_PLACEMENT
return COMPONENT_FORCE_PLACEMENT
/datum/traitor_objective/steal_item/proc/on_bug_planted(obj/item/source, obj/item/location)
SIGNAL_HANDLER
if(objective_state == OBJECTIVE_STATE_ACTIVE)
START_PROCESSING(SSprocessing, src)
/obj/item/traitor_bug
name = "suspicious device"
desc = "It looks dangerous."
item_flags = NOBLUDGEON
icon = 'icons/obj/antags/syndicate_tools.dmi'
icon_state = "bug"
/// The object on which this bug can be planted on. Has to be a type.
var/obj/target_object_type
/// The object this bug is currently planted on.
var/obj/planted_on
/// The time it takes to place this bug.
var/deploy_time = 10 SECONDS
/obj/item/traitor_bug/Initialize(mapload)
. = ..()
ADD_TRAIT(src, TRAIT_EXAMINE_SKIP, INNATE_TRAIT)
/obj/item/traitor_bug/examine(mob/user)
. = ..()
if(planted_on)
return
if(IS_TRAITOR(user))
if(target_object_type)
. += span_notice("This device must be placed by <b>clicking on the [initial(target_object_type.name)]</b> with it.")
. += span_notice("Remember, you may leave behind fingerprints or fibers on the device. Use <b>soap</b> or similar to scrub it clean to be safe!")
/obj/item/traitor_bug/interact_with_atom(atom/movable/target, mob/living/user, list/modifiers)
if(!target_object_type || !ismovable(target))
return NONE
if(SHOULD_SKIP_INTERACTION(target, src, user))
return NONE
var/result = SEND_SIGNAL(src, COMSIG_TRAITOR_BUG_PRE_PLANTED_OBJECT, target)
if(!(result & COMPONENT_FORCE_PLACEMENT))
if(result & COMPONENT_FORCE_FAIL_PLACEMENT || !istype(target, target_object_type))
balloon_alert(user, "you can't attach this onto here!")
return ITEM_INTERACT_BLOCKING
if(!do_after(user, deploy_time, src, hidden = TRUE))
return ITEM_INTERACT_BLOCKING
if(planted_on)
return ITEM_INTERACT_BLOCKING
forceMove(target)
target.vis_contents += src
vis_flags |= VIS_INHERIT_PLANE
planted_on = target
RegisterSignal(planted_on, COMSIG_QDELETING, PROC_REF(handle_planted_on_deletion))
SEND_SIGNAL(src, COMSIG_TRAITOR_BUG_PLANTED_OBJECT, target)
return ITEM_INTERACT_SUCCESS
/obj/item/traitor_bug/proc/handle_planted_on_deletion()
planted_on = null
/obj/item/traitor_bug/Destroy()
if(planted_on)
vis_flags &= ~VIS_INHERIT_PLANE
planted_on.vis_contents -= src
return ..()
/obj/item/traitor_bug/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change = TRUE)
. = ..()
if(planted_on)
vis_flags &= ~VIS_INHERIT_PLANE
planted_on.vis_contents -= src
anchored = FALSE
UnregisterSignal(planted_on, COMSIG_QDELETING)
planted_on = null
@@ -1,264 +0,0 @@
/// A traitor objective. Traitor objectives should not be deleted after they have been created and established, only failed.
/// If a traitor objective needs to be removed from the failed/completed objective list of their handler, then you are doing something wrong
/// and you should reconsider. When an objective is failed/completed, that is final and the only way you can change that is by refactoring the code.
/datum/traitor_objective
/// The name of the traitor objective
var/name = "traitor objective"
/// The description of the traitor objective
var/description = "this is a traitor objective"
/// The uplink handler holder to give the progression and telecrystals to.
var/datum/uplink_handler/handler
/// The minimum required progression points for this objective
var/progression_minimum = null
/// The maximum progression before this objective cannot appear anymore
var/progression_maximum = INFINITY
/// The progression that is rewarded from completing this traitor objective. Can either be a list of list(min, max) or a direct value
var/progression_reward = 0 MINUTES
/// The telecrystals that are rewarded from completing this traitor objective. Can either be a list of list(min,max) or a direct value
var/telecrystal_reward = 0
/// TC penalty for failing an objective or cancelling it
var/telecrystal_penalty = 1
/// The time at which this objective was first created
var/time_of_creation = 0
/// The time at which this objective was completed
var/time_of_completion = 0
/// The current state of this objective
var/objective_state = OBJECTIVE_STATE_INACTIVE
/// Whether this objective was forced upon by an admin. Won't get autocleared by the traitor subsystem if progression surpasses an amount
var/forced = FALSE
/// Whether this objective was skipped by going from an inactive state to a failed state.
var/skipped = FALSE
/// Determines how influential global progression will affect this objective. Set to 0 to disable.
var/global_progression_influence_intensity = 0.1
/// Determines how great the deviance has to be before progression starts to get reduced.
var/global_progression_deviance_required = 1
/// Determines the minimum and maximum progression this objective can be worth as a result of being influenced by global progression
/// Should only be smaller than or equal to 1
var/global_progression_limit_coeff = 0.6
/// The deviance coefficient used to determine the randomness of the progression rewards.
var/progression_cost_coeff_deviance = 0.05
/// This gets added onto the coeff when calculating the updated progression cost. Used for variability and a slight bit of randomness
var/progression_cost_coeff = 0
/// The percentage that this objective has been increased or decreased by as a result of progression. Used by the UI
var/original_progression = 0
/// Abstract type that won't be included as a possible objective
var/abstract_type = /datum/traitor_objective
/// The duplicate type that will be used to check for duplicates.
/// If undefined, this will either take from the abstract type or the type of the objective itself
var/duplicate_type = null
/// Used only in unit testing. Can be used to explicitly skip the progression_reward and telecrystal_reward check for non-abstract objectives.
/// Useful for final objectives as they don't need a reward.
var/needs_reward = TRUE
/// Returns a list of variables that can be changed by config, allows for balance through configuration.
/// It is not recommended to finetweak any values of objectives on your server.
/datum/traitor_objective/proc/supported_configuration_changes()
return list(
NAMEOF(src, global_progression_influence_intensity),
NAMEOF(src, global_progression_deviance_required),
NAMEOF(src, global_progression_limit_coeff)
)
/// Replaces a word in the name of the proc. Also does it for the description
/datum/traitor_objective/proc/replace_in_name(replace, word)
name = replacetext(name, replace, word)
description = replacetext(description, replace, word)
/datum/traitor_objective/New(datum/uplink_handler/handler)
. = ..()
src.handler = handler
src.time_of_creation = world.time
apply_configuration()
if(SStraitor.generate_objectives)
if(islist(telecrystal_reward))
telecrystal_reward = rand(telecrystal_reward[1], telecrystal_reward[2])
if(islist(progression_reward))
progression_reward = rand(progression_reward[1], progression_reward[2])
else
if(!islist(telecrystal_reward))
telecrystal_reward = list(telecrystal_reward, telecrystal_reward)
if(!islist(progression_reward))
progression_reward = list(progression_reward, progression_reward)
progression_cost_coeff = (rand()*2 - 1) * progression_cost_coeff_deviance
/datum/traitor_objective/proc/apply_configuration()
if(!length(SStraitor.configuration_data))
return
var/datum/traitor_objective/current_type = type
var/list/types = list()
while(current_type != /datum/traitor_objective)
types += current_type
current_type = type2parent(current_type)
types += /datum/traitor_objective
// Reverse the list direction
reverse_range(types)
var/list/supported_configurations = supported_configuration_changes()
for(var/typepath in types)
if(!(typepath in SStraitor.configuration_data))
continue
var/list/changes = SStraitor.configuration_data[typepath]
for(var/variable in changes)
if(!(variable in supported_configurations))
continue
vars[variable] = changes[variable]
/// Updates the progression reward, scaling it depending on their current progression compared against the global progression
/datum/traitor_objective/proc/update_progression_reward()
if(!SStraitor.generate_objectives)
return
progression_reward = original_progression
if(global_progression_influence_intensity <= 0)
return
var/minimum_progression = progression_reward * global_progression_limit_coeff
var/maximum_progression = progression_reward * (2-global_progression_limit_coeff)
var/deviance = (SStraitor.current_global_progression - handler.progression_points) / SStraitor.progression_scaling_deviance
if(abs(deviance) < global_progression_deviance_required)
return
if(abs(deviance) == deviance) // If it is positive
deviance = deviance - global_progression_deviance_required
else
deviance = deviance + global_progression_deviance_required
var/coeff = NUM_E ** (global_progression_influence_intensity * abs(deviance)) - 1
if(abs(deviance) != deviance)
coeff *= -1
// This has less of an effect as the coeff gets nearer to -1. Is linear
coeff += progression_cost_coeff * min(max(1 - abs(coeff), 1), 0)
progression_reward = clamp(
progression_reward + progression_reward * coeff,
minimum_progression,
maximum_progression
)
/datum/traitor_objective/Destroy(force)
handler = null
return ..()
/// Called whenever the objective is about to be generated. Bypassed by forcefully adding objectives.
/// Returning false or true will do the same as the generate_objective proc.
/datum/traitor_objective/proc/can_generate_objective(datum/mind/generating_for, list/possible_duplicates)
return TRUE
/// Called when the objective should be generated. Should return if the objective has been successfully generated.
/// If false is returned, the objective will be removed as a potential objective for the traitor it is being generated for.
/// This is only temporary, it will run the proc again when objectives are generated for the traitor again.
/datum/traitor_objective/proc/generate_objective(datum/mind/generating_for, list/possible_duplicates)
return FALSE
/// Used to clean up signals and stop listening to states.
/datum/traitor_objective/proc/ungenerate_objective()
return
/datum/traitor_objective/proc/get_log_data()
return list(
"type" = type,
"owner" = handler.owner.key,
"name" = name,
"description" = description,
"telecrystal_reward" = telecrystal_reward,
"progression_reward" = progression_reward,
"original_progression" = original_progression,
"objective_state" = objective_state,
"forced" = forced,
"time_of_creation" = time_of_creation,
)
/// Converts the type into a useful debug string to be used for logging and debug display.
/datum/traitor_objective/proc/to_debug_string()
return "[type] (Name: [name], TC: [telecrystal_reward], Progression: [progression_reward], Time of creation: [time_of_creation])"
/datum/traitor_objective/proc/save_objective()
SSblackbox.record_feedback("associative", "traitor_objective", 1, get_log_data())
/// Used to handle cleaning up the objective.
/datum/traitor_objective/proc/handle_cleanup()
time_of_completion = world.time
ungenerate_objective()
if(objective_state == OBJECTIVE_STATE_INACTIVE)
skipped = TRUE
handler.complete_objective(src) // Remove this objective immediately, no reason to keep it around. It isn't even active
/// Used to fail objectives. Players can clear completed objectives in the UI
/datum/traitor_objective/proc/fail_objective(penalty_cost = 0, trigger_update = TRUE)
// Don't let players succeed already succeeded/failed objectives
if(objective_state != OBJECTIVE_STATE_INACTIVE && objective_state != OBJECTIVE_STATE_ACTIVE)
return
SEND_SIGNAL(src, COMSIG_TRAITOR_OBJECTIVE_FAILED)
handle_cleanup()
log_traitor("[key_name(handler.owner)] [objective_state == OBJECTIVE_STATE_INACTIVE? "missed" : "failed"] [to_debug_string()]")
if(penalty_cost)
handler.add_telecrystals(-penalty_cost)
objective_state = OBJECTIVE_STATE_FAILED
else
objective_state = OBJECTIVE_STATE_INVALID
save_objective()
if(trigger_update)
handler.on_update() // Trigger an update to the UI
/// Used to succeed objectives. Allows the player to cash it out in the UI.
/datum/traitor_objective/proc/succeed_objective()
// Don't let players succeed already succeeded/failed objectives
if(objective_state != OBJECTIVE_STATE_INACTIVE && objective_state != OBJECTIVE_STATE_ACTIVE)
return
SEND_SIGNAL(src, COMSIG_TRAITOR_OBJECTIVE_COMPLETED)
SEND_GLOBAL_SIGNAL(COMSIG_GLOB_TRAITOR_OBJECTIVE_COMPLETED, src)
handle_cleanup()
log_traitor("[key_name(handler.owner)] [objective_state == OBJECTIVE_STATE_INACTIVE? "missed" : "completed"] [to_debug_string()]")
objective_state = OBJECTIVE_STATE_COMPLETED
save_objective()
handler.on_update() // Trigger an update to the UI
/// Called by player input, do not call directly. Validates whether the objective is finished and pays out the handler if it is.
/datum/traitor_objective/proc/finish_objective(mob/user)
switch(objective_state)
if(OBJECTIVE_STATE_FAILED, OBJECTIVE_STATE_INVALID)
user.playsound_local(get_turf(user), 'sound/music/antag/traitor/objective_failed.ogg', vol = 100, vary = FALSE, channel = CHANNEL_TRAITOR)
return TRUE
if(OBJECTIVE_STATE_COMPLETED)
user.playsound_local(get_turf(user), 'sound/music/antag/traitor/objective_success.ogg', vol = 100, vary = FALSE, channel = CHANNEL_TRAITOR)
completion_payout()
return TRUE
return FALSE
/// Called when rewards should be given to the user.
/datum/traitor_objective/proc/completion_payout()
handler.progression_points += progression_reward
handler.add_telecrystals(telecrystal_reward)
/// Used for sending data to the uplink UI
/datum/traitor_objective/proc/uplink_ui_data(mob/user)
return list(
"name" = name,
"description" = description,
"progression_minimum" = progression_minimum,
"progression_reward" = progression_reward,
"telecrystal_reward" = telecrystal_reward,
"ui_buttons" = generate_ui_buttons(user),
"objective_state" = objective_state,
"original_progression" = original_progression,
"telecrystal_penalty" = telecrystal_penalty,
)
/datum/traitor_objective/proc/on_objective_taken(mob/user)
SStraitor.on_objective_taken(src)
log_traitor("[key_name(handler.owner)] has taken an objective: [to_debug_string()]")
/// Used for generating the UI buttons for the UI. Use ui_perform_action to respond to clicks.
/datum/traitor_objective/proc/generate_ui_buttons(mob/user)
return
/datum/traitor_objective/proc/add_ui_button(name, tooltip, icon, action)
return list(list(
"name" = name,
"tooltip" = tooltip,
"icon" = icon,
"action" = action,
))
/// Return TRUE to trigger a UI update
/datum/traitor_objective/proc/ui_perform_action(mob/user, action)
return TRUE
@@ -20,24 +20,6 @@
var/list/item_stock = list(UPLINK_SHARED_STOCK_KITS = 1 , UPLINK_SHARED_STOCK_SURPLUS = 1)
/// Extra stuff that can be purchased by an uplink, regardless of flag.
var/list/extra_purchasable = list()
/// Whether this uplink handler has objectives.
var/has_objectives = TRUE
/// Whether this uplink handler can TAKE objectives.
var/can_take_objectives = TRUE
/// The maximum number of objectives that can be taken
var/maximum_active_objectives = 2
/// The maximum number of potential objectives that can exist.
var/maximum_potential_objectives = 6
/// Current objectives taken
var/list/active_objectives = list()
/// Potential objectives that can be taken
var/list/potential_objectives = list()
/// Objectives that have been completed.
var/list/completed_objectives = list()
/// All objectives assigned by type to handle any duplicates
var/list/potential_duplicate_objectives = list()
/// Text of the final objective, once assigned. Used for uplink data and traitor greentext. Empty string means not yet reached.
var/final_objective = ""
/// Objectives that must be completed for traitor greentext. Set by the traitor datum.
var/list/primary_objectives
/// The role that this uplink handler is associated to.
@@ -55,10 +37,6 @@
///Reference to a contractor hub that the infiltrator can run, if they purchase it.
var/datum/contractor_hub/contractor_hub
/datum/uplink_handler/New()
. = ..()
maximum_potential_objectives = CONFIG_GET(number/maximum_potential_objectives)
/datum/uplink_handler/Destroy(force)
can_replace_objectives = null
replace_objectives = null
@@ -140,136 +118,6 @@
on_update()
return TRUE
/// Generates objectives for this uplink handler
/datum/uplink_handler/proc/generate_objectives()
var/potential_objectives_left = maximum_potential_objectives - (length(potential_objectives) + length(active_objectives))
var/list/objectives = SStraitor.category_handler.get_possible_objectives(progression_points)
if(!length(objectives))
return
while(length(objectives) && potential_objectives_left > 0)
var/objective_typepath = pick_weight(objectives)
var/list/target_list = objectives
while(islist(objective_typepath))
if(!length(objective_typepath))
// Need to wrap this in a list or else it list unrolls and the list doesn't actually get removed.
// Thank you byond, very cool!
target_list -= list(objective_typepath)
break
target_list = objective_typepath
objective_typepath = pick_weight(objective_typepath)
if(islist(objective_typepath) || !objective_typepath)
continue
if(!try_add_objective(objective_typepath))
target_list -= objective_typepath
continue
potential_objectives_left--
on_update()
/datum/uplink_handler/proc/try_add_objective(datum/traitor_objective/objective_typepath, force = FALSE)
var/datum/traitor_objective/objective = new objective_typepath(src)
var/duplicate_typepath = objective.duplicate_type
if(!duplicate_typepath)
if(objective.abstract_type != /datum/traitor_objective)
duplicate_typepath = objective.abstract_type
else
duplicate_typepath = objective_typepath
if(!force && !objective.can_generate_objective(owner, potential_duplicate_objectives[duplicate_typepath]))
qdel(objective)
return
var/should_abort = SEND_SIGNAL(objective, COMSIG_TRAITOR_OBJECTIVE_PRE_GENERATE, owner, potential_duplicate_objectives[duplicate_typepath]) & COMPONENT_TRAITOR_OBJECTIVE_ABORT_GENERATION
if(should_abort || !objective.generate_objective(owner, potential_duplicate_objectives[duplicate_typepath]))
qdel(objective)
return
if(!handle_duplicate(objective))
qdel(objective)
return
objective.forced = force
log_traitor("[key_name(owner)] has received a potential objective: [objective.to_debug_string()] | Forced: [force]")
objective.original_progression = objective.progression_reward
objective.update_progression_reward()
potential_objectives += objective
SStraitor.add_objective_to_list(objective, SStraitor.all_objectives_by_type)
return objective
/datum/uplink_handler/proc/handle_duplicate(datum/traitor_objective/potential_duplicate)
if(!istype(potential_duplicate))
return FALSE
var/datum/traitor_objective/current_type = potential_duplicate.type
var/list/added_types = list()
while(current_type != /datum/traitor_objective)
if(!potential_duplicate_objectives[current_type])
potential_duplicate_objectives[current_type] = list(potential_duplicate)
else
potential_duplicate_objectives[current_type] += potential_duplicate
added_types += current_type
current_type = type2parent(current_type)
return TRUE
/datum/uplink_handler/proc/get_completion_count(datum/traitor_objective/type)
var/amount_completed = 0
for(var/datum/traitor_objective/objective as anything in potential_duplicate_objectives[type])
if(objective.objective_state == OBJECTIVE_STATE_COMPLETED)
amount_completed += 1
return amount_completed
/datum/uplink_handler/proc/get_completion_progression(datum/traitor_objective/type)
var/total_progression = 0
for(var/datum/traitor_objective/objective as anything in completed_objectives)
if(objective.objective_state == OBJECTIVE_STATE_COMPLETED)
total_progression += objective.progression_reward
return total_progression
/// Used to complete objectives, failed or successful.
/datum/uplink_handler/proc/complete_objective(datum/traitor_objective/to_remove)
if(to_remove in completed_objectives)
return
potential_objectives -= to_remove
active_objectives -= to_remove
completed_objectives += to_remove
update_objectives()
generate_objectives()
/// Updates the objectives on the uplink and deletes
/datum/uplink_handler/proc/update_objectives()
var/list/objectives_copy = potential_objectives + active_objectives
for(var/datum/traitor_objective/objective as anything in objectives_copy)
if(progression_points > objective.progression_maximum && !objective.forced && objective.objective_state != OBJECTIVE_STATE_ACTIVE)
objective.fail_objective(trigger_update = FALSE)
continue
objective.update_progression_reward()
/datum/uplink_handler/proc/abort_objective(datum/traitor_objective/to_abort)
if(istype(to_abort, /datum/traitor_objective/ultimate))
return
if(to_abort.objective_state != OBJECTIVE_STATE_ACTIVE)
return
to_abort.fail_objective(penalty_cost = to_abort.telecrystal_penalty)
/datum/uplink_handler/proc/take_objective(mob/user, datum/traitor_objective/to_take)
if(!(to_take in potential_objectives))
return
user.playsound_local(get_turf(user), 'sound/music/antag/traitor/objective_taken.ogg', vol = 100, vary = FALSE, channel = CHANNEL_TRAITOR)
to_take.on_objective_taken(user)
to_take.objective_state = OBJECTIVE_STATE_ACTIVE
potential_objectives -= to_take
active_objectives += to_take
on_update()
/datum/uplink_handler/proc/ui_objective_act(mob/user, datum/traitor_objective/to_act_on, action)
if(!(to_act_on in active_objectives))
return
if(to_act_on.objective_state != OBJECTIVE_STATE_ACTIVE)
return
to_act_on.ui_perform_action(user, action)
///Helper to add telecrystals to the uplink handler, calling set_telecrystals.
/datum/uplink_handler/proc/add_telecrystals(amount)
set_telecrystals(telecrystals + amount)
-1
View File
@@ -166,7 +166,6 @@
"Nuke Op" = UPLINK_NUKE_OPS,
"Clown Op" = UPLINK_CLOWN_OPS,
"Lone Op" = UPLINK_LONE_OP,
"Infiltrator" = UPLINK_INFILTRATORS,
"Spy" = UPLINK_SPY
)
var/uplink_type = tgui_input_list(usr, "Choose uplink to draw items from.", "Choose uplink type.", possible_uplinks)
@@ -0,0 +1,103 @@
/obj/item/eyesnatcher
name = "portable eyeball extractor"
desc = "An overly complicated device that can pierce target's skull and extract their eyeballs if enough brute force is applied."
icon = 'icons/obj/medical/surgery_tools.dmi'
icon_state = "eyesnatcher"
base_icon_state = "eyesnatcher"
inhand_icon_state = "hypo"
lefthand_file = 'icons/mob/inhands/equipment/medical_lefthand.dmi'
righthand_file = 'icons/mob/inhands/equipment/medical_righthand.dmi'
throwforce = 0
w_class = WEIGHT_CLASS_SMALL
throw_speed = 3
throw_range = 5
///Whether it's been used to steal a pair of eyes already.
var/used = FALSE
/obj/item/eyesnatcher/update_icon_state()
. = ..()
icon_state = "[base_icon_state][used ? "-used" : ""]"
/obj/item/eyesnatcher/attack(mob/living/carbon/human/target, mob/living/user, params)
if(used || !istype(target) || !target.Adjacent(user)) //Works only once, no TK use
return ..()
var/obj/item/organ/eyes/eyeballies = target.get_organ_slot(ORGAN_SLOT_EYES)
var/obj/item/bodypart/head/head = target.get_bodypart(BODY_ZONE_HEAD)
if(!head || !eyeballies || target.is_eyes_covered())
return ..()
var/eye_snatch_enthusiasm = 5 SECONDS
if(HAS_MIND_TRAIT(user, TRAIT_MORBID))
eye_snatch_enthusiasm *= 0.7
user.do_attack_animation(target, used_item = src)
target.visible_message(
span_warning("[user] presses [src] against [target]'s skull!"),
span_userdanger("[user] presses [src] against your skull!"))
if(!do_after(user, eye_snatch_enthusiasm, target = target, extra_checks = CALLBACK(src, PROC_REF(eyeballs_exist), eyeballies, head, target)))
return
to_chat(target, span_userdanger("You feel something forcing its way into your skull!"))
balloon_alert(user, "applying pressure...")
if(!do_after(user, eye_snatch_enthusiasm, target = target, extra_checks = CALLBACK(src, PROC_REF(eyeballs_exist), eyeballies, head, target)))
return
var/min_wound = head.get_wound_threshold_of_wound_type(WOUND_BLUNT, WOUND_SEVERITY_SEVERE, return_value_if_no_wound = 30, wound_source = src)
var/max_wound = head.get_wound_threshold_of_wound_type(WOUND_BLUNT, WOUND_SEVERITY_CRITICAL, return_value_if_no_wound = 50, wound_source = src)
target.apply_damage(20, BRUTE, BODY_ZONE_HEAD, wound_bonus = rand(min_wound, max_wound + 10), attacking_item = src)
target.visible_message(
span_danger("[src] pierces through [target]'s skull, horribly mutilating their eyes!"),
span_userdanger("Something penetrates your skull, horribly mutilating your eyes! Holy fuck!"),
span_hear("You hear a sickening sound of metal piercing flesh!")
)
eyeballies.apply_organ_damage(eyeballies.maxHealth)
target.emote("scream")
playsound(target, 'sound/effects/wounds/crackandbleed.ogg', 100)
log_combat(user, target, "cracked the skull of (eye snatching)", src)
if(!do_after(user, eye_snatch_enthusiasm, target = target, extra_checks = CALLBACK(src, PROC_REF(eyeballs_exist), eyeballies, head, target)))
return
if(!target.is_blind())
to_chat(target, span_userdanger("You suddenly go blind!"))
if(prob(1))
to_chat(target, span_notice("At least you got a new pirate-y look out of it..."))
var/obj/item/clothing/glasses/eyepatch/new_patch = new(target.loc)
target.equip_to_slot_if_possible(new_patch, ITEM_SLOT_EYES, disable_warning = TRUE)
to_chat(user, span_notice("You successfully extract [target]'s eyeballs."))
playsound(target, 'sound/items/handling/surgery/retractor2.ogg', 100, TRUE)
playsound(target, 'sound/effects/pop.ogg', 100, TRAIT_MUTE)
eyeballies.Remove(target)
eyeballies.forceMove(get_turf(target))
notify_ghosts(
"[target] has just had their eyes snatched!",
source = target,
header = "Ouch!",
)
target.emote("scream")
if(prob(20))
target.emote("cry")
used = TRUE
update_appearance(UPDATE_ICON)
/obj/item/eyesnatcher/examine(mob/user)
. = ..()
if(used)
. += span_notice("It has been used up.")
/obj/item/eyesnatcher/proc/eyeballs_exist(obj/item/organ/eyes/eyeballies, obj/item/bodypart/head/head, mob/living/carbon/human/target)
if(!eyeballies || QDELETED(eyeballies))
return FALSE
if(!head || QDELETED(head))
return FALSE
if(eyeballies.owner != target)
return FALSE
var/obj/item/organ/eyes/eyes = target.get_organ_slot(ORGAN_SLOT_EYES)
//got different eyes or doesn't own the head... somehow
if(head.owner != target || eyes != eyeballies)
return FALSE
return TRUE
@@ -147,11 +147,8 @@
target_mind = pick(backup_players)
hidden_uplink = target.AddComponent(/datum/component/uplink, target_mind, enabled = TRUE, starting_tc = telecrystals, has_progression = TRUE)
hidden_uplink.unlock_code = unlock_code
hidden_uplink.uplink_handler.has_objectives = TRUE
hidden_uplink.uplink_handler.owner = target_mind
hidden_uplink.uplink_handler.can_take_objectives = FALSE
hidden_uplink.uplink_handler.progression_points = min(SStraitor.current_global_progression, current_progression)
hidden_uplink.uplink_handler.generate_objectives()
SStraitor.register_uplink_handler(hidden_uplink.uplink_handler)
else
hidden_uplink.uplink_handler.add_telecrystals(telecrystals)
+6 -3
View File
@@ -113,9 +113,12 @@
/obj/item/papercutter/attackby(obj/item/inserted_item, mob/user, params)
if(istype(inserted_item, /obj/item/paper))
if(is_type_in_list(inserted_item, list(
/obj/item/paper/paperslip, /obj/item/paper/report, /obj/item/paper/fake_report,
/obj/item/paper/calling_card, /obj/item/paper/pamphlet, /obj/item/paper/holy_writ)
))
/obj/item/paper/fake_report,
/obj/item/paper/holy_writ,
/obj/item/paper/pamphlet,
/obj/item/paper/paperslip,
/obj/item/paper/report,
)))
balloon_alert(user, "won't fit!")
return
if(stored_paper)
@@ -145,8 +145,6 @@
to_chat(user, span_warning("The last satellite emagged needs [DisplayTimeText(COOLDOWN_TIMELEFT(src, shared_emag_cooldown))] to recalibrate first. Emagging another so soon could damage the satellite network."))
return FALSE
var/cooldown_applied = METEOR_SHIELD_EMAG_COOLDOWN
if(istype(emag_card, /obj/item/card/emag/meteor_shield_recalibrator))
cooldown_applied /= 3
COOLDOWN_START(src, shared_emag_cooldown, cooldown_applied)
obj_flags |= EMAGGED
to_chat(user, span_notice("You access the satellite's debug mode and it begins emitting a strange signal, increasing the chance of meteor strikes."))
-1
View File
@@ -225,7 +225,6 @@
#include "mutant_organs.dm"
#include "novaflower_burn.dm"
#include "nuke_cinematic.dm"
#include "objectives.dm"
#include "omnitools.dm"
#include "operating_table.dm"
#include "orderable_items.dm"
-49
View File
@@ -1,49 +0,0 @@
/datum/unit_test/objectives_category/Run()
var/datum/traitor_category_handler/category_handler = allocate(/datum/traitor_category_handler)
var/list/objectives_that_exist = list()
for(var/datum/traitor_objective_category/category as anything in category_handler.all_categories)
for(var/value in category.objectives)
TEST_ASSERT(isnum(category.objectives[value]), "[category.type] does not have a valid format for its objectives as an objective category! ([value] requires a weight to be assigned to it)")
if(islist(value))
recursive_check_list(category.type, value, objectives_that_exist)
else
objectives_that_exist += value
SStraitor.generate_objectives = FALSE
for(var/datum/traitor_objective/objective_typepath as anything in subtypesof(/datum/traitor_objective))
var/datum/traitor_objective/objective = allocate(objective_typepath)
if(objective.abstract_type == objective_typepath)
// In this case, we don't want abstract types to define values that should be defined on non-abstract types
// Nor do we want abstract types to appear in the pool of traitor objectives.
if(objective_typepath in objectives_that_exist)
TEST_FAIL("[objective_typepath] is in a traitor category and is an abstract type! Please remove it from the [/datum/traitor_objective_category].")
// Since we didn't generate the objective, the rewards are going to be in list form: (min, max)
if(!reward_is_zero(objective.progression_reward))
TEST_FAIL("[objective_typepath] has set a progression reward as an abstract type! Please define progression rewards on non-abstract types rather than abstract types.")
// Since we didn't generate the objective, the rewards are going to be in list form: (min, max)
if(!reward_is_zero(objective.telecrystal_reward))
TEST_FAIL("[objective_typepath] has set a telecrystal reward as an abstract type! Please define telecrystal rewards on non-abstract types rather than abstract types.")
continue
if(!(objective_typepath in objectives_that_exist))
TEST_FAIL("[objective_typepath] is not in a traitor category and isn't an abstract type! Place it into a [/datum/traitor_objective_category] or remove it from code.")
if(objective.progression_minimum == null)
TEST_FAIL("[objective_typepath] has not defined a minimum progression level and isn't an abstract type! Please define the progression minimum variable on the datum")
if(objective.needs_reward && reward_is_zero(objective.progression_reward) && reward_is_zero(objective.telecrystal_reward))
TEST_FAIL("[objective_typepath] has not set either a progression reward or a telecrystal reward! Please set either a telecrystal or progression reward for this objective.")
/// Returns whether the reward specified (in format (min, max)) is zero or not.
/datum/unit_test/objectives_category/proc/reward_is_zero(list/reward)
return (reward[1] == 0 && reward[2] == 0)
/datum/unit_test/objectives_category/Destroy()
SStraitor.generate_objectives = TRUE
return ..()
/datum/unit_test/objectives_category/proc/recursive_check_list(base_type, list/to_check, list/to_add_to)
for(var/value in to_check)
TEST_ASSERT(isnum(to_check[value]), "[base_type] does not have a valid format for its objectives as an objective category! ([value] requires a weight to be assigned to it)")
if(islist(value))
recursive_check_list(base_type, value, to_add_to)
else
to_add_to += value
-8
View File
@@ -22,11 +22,3 @@
var/datum/antagonist/traitor/traitor = mind.add_antag_datum(/datum/antagonist/traitor)
if(!traitor.uplink_handler)
TEST_FAIL("[job_name] when made traitor does not have a proper uplink created when spawned in!")
for(var/datum/traitor_objective/objective_typepath as anything in subtypesof(/datum/traitor_objective))
if(initial(objective_typepath.abstract_type) == objective_typepath)
continue
var/datum/traitor_objective/objective = allocate(objective_typepath, traitor.uplink_handler)
try
objective.generate_objective(mind, list())
catch(var/exception/exception)
TEST_FAIL("[objective_typepath] failed to generate their objective. Reason: [exception.name] [exception.file]:[exception.line]\n[exception.desc]")
-28
View File
@@ -84,34 +84,6 @@
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, datum/uplink_handler/uplink_handler_override = null) //ORIGINAL
/obj/item/multitool/uplink/Initialize(mapload, owner, tc_amount = 35, datum/uplink_handler/uplink_handler_override = null) //SKYRAT EDIT CHANGE. Original tc_amount = 20
@@ -13,7 +13,7 @@
item = /obj/item/storage/box/syndicate/contract_kit
category = /datum/uplink_category/contractor
cost = 20
purchasable_from = UPLINK_INFILTRATORS
purchasable_from = UPLINK_TRAITORS
/datum/uplink_item/bundles_tc/contract_kit/purchase(mob/user, datum/uplink_handler/uplink_handler, atom/movable/source)
. = ..()
+2 -1
View File
@@ -604,7 +604,8 @@
along with slurred speech, aggression, and the ability to infect others with this agent."
item = /obj/item/storage/box/syndie_kit/romerol
cost = 25
purchasable_from = UPLINK_ALL_SYNDIE_OPS
progression_minimum = 30 MINUTES
purchasable_from = UPLINK_ALL_SYNDIE_OPS | UPLINK_TRAITORS // Don't give this to spies
cant_discount = TRUE
// Modsuits
+1 -28
View File
@@ -2923,7 +2923,6 @@
#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"
@@ -3494,40 +3493,13 @@
#include "code\modules\antagonists\spy\spy_uplink.dm"
#include "code\modules\antagonists\survivalist\survivalist.dm"
#include "code\modules\antagonists\syndicate_monkey\syndicate_monkey.dm"
#include "code\modules\antagonists\traitor\balance_helper.dm"
#include "code\modules\antagonists\traitor\datum_traitor.dm"
#include "code\modules\antagonists\traitor\objective_category.dm"
#include "code\modules\antagonists\traitor\traitor_objective.dm"
#include "code\modules\antagonists\traitor\uplink_handler.dm"
#include "code\modules\antagonists\traitor\components\demoraliser.dm"
#include "code\modules\antagonists\traitor\components\traitor_objective_helpers.dm"
#include "code\modules\antagonists\traitor\components\traitor_objective_limit_per_time.dm"
#include "code\modules\antagonists\traitor\components\traitor_objective_mind_tracker.dm"
#include "code\modules\antagonists\traitor\contractor\contract_teammate.dm"
#include "code\modules\antagonists\traitor\contractor\contractor_hub.dm"
#include "code\modules\antagonists\traitor\contractor\contractor_items.dm"
#include "code\modules\antagonists\traitor\contractor\syndicate_contract.dm"
#include "code\modules\antagonists\traitor\objectives\assassination.dm"
#include "code\modules\antagonists\traitor\objectives\demoralise_assault.dm"
#include "code\modules\antagonists\traitor\objectives\destroy_heirloom.dm"
#include "code\modules\antagonists\traitor\objectives\destroy_item.dm"
#include "code\modules\antagonists\traitor\objectives\eyesnatching.dm"
#include "code\modules\antagonists\traitor\objectives\hack_comm_console.dm"
#include "code\modules\antagonists\traitor\objectives\infect.dm"
#include "code\modules\antagonists\traitor\objectives\kidnapping.dm"
#include "code\modules\antagonists\traitor\objectives\kill_pet.dm"
#include "code\modules\antagonists\traitor\objectives\locate_weakpoint.dm"
#include "code\modules\antagonists\traitor\objectives\sabotage_machinery.dm"
#include "code\modules\antagonists\traitor\objectives\steal.dm"
#include "code\modules\antagonists\traitor\objectives\abstract\target_player.dm"
#include "code\modules\antagonists\traitor\objectives\final_objective\battle_royale.dm"
#include "code\modules\antagonists\traitor\objectives\final_objective\battlecruiser.dm"
#include "code\modules\antagonists\traitor\objectives\final_objective\final_objective.dm"
#include "code\modules\antagonists\traitor\objectives\final_objective\infect_ai.dm"
#include "code\modules\antagonists\traitor\objectives\final_objective\no_escape.dm"
#include "code\modules\antagonists\traitor\objectives\final_objective\objective_dark_matteor.dm"
#include "code\modules\antagonists\traitor\objectives\final_objective\romerol.dm"
#include "code\modules\antagonists\traitor\objectives\final_objective\supermatter_cascade.dm"
#include "code\modules\antagonists\valentines\heartbreaker.dm"
#include "code\modules\antagonists\valentines\valentine.dm"
#include "code\modules\antagonists\venus_human_trap\venus_human_trap.dm"
@@ -4854,6 +4826,7 @@
#include "code\modules\mapfluff\ruins\spaceruin_code\derelict_sulaco.dm"
#include "code\modules\mapfluff\ruins\spaceruin_code\DJstation.dm"
#include "code\modules\mapfluff\ruins\spaceruin_code\forgottenship.dm"
#include "code\modules\mapfluff\ruins\spaceruin_code\garbagetruck.dm"
#include "code\modules\mapfluff\ruins\spaceruin_code\hauntedtradingpost.dm"
#include "code\modules\mapfluff\ruins\spaceruin_code\hellfactory.dm"
#include "code\modules\mapfluff\ruins\spaceruin_code\hilbertshotel.dm"
@@ -1,10 +1,4 @@
import {
BlockQuote,
Button,
Dimmer,
Section,
Stack,
} from 'tgui-core/components';
import { BlockQuote, Button, Section, Stack } from 'tgui-core/components';
import { BooleanLike } from 'tgui-core/react';
import { useBackend } from '../backend';
@@ -37,8 +31,6 @@ 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;
@@ -117,37 +109,12 @@ const EmployerSection = (props) => {
const UplinkSection = (props) => {
const { data } = useBackend<Info>();
const {
has_uplink,
uplink_intro,
uplink_unlock_info,
code,
failsafe_code,
replacement_code,
replacement_frequency,
} = data;
const { has_uplink, uplink_intro, uplink_unlock_info, code, failsafe_code } =
data;
return (
<Section title="Uplink" mb={!has_uplink && -1}>
<Stack fill>
{(!has_uplink && (
<Dimmer>
<Stack.Item fontSize="16px">
<Section textAlign="Center">
Your uplink is missing or destroyed. <br />
Craft a Syndicate Uplink Beacon and then speak
<br />
<span style={goalstyle}>
<b>{replacement_code}</b>
</span>{' '}
on frequency{' '}
<span style={goalstyle}>
<b>{replacement_frequency}</b>
</span>{' '}
after synchronizing with the beacon.
</Section>
</Stack.Item>
</Dimmer>
)) || (
{
<>
<Stack.Item bold>
{uplink_intro}
@@ -163,29 +130,16 @@ const UplinkSection = (props) => {
<BlockQuote>{uplink_unlock_info}</BlockQuote>
</Stack.Item>
</>
)}
}
</Stack>
<br />
{(has_uplink && (
<Section textAlign="Center">
If you lose your uplink, you can craft a Syndicate Uplink Beacon and
then speak{' '}
<span style={goalstyle}>
<b>{replacement_code}</b>
</span>{' '}
on radio frequency{' '}
<span style={goalstyle}>
<b>{replacement_frequency}</b>
</span>{' '}
after synchronizing with the beacon.
</Section>
)) || (
{
<Section>
{' '}
<br />
<br />
</Section>
)}
}
</Section>
);
};
@@ -1,428 +0,0 @@
import { useState } from 'react';
import { Box, LabeledList, Stack, Tabs, Tooltip } from 'tgui-core/components';
import { useBackend } from '../backend';
import { Window } from '../layouts';
import { getDangerLevel } from './Uplink/calculateDangerLevel';
type Objective = {
name: string;
description: string;
progression_minimum: number;
progression_maximum: number;
global_progression_limit_coeff: number;
global_progression_influence_intensity: number;
progression_reward: [number, number];
telecrystal_reward: [number, number];
telecrystal_penalty: number;
weight: number;
type: string;
};
type ObjectiveList = {
objectives: (ObjectiveList | Objective)[];
weight: number;
};
type ObjectiveCategory = ObjectiveList & {
name: string;
};
type PlayerData = {
player: string;
progression_points: number;
total_progression_from_objectives: number;
};
type ObjectiveData = {
current_progression: number;
objective_data: ObjectiveCategory[];
player_data: PlayerData[];
};
const recursivelyGetObjectives = (value: ObjectiveList) => {
let listToReturn: Objective[] = [];
for (let i = 0; i < value.objectives.length; i++) {
const possibleValue = value.objectives[i];
if ((possibleValue as ObjectiveList).objectives) {
listToReturn = listToReturn.concat(
recursivelyGetObjectives(possibleValue as ObjectiveList),
);
} else {
listToReturn.push(possibleValue as Objective);
}
}
return listToReturn;
};
// 150 minutes
const sizeLimit = 90000;
type SortingOption = {
name: string;
// Function used to determine the order of the elements.
// It is expected to return a negative value
// if first argument is less than second argument,
// zero if they're equal and a positive value otherwise.
sort: (a: Objective, b: Objective) => number;
};
const sortingOptions: SortingOption[] = [
{
name: 'Minimum Progression',
sort: (a, b) => {
if (a.progression_minimum < b.progression_minimum) {
return -1;
} else if (a.progression_minimum === b.progression_minimum) {
return 0;
}
return 1;
},
},
{
name: 'Telecrystal Payout',
sort: (a, b) => {
const telecrystalMeanA =
(a.telecrystal_reward[0] + a.telecrystal_reward[1]) / 2;
const telecrystalMeanB =
(b.telecrystal_reward[0] + b.telecrystal_reward[1]) / 2;
if (telecrystalMeanA < telecrystalMeanB) {
return -1;
} else if (telecrystalMeanA === telecrystalMeanB) {
return 0;
}
return 1;
},
},
{
name: 'Progression Payout',
sort: (a, b) => {
const progressionMeanA =
(a.progression_reward[0] + a.progression_reward[1]) / 2;
const progressionMeanB =
(b.progression_reward[0] + b.progression_reward[1]) / 2;
if (progressionMeanA < progressionMeanB) {
return -1;
} else if (progressionMeanA === progressionMeanB) {
return 0;
}
return 1;
},
},
{
name: 'Progression Payout + Min. Prog.',
sort: (a, b) => {
const progressionMeanA =
(a.progression_reward[0] + a.progression_reward[1]) / 2 +
a.progression_minimum;
const progressionMeanB =
(b.progression_reward[0] + b.progression_reward[1]) / 2 +
b.progression_minimum;
if (progressionMeanA < progressionMeanB) {
return -1;
} else if (progressionMeanA === progressionMeanB) {
return 0;
}
return 1;
},
},
];
export const TraitorObjectiveDebug = (props) => {
const { data, act } = useBackend<ObjectiveData>();
const { objective_data, player_data, current_progression } = data;
const lines: JSX.Element[] = [];
lines.sort();
for (let i = 10; i < 100; i += 10) {
lines.push(
<Box
position="absolute"
height="100%"
top={0}
left={`${i}%`}
width="2px"
backgroundColor="green"
>
<Box
position="absolute"
top={0}
left={0}
width="2px"
backgroundColor="green"
height="5px"
style={{
zIndex: '5',
}}
/>
<Box
position="absolute"
top={0}
left={1}
style={{
zIndex: '5',
}}
>
{/* Time in minutes of this threshold */}
{Math.round((sizeLimit * (i / 100)) / 600)} mins
</Box>
</Box>,
);
}
let objectivesToRender: Objective[] = [];
const [currentTab, setCurrentTab] = useState('All');
const [sortingFunc, setSortingFunc] = useState(sortingOptions[0].name);
// true = ascending, false = descending
const [sortDirection, setSortingDirection] = useState(true);
let actualSortingFunc;
for (let index = 0; index < sortingOptions.length; index++) {
const value = sortingOptions[index];
if (value.name === sortingFunc) {
actualSortingFunc = value.sort;
}
}
for (let index = 0; index < objective_data.length; index++) {
const value = objective_data[index];
if (value.name !== currentTab && currentTab !== 'All') {
continue;
}
objectivesToRender = objectivesToRender.concat(
recursivelyGetObjectives(value),
);
}
objectivesToRender.sort(actualSortingFunc);
if (!sortDirection) {
objectivesToRender.reverse();
}
return (
<Window width={1000} height={1000} theme="admin">
<Window.Content scrollable>
<Box position="absolute" height="100px" width="100%" top={0} left={0}>
<Stack vertical>
<Stack.Item>
<Tabs width="100%" fluid textAlign="center">
{sortingOptions.map((value) => (
<Tabs.Tab
key={value.name}
selected={value.name === sortingFunc}
onClick={() => setSortingFunc(value.name)}
>
{value.name}
</Tabs.Tab>
))}
</Tabs>
</Stack.Item>
<Stack.Item>
<Tabs height="100%" width="100%" fluid textAlign="center">
<Tabs.Tab
selected={currentTab === 'All'}
onClick={() => setCurrentTab('All')}
>
All
</Tabs.Tab>
{objective_data.map((value) => (
<Tabs.Tab
key={value.name}
selected={value.name === currentTab}
onClick={() => setCurrentTab(value.name)}
>
{value.name}
</Tabs.Tab>
))}
</Tabs>
</Stack.Item>
<Stack.Item>
<Tabs width="100%" fluid textAlign="center">
<Tabs.Tab
selected={sortDirection === true}
onClick={() => setSortingDirection(true)}
>
Ascending
</Tabs.Tab>
<Tabs.Tab
selected={sortDirection === false}
onClick={() => setSortingDirection(false)}
>
Descending
</Tabs.Tab>
</Tabs>
</Stack.Item>
</Stack>
</Box>
<Box
position="absolute"
width="100%"
backgroundColor="black"
left={0}
top="100px"
>
{lines}
<Stack vertical mt={8}>
{objectivesToRender.map((value, index) => (
<Stack.Item key={index} position="relative" basis="100px">
<ObjectiveBox objective={value} />
</Stack.Item>
))}
</Stack>
{player_data.map((value) => {
const rep = getDangerLevel(value.progression_points);
return (
<Tooltip
key={value.player}
content={
<Box>
<LabeledList>
<LabeledList.Item label={'Key'}>
{value.player}
</LabeledList.Item>
<LabeledList.Item label={'Total PR'}>
{Math.floor(value.progression_points / 600)} mins
</LabeledList.Item>
<LabeledList.Item label={'Obj PR'}>
{Math.floor(
value.total_progression_from_objectives / 600,
)}{' '}
mins
</LabeledList.Item>
</LabeledList>
</Box>
}
position="top"
>
<Box
backgroundColor="red"
position="absolute"
left={`${
(value.progression_points / sizeLimit) * window.innerWidth
}px`}
width="3px"
height="100%"
top={0}
opacity={0.8}
>
<Box
position="absolute"
top={0}
left="-50px"
width="100px"
height="100%"
/>
</Box>
</Tooltip>
);
})}
<Tooltip
content={`Expected Progression: ${Math.floor(
current_progression / 600,
)} mins`}
position="top"
>
<Box
position="absolute"
left={`${
(current_progression / sizeLimit) * window.innerWidth
}px`}
width="3px"
height="100%"
top={0}
opacity={1}
backgroundColor="pink"
>
<Box
position="absolute"
top={0}
left="-50px"
width="100px"
height="100%"
/>
</Box>
</Tooltip>
</Box>
</Window.Content>
</Window>
);
};
type ObjectiveBoxProps = {
objective: Objective;
};
const ObjectiveBox = (props: ObjectiveBoxProps) => {
const { objective } = props;
let width = `${
(objective.progression_maximum / sizeLimit) * window.innerWidth
}px`;
if (objective.progression_maximum > sizeLimit) {
width = '100%';
}
return (
<Box
backgroundColor="grey"
position="absolute"
left={`${
(objective.progression_minimum / sizeLimit) * window.innerWidth
}px`}
width={width}
height="95px"
>
<Stack vertical width="100%">
<Stack.Item
style={{
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
overflow: 'hidden',
}}
>
{objective.name}
</Stack.Item>
<Stack.Item>
<Stack>
<Stack.Item grow>
{objective.progression_minimum / 600} mins
</Stack.Item>
{objective.progression_maximum <= sizeLimit && (
<Stack.Item>
{objective.progression_maximum / 600} mins
</Stack.Item>
)}
</Stack>
</Stack.Item>
<Stack.Item position="relative" basis="18px">
<Box
position="absolute"
left={0}
height="100%"
backgroundColor="green"
width={`${
(objective.progression_reward[1] / sizeLimit) * window.innerWidth
}px`}
style={{
whiteSpace: 'nowrap',
}}
>
{objective.progression_reward[0] / 600}
&nbsp;to {objective.progression_reward[1] / 600} pr
</Box>
</Stack.Item>
<Stack.Item position="relative" basis="18px">
<Box
position="absolute"
left={0}
height="100%"
backgroundColor="red"
width={`${objective.telecrystal_reward[1] * 10}px`}
style={{
whiteSpace: 'nowrap',
}}
>
{objective.telecrystal_reward[0]}
&nbsp;to {objective.telecrystal_reward[1]} tc
</Box>
</Stack.Item>
</Stack>
</Box>
);
};
@@ -0,0 +1,41 @@
import { Box, Flex, Stack } from 'tgui-core/components';
import { classes } from 'tgui-core/react';
export type Objective = {
id: number;
name: string;
description: string;
};
type ObjectiveElementProps = {
name: string;
description: string;
};
export const ObjectiveElement = (props: ObjectiveElementProps) => {
const { name, description } = props;
return (
<Flex direction="column">
<Flex.Item grow={false} basis="content">
<Box
className={classes([
'UplinkObjective__Titlebar',
'reputation-very-good',
])}
width="100%"
height="100%"
>
<Stack>
<Stack.Item grow={1}>{name} </Stack.Item>
</Stack>
</Box>
</Flex.Item>
<Flex.Item grow={false} basis="content">
<Box className="UplinkObjective__Content" height="100%">
<Box>{description}</Box>
</Box>
</Flex.Item>
</Flex>
);
};
@@ -1,569 +0,0 @@
import { Component, MouseEvent } from 'react';
import {
Box,
Button,
Dimmer,
Flex,
Icon,
NoticeBox,
Section,
Stack,
Tooltip,
} from 'tgui-core/components';
import { BooleanLike, classes } from 'tgui-core/react';
import {
calculateProgression,
getDangerLevel,
Rank,
} from './calculateDangerLevel';
import { ObjectiveState } from './constants';
export type Objective = {
id: number;
name: string;
description: string;
progression_minimum: number;
progression_reward: number;
telecrystal_reward: number;
telecrystal_penalty: number;
ui_buttons?: ObjectiveUiButton[];
objective_state: ObjectiveState;
original_progression: number;
final_objective: BooleanLike;
};
export type ObjectiveUiButton = {
name: string;
tooltip: string;
icon: string;
action: string;
};
type ObjectiveMenuProps = {
activeObjectives: Objective[];
potentialObjectives: Objective[];
maximumActiveObjectives: number;
maximumPotentialObjectives: number;
handleStartObjective: (objective: Objective) => void;
handleObjectiveAction: (objective: Objective, action: string) => void;
handleObjectiveCompleted: (objective: Objective) => void;
handleObjectiveAbort: (objective: Objective) => void;
handleRequestObjectives: () => void;
};
type ObjectiveMenuState = {
draggingObjective: Objective | null;
objectiveX: number;
objectiveY: number;
};
let dragClickTimer = 0;
export class ObjectiveMenu extends Component<
ObjectiveMenuProps,
ObjectiveMenuState
> {
constructor(props) {
super(props);
this.state = {
draggingObjective: null,
objectiveX: 0,
objectiveY: 0,
};
this.handleObjectiveClick = this.handleObjectiveClick.bind(this);
this.handleMouseUp = this.handleMouseUp.bind(this);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.handleObjectiveAdded = this.handleObjectiveAdded.bind(this);
}
handleObjectiveClick(event: MouseEvent, objective: Objective) {
if (this.state?.draggingObjective) {
return;
}
if (event.button === 0) {
// Left click
this.setState({
draggingObjective: objective,
objectiveX: event.clientX,
objectiveY: event.clientY,
});
window.addEventListener('mouseup', this.handleMouseUp as any);
window.addEventListener('mousemove', this.handleMouseMove as any);
event.stopPropagation();
event.preventDefault();
dragClickTimer = Date.now() + 100; // 100 milliseconds
}
}
handleMouseUp(event: MouseEvent<HTMLDivElement>) {
if (dragClickTimer > Date.now()) {
return;
}
window.removeEventListener('mouseup', this.handleMouseUp as any);
window.removeEventListener('mousemove', this.handleMouseMove as any);
this.setState({
draggingObjective: null,
});
}
handleMouseMove(event: MouseEvent<HTMLDivElement>) {
this.setState({
objectiveX: event.pageX,
objectiveY: event.pageY - 32,
});
}
handleObjectiveAdded(event: MouseEvent<HTMLDivElement>) {
const { draggingObjective } = this.state as ObjectiveMenuState;
if (!draggingObjective) {
return;
}
const { handleStartObjective } = this.props;
handleStartObjective(draggingObjective);
}
render() {
const {
activeObjectives = [],
potentialObjectives,
maximumActiveObjectives,
maximumPotentialObjectives,
handleObjectiveAction,
handleObjectiveCompleted,
handleObjectiveAbort,
handleRequestObjectives,
} = this.props;
const { draggingObjective, objectiveX, objectiveY } = this
.state as ObjectiveMenuState;
potentialObjectives.sort((objA, objB) => {
if (objA.progression_minimum < objB.progression_minimum) {
return 1;
} else if (objA.progression_minimum > objB.progression_minimum) {
return -1;
}
return 0;
});
return (
<>
<Stack vertical fill scrollable>
<Stack.Item>
<Section>
<Stack>
{Array.apply(null, Array(maximumActiveObjectives)).map(
(_, index) => {
if (index >= activeObjectives.length) {
return (
<Stack.Item key={index} minHeight="100px" grow>
<Box
color="label"
className="UplinkObjective__EmptyObjective"
onMouseUp={this.handleObjectiveAdded}
>
<Stack textAlign="center" fill align="center">
<Stack.Item textAlign="center" width="100%">
Empty Objective, drop objectives here to take
them
</Stack.Item>
</Stack>
</Box>
</Stack.Item>
);
}
const objective = activeObjectives[index];
return (
<Stack.Item key={index} grow>
{ObjectiveFunction(
objective,
true,
handleObjectiveAction,
handleObjectiveCompleted,
handleObjectiveAbort,
true,
)}
</Stack.Item>
);
},
)}
</Stack>
</Section>
</Stack.Item>
<Stack.Item grow>
<Section
title="Potential Objectives"
textAlign="center"
fill
scrollable
>
<Flex wrap="wrap" justify="space-evenly">
{potentialObjectives.map((objective) => {
return (
<Flex.Item
key={objective.id}
textAlign="left"
basis="49%"
mb={1}
mx="0.5%"
onMouseDown={(event) => {
this.handleObjectiveClick(event, objective);
}}
>
{(objective.id !== draggingObjective?.id &&
ObjectiveFunction(
objective,
false,
undefined,
undefined,
undefined,
true,
)) || (
<Box
style={{
border: '2px dashed black',
}}
width="100%"
height="100%"
minHeight="100px"
/>
)}
</Flex.Item>
);
})}
{(maximumPotentialObjectives === 0 && (
<Dimmer>
<Icon name="lock" color="red" fontSize={8} mb={2} />
<Box color="red" fontSize={3}>
You are locked out of objectives
</Box>
</Dimmer>
)) ||
(potentialObjectives.length < maximumPotentialObjectives && (
<Flex.Item basis="100%" mb={1} mx="0.5%" minHeight="100px">
<Stack
align="center"
height="100%"
width="100%"
textAlign="center"
>
<Stack.Item width="100%">
<Button
content="Request More Objectives"
fontSize={2}
onClick={handleRequestObjectives}
/>
</Stack.Item>
</Stack>
</Flex.Item>
))}
</Flex>
</Section>
</Stack.Item>
</Stack>
{!!draggingObjective && (
<Box
width="360px"
height="200px"
position="absolute"
left={`${objectiveX - 180}px`}
top={`${objectiveY}px`}
style={{
pointerEvents: 'none',
}}
>
{ObjectiveFunction(draggingObjective, false)}
</Box>
)}
</>
);
}
}
const ObjectiveFunction = (
objective: Objective,
active: boolean,
handleObjectiveAction?: (objective: Objective, action: string) => void,
handleCompletion?: (objective: Objective) => void,
handleAbort?: (objective: Objective) => void,
grow: boolean = false,
) => {
const dangerLevel = getDangerLevel(objective.progression_minimum);
return (
<ObjectiveElement
name={objective.name}
description={objective.description}
dangerLevel={dangerLevel}
telecrystalReward={objective.telecrystal_reward}
telecrystalPenalty={objective.telecrystal_penalty}
progressionReward={objective.progression_reward}
objectiveState={objective.objective_state}
originalProgression={objective.original_progression}
hideTcRep={objective.final_objective}
finalObjective={objective.final_objective}
canAbort={
!!handleAbort &&
!objective.final_objective &&
objective.objective_state === ObjectiveState.Active
}
grow={grow}
handleCompletion={(event) => {
if (handleCompletion) {
handleCompletion(objective);
}
}}
handleAbort={(event) => {
if (handleAbort) {
handleAbort(objective);
}
}}
uiButtons={
active && handleObjectiveAction ? (
<Stack width="100%" justify="center">
{objective.ui_buttons?.map((value, index) => (
<Stack.Item key={index}>
<Button
content={value.name}
icon={value.icon}
tooltip={value.tooltip}
className={dangerLevel.gradient}
onClick={() => {
handleObjectiveAction(objective, value.action);
}}
/>
</Stack.Item>
))}
</Stack>
) : undefined
}
/>
);
};
type ObjectiveElementProps = {
name: string;
dangerLevel: Rank;
description: string;
telecrystalReward: number;
progressionReward: number;
uiButtons?: JSX.Element;
objectiveState?: ObjectiveState;
originalProgression: number;
telecrystalPenalty: number;
grow: boolean;
hideTcRep: BooleanLike;
finalObjective: BooleanLike;
canAbort: BooleanLike;
handleCompletion?: (event: MouseEvent) => void;
handleAbort?: (event: MouseEvent) => void;
};
export const ObjectiveElement = (props: ObjectiveElementProps) => {
const {
name,
dangerLevel,
description,
uiButtons = null,
telecrystalReward,
progressionReward,
objectiveState,
telecrystalPenalty,
handleCompletion,
handleAbort,
canAbort,
originalProgression,
hideTcRep,
grow,
finalObjective,
...rest
} = props;
const objectiveFinished =
objectiveState === ObjectiveState.Completed ||
objectiveState === ObjectiveState.Failed ||
objectiveState === ObjectiveState.Invalid;
const objectiveFailed = objectiveState !== ObjectiveState.Completed;
let objectiveCompletionText;
switch (objectiveState) {
case ObjectiveState.Invalid:
objectiveCompletionText = 'Invalidated';
break;
case ObjectiveState.Completed:
objectiveCompletionText = 'Completed';
break;
case ObjectiveState.Failed:
objectiveCompletionText = 'Failed';
break;
}
const progressionDiff =
Math.round((1 - progressionReward / originalProgression) * 1000) / 10;
return (
<Flex height={grow ? '100%' : undefined} direction="column">
<Flex.Item grow={grow} basis="content">
<Box
className={classes([
'UplinkObjective__Titlebar',
dangerLevel.gradient,
])}
width="100%"
height="100%"
>
<Stack>
<Stack.Item grow={1}>
{name}{' '}
{!objectiveFinished ? null : `- ${objectiveCompletionText}`}
</Stack.Item>
{canAbort && (
<Stack.Item>
<Button
icon="trash"
color="transparent"
tooltip="Abort Objective"
onClick={handleAbort}
/>
</Stack.Item>
)}
</Stack>
</Box>
</Flex.Item>
<Flex.Item grow={grow} basis="content">
<Box className="UplinkObjective__Content" height="100%">
<Box>{description}</Box>
{!hideTcRep && (
<Box mt={1}>
Failing this objective will deduct {telecrystalPenalty} TC.
</Box>
)}
{finalObjective && objectiveState === ObjectiveState.Inactive && (
<NoticeBox mt={1}>
Taking this objective will lock you out of getting anymore
objectives! Furthermore, you will be unable to abort this
objective.
</NoticeBox>
)}
</Box>
</Flex.Item>
<Flex.Item>
<Box className="UplinkObjective__Footer">
<Stack vertical>
{!hideTcRep && (
<Stack.Item>
<Stack align="center" justify="center">
<Box
style={{
border: '2px solid rgba(0, 0, 0, 0.5)',
borderLeft: 'none',
borderRight: 'none',
borderBottom: objectiveFinished ? 'none' : undefined,
}}
className={dangerLevel.gradient}
py={0.5}
width="100%"
textAlign="center"
>
{telecrystalReward} TC,
<Box ml={1} as="span">
{calculateProgression(progressionReward)} Threat Level
{Math.abs(progressionDiff) > 10 && (
<Tooltip
content={
<Box>
You will get
<Box
mr={1}
ml={1}
color={
progressionDiff > 0
? progressionDiff > 25
? 'red'
: 'orange'
: 'green'
}
as="span"
>
{Math.abs(progressionDiff)}%
</Box>
{progressionDiff > 0 ? 'less' : 'more'} threat
from this objective. This is because your threat
level is{' '}
{progressionDiff > 0 ? 'ahead ' : 'behind '}
where it normally should be at.
</Box>
}
>
<Box
ml={1}
color={
progressionDiff > 0
? progressionDiff > 35
? 'red'
: 'orange'
: 'green'
}
as="span"
>
({progressionDiff > 0 ? '-' : '+'}
{Math.abs(progressionDiff)}%)
</Box>
</Tooltip>
)}
</Box>
</Box>
</Stack>
{objectiveFinished ? (
<Box
inline
className={dangerLevel.gradient}
style={{
borderRadius: '0',
border: '2px solid rgba(0, 0, 0, 0.5)',
borderLeft: 'none',
borderRight: 'none',
}}
position="relative"
width="100%"
textAlign="center"
bold
>
<Box
width="100%"
height="100%"
backgroundColor={
objectiveFailed
? 'rgba(255, 0, 0, 0.1)'
: 'rgba(0, 255, 0, 0.1)'
}
position="absolute"
left={0}
top={0}
/>
<Button
onClick={handleCompletion}
color={objectiveFailed ? 'bad' : 'good'}
style={{
border: '1px solid rgba(0, 0, 0, 0.65)',
}}
my={1}
>
TURN IN
</Button>
</Box>
) : null}
</Stack.Item>
)}
{!!uiButtons && !objectiveFinished && (
<Stack.Item>{uiButtons}</Stack.Item>
)}
</Stack>
</Box>
</Flex.Item>
</Flex>
);
};
@@ -1,17 +1,16 @@
import { Box, Button, Dimmer, Section, Stack } from 'tgui-core/components';
import { Box, Button, Section, Stack } from 'tgui-core/components';
import { useBackend } from '../../backend';
import { ObjectiveElement } from './ObjectiveMenu';
import { ObjectiveElement } from './ObjectiveElement';
type PrimaryObjectiveMenuProps = {
primary_objectives;
final_objective;
can_renegotiate;
};
export const PrimaryObjectiveMenu = (props: PrimaryObjectiveMenuProps) => {
const { act } = useBackend();
const { primary_objectives, final_objective, can_renegotiate } = props;
const { primary_objectives, can_renegotiate } = props;
return (
<Section fill scrollable align="center">
<Box my={4} bold fontSize={1.2} color="green">
@@ -20,34 +19,6 @@ export const PrimaryObjectiveMenu = (props: PrimaryObjectiveMenuProps) => {
<Box my={4} bold fontSize={1.2}>
Your Primary Objectives are as follows. Complete these at all costs.
</Box>
<Box my={4} bold fontSize={1.2}>
Completing Secondary Objectives may allow you to aquire additional
equipment.
</Box>
{final_objective && (
<Dimmer>
<Box
color="red"
fontFamily={'Bahnschrift'}
fontSize={3}
align={'top'}
as="span"
>
PRIORITY MESSAGE
<br />
SOURCE: xxx.xxx.xxx.224:41394
<br />
<br />
\\Debrief in progress.
<br />
\\Final Objective confirmed complete. <br />
\\Your work is done here, agent.
<br />
<br />
CONNECTION CLOSED_
</Box>
</Dimmer>
)}
<Stack vertical>
{primary_objectives.map((prim_obj, index) => (
<Stack.Item key={index}>
@@ -55,22 +26,6 @@ export const PrimaryObjectiveMenu = (props: PrimaryObjectiveMenuProps) => {
key={prim_obj.id}
name={prim_obj['task_name']}
description={prim_obj['task_text']}
dangerLevel={{
minutesLessThan: 0,
title: 'none',
gradient:
index === primary_objectives.length - 1
? 'reputation-good'
: 'reputation-very-good',
}}
telecrystalReward={0}
telecrystalPenalty={0}
progressionReward={0}
originalProgression={0}
hideTcRep
canAbort={false}
grow={false}
finalObjective={false}
/>
</Stack.Item>
))}
@@ -1,7 +0,0 @@
export enum ObjectiveState {
Inactive = 1,
Active = 2,
Completed = 3,
Failed = 4,
Invalid = 5,
}
+31 -97
View File
@@ -21,7 +21,6 @@ import {
dangerLevelsTooltip,
} from './calculateDangerLevel';
import { GenericUplink, Item } from './GenericUplink';
import { Objective, ObjectiveMenu } from './ObjectiveMenu';
import { PrimaryObjectiveMenu } from './PrimaryObjectiveMenu';
type UplinkItem = {
@@ -63,16 +62,10 @@ type UplinkData = {
[key: string]: number;
};
has_objectives: BooleanLike;
has_progression: BooleanLike;
primary_objectives: {
[key: number]: string;
};
completed_final_objective: string;
potential_objectives: Objective[];
active_objectives: Objective[];
maximum_active_objectives: number;
maximum_potential_objectives: number;
purchased_items: number;
shop_locked: BooleanLike;
can_renegotiate: BooleanLike;
@@ -184,13 +177,7 @@ export class Uplink extends Component<{}, UplinkState> {
progression_points,
primary_objectives,
can_renegotiate,
completed_final_objective,
active_objectives,
potential_objectives,
has_objectives,
has_progression,
maximum_active_objectives,
maximum_potential_objectives,
current_expected_progression,
progression_scaling_deviance,
current_progression_scaling,
@@ -294,11 +281,7 @@ export class Uplink extends Component<{}, UplinkState> {
<Box>
<Box>
<Box>Your current level of threat.</Box> Threat
determines
{has_objectives
? ' the severity of secondary objectives you get and '
: ' '}
what items you can purchase.&nbsp;
determines what items you can purchase.&nbsp;
<Box mt={0.5}>
{/* A minute in deciseconds */}
Threat passively increases by{' '}
@@ -341,7 +324,7 @@ export class Uplink extends Component<{}, UplinkState> {
</Tooltip>
</Stack.Item>
)}
{!!(primary_objectives || has_objectives) && (
{!!primary_objectives && (
<Stack.Item grow={1}>
<Tabs fluid>
{primary_objectives && (
@@ -358,20 +341,6 @@ export class Uplink extends Component<{}, UplinkState> {
Primary Objectives
</Tabs.Tab>
)}
{!!has_objectives && (
<Tabs.Tab
style={{
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
}}
icon="star-half-stroke"
selected={currentTab === 1}
onClick={() => this.setState({ currentTab: 1 })}
>
Secondary Objectives
</Tabs.Tab>
)}
<Tabs.Tab
style={{
overflow: 'hidden',
@@ -409,73 +378,38 @@ export class Uplink extends Component<{}, UplinkState> {
{(currentTab === 0 && primary_objectives && (
<PrimaryObjectiveMenu
primary_objectives={primary_objectives}
final_objective={completed_final_objective}
can_renegotiate={can_renegotiate}
/>
)) ||
(currentTab === 1 && has_objectives && (
<ObjectiveMenu
activeObjectives={active_objectives}
potentialObjectives={potential_objectives}
maximumActiveObjectives={maximum_active_objectives}
maximumPotentialObjectives={maximum_potential_objectives}
handleObjectiveAction={(objective, action) =>
act('objective_act', {
check: objective.original_progression,
objective_action: action,
index: objective.id,
})
}
handleStartObjective={(objective) =>
act('start_objective', {
check: objective.original_progression,
index: objective.id,
})
}
handleObjectiveAbort={(objective) =>
act('objective_abort', {
check: objective.original_progression,
index: objective.id,
})
}
handleObjectiveCompleted={(objective) =>
act('finish_objective', {
check: objective.original_progression,
index: objective.id,
})
}
handleRequestObjectives={() => act('regenerate_objectives')}
)) || (
<>
<GenericUplink
currency={`${telecrystals} TC`}
categories={allCategories}
items={items}
handleBuy={(item: ItemExtraData) => {
if (!item.extraData?.ref) {
act('buy', { path: item.id });
} else {
act('buy', { ref: item.extraData.ref });
}
}}
/>
)) || (
<>
<GenericUplink
currency={`${telecrystals} TC`}
categories={allCategories}
items={items}
handleBuy={(item: ItemExtraData) => {
if (!item.extraData?.ref) {
act('buy', { path: item.id });
} else {
act('buy', { ref: item.extraData.ref });
}
}}
/>
{(shop_locked && !data.debug && (
<Dimmer>
<Box
color="red"
fontFamily={'Bahnschrift'}
fontSize={3}
align={'top'}
as="span"
>
SHOP LOCKED
</Box>
</Dimmer>
)) ||
null}
</>
)}
{(shop_locked && !data.debug && (
<Dimmer>
<Box
color="red"
fontFamily={'Bahnschrift'}
fontSize={3}
align={'top'}
as="span"
>
SHOP LOCKED
</Box>
</Dimmer>
)) ||
null}
</>
)}
</Stack.Item>
</Stack>
</Window.Content>