diff --git a/code/datums/components/uplink.dm b/code/datums/components/uplink.dm index 393074eb56e..0e15654bd2f 100644 --- a/code/datums/components/uplink.dm +++ b/code/datums/components/uplink.dm @@ -166,20 +166,34 @@ data["maximum_potential_objectives"] = uplink_handler.maximum_potential_objectives if(uplink_handler.has_objectives) + var/list/primary_objectives = list() + for(var/datum/objective/task as anything in uplink_handler.primary_objectives) + var/list/task_data = list() + if(length(primary_objectives) > length(GLOB.phonetic_alphabet)) + task_data["task_name"] = "DIRECTIVE [length(primary_objectives) + 1]" //The english alphabet is WEAK + else + task_data["task_name"] = "DIRECTIVE [uppertext(GLOB.phonetic_alphabet[length(primary_objectives) + 1])]" + task_data["task_text"] = task.explanation_text + primary_objectives += list(task_data) + var/list/potential_objectives = list() for(var/index in 1 to uplink_handler.potential_objectives.len) var/datum/traitor_objective/objective = uplink_handler.potential_objectives[index] var/list/objective_data = objective.uplink_ui_data(user) objective_data["id"] = index potential_objectives += list(objective_data) + var/list/active_objectives = list() for(var/index in 1 to uplink_handler.active_objectives.len) var/datum/traitor_objective/objective = uplink_handler.active_objectives[index] var/list/objective_data = objective.uplink_ui_data(user) objective_data["id"] = index active_objectives += list(objective_data) + + data["primary_objectives"] = primary_objectives data["potential_objectives"] = potential_objectives data["active_objectives"] = active_objectives + data["completed_final_objective"] = uplink_handler.final_objective var/list/stock_list = uplink_handler.item_stock.Copy() var/list/extra_purchasable_stock = list() diff --git a/code/game/gamemodes/objective.dm b/code/game/gamemodes/objective.dm index 0c3f903e0c9..e9eed8e3efc 100644 --- a/code/game/gamemodes/objective.dm +++ b/code/game/gamemodes/objective.dm @@ -110,7 +110,7 @@ GLOBAL_LIST(admin_objective_list) //Prefilled admin assignable objective list return target //dupe_search_range is a list of antag datums / minds / teams -/datum/objective/proc/find_target(dupe_search_range, blacklist) +/datum/objective/proc/find_target(dupe_search_range, list/blacklist) var/list/datum/mind/owners = get_owners() if(!dupe_search_range) dupe_search_range = get_owners() @@ -462,7 +462,7 @@ GLOBAL_LIST(admin_objective_list) //Prefilled admin assignable objective list var/target_real_name // Has to be stored because the target's real_name can change over the course of the round var/target_missing_id -/datum/objective/escape/escape_with_identity/find_target(dupe_search_range) +/datum/objective/escape/escape_with_identity/find_target(dupe_search_range, list/blacklist) target = ..() update_explanation_text() @@ -576,7 +576,7 @@ GLOBAL_LIST_EMPTY(possible_items) for(var/I in subtypesof(/datum/objective_item/steal)) new I -/datum/objective/steal/find_target(dupe_search_range) +/datum/objective/steal/find_target(dupe_search_range, list/blacklist) var/list/datum/mind/owners = get_owners() if(!dupe_search_range) dupe_search_range = get_owners() @@ -659,7 +659,7 @@ GLOBAL_LIST_EMPTY(possible_items_special) for(var/I in subtypesof(/datum/objective_item/special) + subtypesof(/datum/objective_item/stack)) new I -/datum/objective/steal/special/find_target(dupe_search_range) +/datum/objective/steal/special/find_target(dupe_search_range, list/blacklist) return set_target(pick(GLOB.possible_items_special)) /datum/objective/capture @@ -821,8 +821,9 @@ GLOBAL_LIST_EMPTY(possible_items_special) name = "destroy AI" martyr_compatible = TRUE -/datum/objective/destroy/find_target(dupe_search_range) - var/list/possible_targets = active_ais(1) +/datum/objective/destroy/find_target(dupe_search_range, list/blacklist) + var/list/possible_targets = active_ais(TRUE) + possible_targets -= blacklist var/mob/living/silicon/ai/target_ai = pick(possible_targets) target = target_ai.mind update_explanation_text() diff --git a/code/modules/antagonists/cult/cult.dm b/code/modules/antagonists/cult/cult.dm index bc158ec0b8d..a250563da7a 100644 --- a/code/modules/antagonists/cult/cult.dm +++ b/code/modules/antagonists/cult/cult.dm @@ -347,7 +347,7 @@ UnregisterSignal(target.current, list(COMSIG_PARENT_QDELETING, COMSIG_MOB_MIND_TRANSFERRED_INTO)) target = null -/datum/objective/sacrifice/find_target(dupe_search_range) +/datum/objective/sacrifice/find_target(dupe_search_range, list/blacklist) clear_sacrifice() if(!istype(team, /datum/team/cult)) return diff --git a/code/modules/antagonists/traitor/datum_traitor.dm b/code/modules/antagonists/traitor/datum_traitor.dm index e4dcc9132c6..fc8d7de39dd 100644 --- a/code/modules/antagonists/traitor/datum_traitor.dm +++ b/code/modules/antagonists/traitor/datum_traitor.dm @@ -47,6 +47,7 @@ uplink.uplink_handler = uplink_handler else uplink_handler = uplink.uplink_handler + uplink_handler.primary_objectives = objectives uplink_handler.has_progression = TRUE SStraitor.register_uplink_handler(uplink_handler) @@ -147,71 +148,33 @@ employer = pick(possible_employers) traitor_flavor = strings(TRAITOR_FLAVOR_FILE, employer) -/datum/objective/traitor_progression - name = "traitor progression" - explanation_text = "Become a living legend by getting a total of %REPUTATION% reputation points" - - var/possible_range = list(40 MINUTES, 90 MINUTES) - var/required_total_progression_points - -/datum/objective/traitor_progression/New(text) - . = ..() - required_total_progression_points = round(rand(possible_range[1], possible_range[2])) - explanation_text = replacetext(explanation_text, "%REPUTATION%", DISPLAY_PROGRESSION(required_total_progression_points)) - -/datum/objective/traitor_progression/check_completion() - if(!owner) - return FALSE - var/datum/antagonist/traitor/traitor = owner.has_antag_datum(/datum/antagonist/traitor) - if(!traitor) - return FALSE - if(!traitor.uplink_handler) - return FALSE - if(traitor.uplink_handler.progression_points < required_total_progression_points) - return FALSE - return TRUE - -/datum/objective/traitor_objectives - name = "traitor objective" - explanation_text = "Complete objectives colletively worth more than %REPUTATION% reputation points" - - var/possible_range = list(20 MINUTES, 30 MINUTES) - var/required_progression_in_objectives - -/datum/objective/traitor_objectives/New(text) - . = ..() - required_progression_in_objectives = round(rand(possible_range[1], possible_range[2])) - explanation_text = replacetext(explanation_text, "%REPUTATION%", DISPLAY_PROGRESSION(required_progression_in_objectives)) - -/datum/objective/traitor_objectives/check_completion() - if(!owner) - return FALSE - var/datum/antagonist/traitor/traitor = owner.has_antag_datum(/datum/antagonist/traitor) - if(!traitor) - return FALSE - if(!traitor.uplink_handler) - return FALSE - var/total_points = 0 - for(var/datum/traitor_objective/objective as anything in traitor.uplink_handler.completed_objectives) - if(objective.objective_state != OBJECTIVE_STATE_COMPLETED) - continue - total_points += objective.progression_reward - if(total_points < required_progression_in_objectives) - return FALSE - return TRUE - -/// Generates a complete set of traitor objectives up to the traitor objective limit, including non-generic objectives such as martyr and hijack. /datum/antagonist/traitor/proc/forge_traitor_objectives() objectives.Cut() - var/datum/objective/traitor_progression/final_objective = new /datum/objective/traitor_progression() - final_objective.owner = owner - objectives += final_objective + var/list/kill_targets = list() //for blacklisting already-set targets + var/objective_limit = CONFIG_GET(number/traitor_objectives_amount) + for(var/i in 1 to objective_limit) + var/list/ai_targets = active_ais(z = 2) //For multiZ stations, this proc will include AIs on all station levels if the provided arg is 2. + ai_targets -= kill_targets + if(ai_targets.len) + var/diceroll = rand(1, (living_player_count() - 1)) //AI kill and crew kill objectives are different, but I want a rough equal chance for AIs to be targets. Maybe I'll refractor this someday + if(diceroll <= ai_targets.len) + var/datum/objective/task = new /datum/objective/destroy() + task.owner = owner + task.find_target(blacklist = kill_targets) + kill_targets += task.target + objectives += task + continue - var/datum/objective/traitor_objectives/objective_completion = new /datum/objective/traitor_objectives() - objective_completion.owner = owner - objectives += objective_completion + var/datum/objective/task = new /datum/objective/assassinate() + task.owner = owner + task.find_target(blacklist = kill_targets) + kill_targets += task.target + objectives += task + var/datum/objective/escape/bye = new /datum/objective/escape() + objectives += bye + bye.owner = owner /datum/antagonist/traitor/apply_innate_effects(mob/living/mob_override) . = ..() @@ -279,6 +242,9 @@ objectives_text += "
Objective #[count]: [objective.explanation_text] [span_redtext("Fail.")]" traitor_won = FALSE count++ + if(uplink_handler.final_objective) + objectives_text += "
[span_greentext("[traitor_won ? "Additionally" : "However"], the final objective \"[uplink_handler.final_objective]\" was completed!")]" + traitor_won = TRUE result += "
[owner.name] [traitor_flavor["roundend_report"]]" @@ -292,11 +258,7 @@ result += objectives_text if(uplink_handler) - var/completed_objectives_text = "Completed Uplink Objectives: " - for(var/datum/traitor_objective/objective as anything in uplink_handler.completed_objectives) - if(objective.objective_state == OBJECTIVE_STATE_COMPLETED) - completed_objectives_text += "
[objective.name] - ([objective.telecrystal_reward] TC, [DISPLAY_PROGRESSION(objective.progression_reward)] Reputation)" - result += completed_objectives_text + result += "
The traitor had a total of [uplink_handler.progression_points] Reputation and [uplink_handler.telecrystals] Unused Telecrystals." var/special_role_text = lowertext(name) diff --git a/code/modules/antagonists/traitor/objectives/assassination.dm b/code/modules/antagonists/traitor/objectives/assassination.dm index bb0d1af5908..f69766587d6 100644 --- a/code/modules/antagonists/traitor/objectives/assassination.dm +++ b/code/modules/antagonists/traitor/objectives/assassination.dm @@ -171,6 +171,12 @@ /datum/traitor_objective/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)) @@ -182,6 +188,8 @@ 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 diff --git a/code/modules/antagonists/traitor/objectives/eyesnatching.dm b/code/modules/antagonists/traitor/objectives/eyesnatching.dm index cfc6da292b5..d485983b603 100644 --- a/code/modules/antagonists/traitor/objectives/eyesnatching.dm +++ b/code/modules/antagonists/traitor/objectives/eyesnatching.dm @@ -50,6 +50,13 @@ heads_of_staff = TRUE /datum/traitor_objective/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) @@ -59,6 +66,9 @@ if(possible_target == generating_for) continue + if(possible_target in already_targeting) + continue + if(!ishuman(possible_target.current)) continue diff --git a/code/modules/antagonists/traitor/objectives/final_objective/final_objective.dm b/code/modules/antagonists/traitor/objectives/final_objective/final_objective.dm index fdce4cdf651..5503f78c49e 100644 --- a/code/modules/antagonists/traitor/objectives/final_objective/final_objective.dm +++ b/code/modules/antagonists/traitor/objectives/final_objective/final_objective.dm @@ -30,6 +30,7 @@ for(var/datum/traitor_objective/objective as anything in handler.potential_objectives) objective.fail_objective() user.playsound_local(get_turf(user), 'sound/traitor/final_objective.ogg', vol = 100, vary = FALSE, channel = CHANNEL_TRAITOR) + handler.final_objective = name /datum/traitor_objective/ultimate/uplink_ui_data(mob/user) . = ..() diff --git a/code/modules/antagonists/traitor/objectives/kidnapping.dm b/code/modules/antagonists/traitor/objectives/kidnapping.dm index 84e07d1cd1d..acaa6d3a5c9 100644 --- a/code/modules/antagonists/traitor/objectives/kidnapping.dm +++ b/code/modules/antagonists/traitor/objectives/kidnapping.dm @@ -136,11 +136,21 @@ alive_bonus = 2 /datum/traitor_objective/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 diff --git a/code/modules/antagonists/traitor/uplink_handler.dm b/code/modules/antagonists/traitor/uplink_handler.dm index 42e236b1e92..8b65312fc6c 100644 --- a/code/modules/antagonists/traitor/uplink_handler.dm +++ b/code/modules/antagonists/traitor/uplink_handler.dm @@ -36,6 +36,10 @@ 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. var/assigned_role /// The species this uplink handler is associated to. diff --git a/tgui/packages/tgui/interfaces/Uplink/ObjectiveMenu.tsx b/tgui/packages/tgui/interfaces/Uplink/ObjectiveMenu.tsx index 3ab37667831..8ef793bec15 100644 --- a/tgui/packages/tgui/interfaces/Uplink/ObjectiveMenu.tsx +++ b/tgui/packages/tgui/interfaces/Uplink/ObjectiveMenu.tsx @@ -291,6 +291,7 @@ const ObjectiveFunction = ( progressionReward={objective.progression_reward} objectiveState={objective.objective_state} originalProgression={objective.original_progression} + hideTcRep={objective.final_objective} finalObjective={objective.final_objective} canAbort={ !!handleAbort && @@ -338,18 +339,19 @@ type ObjectiveElementProps = { telecrystalReward: number; progressionReward: number; uiButtons?: JSX.Element; - objectiveState: ObjectiveState; + objectiveState?: ObjectiveState; originalProgression: number; telecrystalPenalty: number; grow: boolean; + hideTcRep: BooleanLike; finalObjective: BooleanLike; canAbort: BooleanLike; - handleCompletion: (event: MouseEvent) => void; - handleAbort: (event: MouseEvent) => void; + handleCompletion?: (event: MouseEvent) => void; + handleAbort?: (event: MouseEvent) => void; }; -const ObjectiveElement = (props: ObjectiveElementProps, context) => { +export const ObjectiveElement = (props: ObjectiveElementProps, context) => { const { name, reputation, @@ -364,6 +366,7 @@ const ObjectiveElement = (props: ObjectiveElementProps, context) => { canAbort, originalProgression, grow, + hideTcRep, finalObjective, ...rest } = props; @@ -419,9 +422,17 @@ const ObjectiveElement = (props: ObjectiveElementProps, context) => { - + {description} - {!finalObjective && ( + {!hideTcRep && ( Failing this objective will deduct {telecrystalPenalty} TC. @@ -436,109 +447,112 @@ const ObjectiveElement = (props: ObjectiveElementProps, context) => { - - - - - - {telecrystalReward} TC, - - {calculateProgression(progressionReward)} Reputation - {Math.abs(progressionDiff) > 10 && ( - - You will get - 0 - ? progressionDiff > 25 - ? 'red' - : 'orange' - : 'green' - } - as="span"> - {Math.abs(progressionDiff)}% - - {progressionDiff > 0 ? 'less' : 'more'} reputation - from this objective. This is because your reputation - is {progressionDiff > 0 ? 'ahead ' : 'behind '} - where it normally should be at. - - }> - 0 - ? progressionDiff > 35 - ? 'red' - : 'orange' - : 'green' - } - as="span"> - ({progressionDiff > 0 ? '-' : '+'} - {Math.abs(progressionDiff)}%) - - - )} - - - - {objectiveFinished ? ( - + {!hideTcRep && ( + + + + - - - ) : null} - - {!!uiButtons && !objectiveFinished && ( - {uiButtons} - )} - - + className={reputation.gradient} + py={0.5} + width="100%" + textAlign="center"> + {telecrystalReward} TC, + + {calculateProgression(progressionReward)} Reputation + {Math.abs(progressionDiff) > 10 && ( + + You will get + 0 + ? progressionDiff > 25 + ? 'red' + : 'orange' + : 'green' + } + as="span"> + {Math.abs(progressionDiff)}% + + {progressionDiff > 0 ? 'less' : 'more'} reputation + from this objective. This is because your + reputation is{' '} + {progressionDiff > 0 ? 'ahead ' : 'behind '} + where it normally should be at. + + }> + 0 + ? progressionDiff > 35 + ? 'red' + : 'orange' + : 'green' + } + as="span"> + ({progressionDiff > 0 ? '-' : '+'} + {Math.abs(progressionDiff)}%) + + + )} + + + + {objectiveFinished ? ( + + + + + ) : null} + + {!!uiButtons && !objectiveFinished && ( + {uiButtons} + )} + + + )} ); diff --git a/tgui/packages/tgui/interfaces/Uplink/PrimaryObjectiveMenu.tsx b/tgui/packages/tgui/interfaces/Uplink/PrimaryObjectiveMenu.tsx new file mode 100644 index 00000000000..18348e50800 --- /dev/null +++ b/tgui/packages/tgui/interfaces/Uplink/PrimaryObjectiveMenu.tsx @@ -0,0 +1,82 @@ +import { Box, Dimmer, Section, Stack } from '../../components'; +import { ObjectiveElement } from './ObjectiveMenu'; + +type PrimaryObjectiveMenuProps = { + primary_objectives; + final_objective; +}; + +export const PrimaryObjectiveMenu = ( + props: PrimaryObjectiveMenuProps, + context +) => { + const { primary_objectives, final_objective } = props; + return ( +
+
+ + { + 'Agent, your Primary Objectives are as follows. Complete these at all costs.' + } + + + { + 'Completing Secondary Objectives may allow you to aquire additional equipment.' + } + +
+ {final_objective && ( + + + PRIORITY MESSAGE +
+ SOURCE: xxx.xxx.xxx.224:41394 +
+
+ \\Debrief in progress. +
+ \\Final Objective confirmed complete.
+ \\Your work is done here, agent. +
+
+ CONNECTION CLOSED_ +
+
+ )} +
+ + {primary_objectives.map((prim_obj, index) => ( + + + + ))} + +
+
+ ); +}; diff --git a/tgui/packages/tgui/interfaces/Uplink/index.tsx b/tgui/packages/tgui/interfaces/Uplink/index.tsx index bb897ce7b68..f432d422e39 100644 --- a/tgui/packages/tgui/interfaces/Uplink/index.tsx +++ b/tgui/packages/tgui/interfaces/Uplink/index.tsx @@ -1,11 +1,12 @@ import { useBackend } from '../../backend'; import { Window } from '../../layouts'; import { GenericUplink, Item } from './GenericUplink'; -import { Component } from 'inferno'; +import { Component, Fragment } from 'inferno'; import { fetchRetry } from '../../http'; import { resolveAsset } from '../../assets'; import { BooleanLike } from 'common/react'; import { Box, Tabs, Button, Stack, Section, Tooltip } from '../../components'; +import { PrimaryObjectiveMenu } from './PrimaryObjectiveMenu'; import { Objective, ObjectiveMenu } from './ObjectiveMenu'; import { calculateProgression, calculateReputationLevel, reputationDefault, reputationLevelsTooltip } from './calculateReputationLevel'; @@ -46,6 +47,10 @@ type UplinkData = { 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; @@ -150,6 +155,8 @@ export class Uplink extends Component<{}, UplinkState> { const { telecrystals, progression_points, + primary_objectives, + completed_final_objective, active_objectives, potential_objectives, has_objectives, @@ -313,15 +320,22 @@ export class Uplink extends Component<{}, UplinkState> { {!!has_objectives && ( - this.setState({ currentTab: 0 })}> - Objectives - + + this.setState({ currentTab: 0 })}> + Primary Objectives + + this.setState({ currentTab: 1 })}> + Secondary Objectives + + )} this.setState({ currentTab: 1 })}> + selected={currentTab === 2 || !has_objectives} + onClick={() => this.setState({ currentTab: 2 })}> Market @@ -341,53 +355,59 @@ export class Uplink extends Component<{}, UplinkState> { {(currentTab === 0 && has_objectives && ( - - 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')} + - )) || ( - { - const extraDataItem = item as Item; - if (!extraDataItem.extraData?.ref) { - act('buy', { path: item.id }); - } else { - act('buy', { ref: extraDataItem.extraData.ref }); + )) || + (currentTab === 1 && has_objectives && ( + + 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')} + /> + )) || ( + { + const extraDataItem = item as Item; + if (!extraDataItem.extraData?.ref) { + act('buy', { path: item.id }); + } else { + act('buy', { ref: extraDataItem.extraData.ref }); + } + }} + /> + )}