diff --git a/code/__DEFINES/chat.dm b/code/__DEFINES/chat.dm index 4d170b0b6968..e73ac4d08dc0 100644 --- a/code/__DEFINES/chat.dm +++ b/code/__DEFINES/chat.dm @@ -25,6 +25,9 @@ #define MESSAGE_TYPE_MENTORPM "mentorpm" #define MESSAGE_TYPE_DONATOR "donator" +/// Adds a generic box around whatever message you're sending in chat. Really makes things stand out. +#define EXAMINE_BLOCK(str) ("
" + str + "
") + /// Max length of chat message in characters #define CHAT_MESSAGE_MAX_LENGTH 110 diff --git a/code/__DEFINES/html_assistant.dm b/code/__DEFINES/html_assistant.dm new file mode 100644 index 000000000000..03f95ad20158 --- /dev/null +++ b/code/__DEFINES/html_assistant.dm @@ -0,0 +1,34 @@ +#define TOOLTIP_CSS_SETUP \ +"" +// IE11 does not support the max-content attribute, so 'width: max-content;' doesn't work. + +#define TOOLTIP_WRAPPER(hover_me, width_px, tooltip_text) \ +"
[hover_me][tooltip_text]
" + +#define TOOLTIP_CONFIG_CALLER(hover_me, width_px, config_key) \ +"[(GLOB.tooltips[config_key] ? "
[hover_me][GLOB.tooltips[config_key]]
" : "[hover_me]")]" + +#define OPEN_WIKI(wiki_url, text) (CONFIG_GET(string/wikiurl) ? "[text]" : "[text]") diff --git a/code/__DEFINES/traitor.dm b/code/__DEFINES/traitor.dm new file mode 100644 index 000000000000..00388b3afbb9 --- /dev/null +++ b/code/__DEFINES/traitor.dm @@ -0,0 +1,22 @@ +#define TRAITOR_FACTION_BLACK_MARKET "black_market" +#define TRAITOR_FACTION_SYNDICATE "syndicate" +#define TRAITOR_FACTION_INDEPENDENT "independent" + +/// If this backstory involves being forced into the job +#define TRAITOR_MOTIVATION_FORCED "Forced Into It" +/// If this backstory does not involve being forced into the job +#define TRAITOR_MOTIVATION_NOT_FORCED "Not Forced Into It" +/// If this backstory is motivated by money or personal gain +#define TRAITOR_MOTIVATION_MONEY "Money" +/// If this backstory is politically motivated, wanting to "change the world". +#define TRAITOR_MOTIVATION_POLITICAL "Political" +/// If this backstory is motivated through the power of love (your family, friends, etc) +#define TRAITOR_MOTIVATION_LOVE "Love" +/// If this backstory is motivated by your reputation, or by knowledge (blackmail) +#define TRAITOR_MOTIVATION_REPUTATION "Reputation" +/// If this backstory is motivated by the threat of death or personal harm +#define TRAITOR_MOTIVATION_DEATH_THREAT "Death Threat" +/// If this backstory is motivated by their faction or presence in an organization +#define TRAITOR_MOTIVATION_AUTHORITY "Authority" +/// If this backstory is motivated by themselves or the activity +#define TRAITOR_MOTIVATION_FUN "Fun" diff --git a/code/_globalvars/lists/traitor.dm b/code/_globalvars/lists/traitor.dm new file mode 100644 index 000000000000..f9cd921f13b3 --- /dev/null +++ b/code/_globalvars/lists/traitor.dm @@ -0,0 +1,35 @@ +/// Associative list of /datum/traitor_backstory path strings to datums +GLOBAL_LIST_INIT(traitor_backstories, generate_traitor_backstories()) +/// Associative list of /datum/traitor_faction keys to datums +GLOBAL_LIST_INIT(traitor_factions_to_datum, generate_traitor_factions()) +GLOBAL_LIST_INIT(traitor_factions, assoc_to_keys(GLOB.traitor_factions_to_datum)) + +/proc/generate_traitor_backstories() + var/list/result = list() + for(var/datum/traitor_backstory/path as anything in subtypesof(/datum/traitor_backstory)) + if(isnull(initial(path.name))) + continue + result["[path]"] = new path() + return result + +/proc/generate_traitor_factions() + var/list/result = list() + for(var/datum/traitor_faction/path as anything in subtypesof(/datum/traitor_faction)) + var/key = initial(path.key) + if(!istext(key)) + continue + result[key] = new path() + return result + +GLOBAL_LIST_INIT(traitor_motivations, list( + TRAITOR_MOTIVATION_FORCED, + TRAITOR_MOTIVATION_NOT_FORCED, + TRAITOR_MOTIVATION_MONEY, + TRAITOR_MOTIVATION_POLITICAL, + TRAITOR_MOTIVATION_LOVE, + TRAITOR_MOTIVATION_REPUTATION, + TRAITOR_MOTIVATION_DEATH_THREAT, + TRAITOR_MOTIVATION_AUTHORITY, + TRAITOR_MOTIVATION_FUN, +)) + diff --git a/code/datums/actions/action.dm b/code/datums/actions/action.dm index e7edb81bf763..ae65faa00c93 100644 --- a/code/datums/actions/action.dm +++ b/code/datums/actions/action.dm @@ -46,6 +46,8 @@ /// This is the icon state for any FOREGROUND overlay icons on the button (such as borders) var/overlay_icon_state + var/atom/movable/screen/movable/action_button/button = null + /datum/action/New(Target) link_to(Target) diff --git a/code/game/gamemodes/objective.dm b/code/game/gamemodes/objective.dm index 549bc89f94a5..a29d31346f1d 100644 --- a/code/game/gamemodes/objective.dm +++ b/code/game/gamemodes/objective.dm @@ -209,7 +209,6 @@ GLOBAL_LIST_EMPTY(objectives) /datum/objective/assassinate name = "assassinate" var/target_role_type=FALSE - martyr_compatible = 1 /datum/objective/assassinate/find_target_by_role(role, role_type=FALSE,invert=FALSE) if(!invert) @@ -472,7 +471,6 @@ GLOBAL_LIST_EMPTY(objectives) /datum/objective/purge name = "no mutants on shuttle" explanation_text = "Ensure no mutant humanoids or nonhuman species are present aboard the escape shuttle. Felinids/Catpeople do NOT count as nonhuman." - martyr_compatible = 1 /datum/objective/purge/check_completion() if(..()) @@ -986,7 +984,6 @@ GLOBAL_LIST_EMPTY(possible_items_special) /datum/objective/destroy name = "destroy AI" - martyr_compatible = 1 /datum/objective/destroy/find_target(dupe_search_range, blacklist) var/list/possible_targets = active_ais(1) diff --git a/code/modules/admin/antag_panel.dm b/code/modules/admin/antag_panel.dm index 8d7aaeba7c33..5f494c9ab29b 100644 --- a/code/modules/admin/antag_panel.dm +++ b/code/modules/admin/antag_panel.dm @@ -113,7 +113,7 @@ GLOBAL_VAR(antag_prototypes) tgui_alert(usr, "This mind doesn't have a mob, or is deleted! For some reason!", "Edit Memory") return - var/out = "[name][(current && (current.real_name!=name))?" (as [current.real_name])":""]
" + var/out = "[TOOLTIP_CSS_SETUP][name][(current && (current.real_name!=name))?" (as [current.real_name])":""]
" // yogs start - Donor features, quiet round if(quiet_round) out += "QUIET ROUND ACTIVE (Override)
" diff --git a/code/modules/antagonists/_common/antag_datum.dm b/code/modules/antagonists/_common/antag_datum.dm index 604ef5a2b179..4e2808bb342e 100644 --- a/code/modules/antagonists/_common/antag_datum.dm +++ b/code/modules/antagonists/_common/antag_datum.dm @@ -453,3 +453,13 @@ GLOBAL_LIST_EMPTY(antagonists) if(!(target in owner.mind.antag_datums)) return FALSE return TRUE + +//in the future, this should entirely replace greet. +/datum/antagonist/proc/make_info_button() + if(!ui_name) + return + var/datum/action/antag_info/info_button = new(src) + info_button.Grant(owner.current) + info_button_ref = WEAKREF(info_button) + return info_button + diff --git a/code/modules/antagonists/traitor/datum_traitor.dm b/code/modules/antagonists/traitor/datum_traitor.dm index 43cf5f048c65..3cf6ce940ebd 100644 --- a/code/modules/antagonists/traitor/datum_traitor.dm +++ b/code/modules/antagonists/traitor/datum_traitor.dm @@ -19,6 +19,9 @@ var/datum/contractor_hub/contractor_hub var/obj/item/uplink_holder can_hijack = HIJACK_HIJACKER + /// If this specific traitor has been assigned codewords. This is not always true, because it varies by faction. + var/has_codewords = FALSE + var/datum/weakref/uplink_ref /datum/antagonist/traitor/on_gain() if(owner.current && iscyborg(owner.current)) @@ -168,6 +171,8 @@ add_objective(escape_objective) else forge_single_human_objective() + // Finally, set up our traitor's backstory! + setup_backstories(!is_hijacker && martyr_compatibility, is_hijacker) /datum/antagonist/traitor/proc/forge_ai_objectives() var/objective_count = 0 @@ -249,7 +254,10 @@ .=2 /datum/antagonist/traitor/greet() + var/list/msg = list() to_chat(owner.current, span_alertsyndie("You are the [owner.special_role].")) + msg += "Use the 'Traitor Info and Backstory' action at the top left in order to select a backstory and review your objectives, uplink location, and codewords!" + to_chat(owner.current, EXAMINE_BLOCK(msg.Join("\n"))) owner.announce_objectives() if(should_give_codewords) give_codewords() @@ -272,13 +280,13 @@ ability.Grant(owner.current) if(TRAITOR_HUMAN) - if(should_equip) - equip(silent) + ui_interact(owner.current) owner.current.playsound_local(get_turf(owner.current), 'sound/ambience/antag/tatoralert.ogg', 100, FALSE, pressure_affected = FALSE) /datum/antagonist/traitor/proc/give_codewords() if(!owner.current) return + has_codewords = TRUE var/mob/traitor_mob=owner.current var/phrases = jointext(GLOB.syndicate_code_phrase, ", ") @@ -306,9 +314,12 @@ if(malf) killer.add_malf_picker() -/datum/antagonist/traitor/proc/equip(silent = FALSE) +/datum/antagonist/traitor/proc/equip(var/silent = FALSE) if(traitor_kind == TRAITOR_HUMAN) - uplink_holder = owner.equip_traitor(employer, silent, src) //yogs - uplink_holder = + var/obj/item/uplink_loc = owner.equip_traitor(employer, silent, src) + var/datum/component/uplink/uplink = uplink_loc?.GetComponent(/datum/component/uplink) + if(uplink) + uplink_ref = WEAKREF(uplink) //yogs - uplink_holder = /datum/antagonist/traitor/proc/assign_exchange_role() //set faction @@ -390,6 +401,15 @@ result += objectives_text + var/backstory_text = "
" + if(istype(faction)) + backstory_text += "Faction: \[ [faction.name][faction.description] \]
" + if(istype(backstory)) + backstory_text += "Backstory: \[ [backstory.name][backstory.description] \]
" + else + backstory_text += "No backstory was selected!
" + result += backstory_text + var/special_role_text = lowertext(name) if (contractor_hub) @@ -464,3 +484,17 @@ /datum/outfit/traitor/post_equip(mob/living/carbon/human/H, visualsOnly) var/obj/item/melee/transforming/energy/sword/sword = locate() in H.held_items sword.transform_weapon(H) + + +/datum/antagonist/traitor/antag_panel_data() + // Traitor Backstory + var/backstory_text = "Traitor Backstory:
" + if(istype(faction)) + backstory_text += "Faction: \[ [faction.name][faction.description] \]
" + else + backstory_text += "No faction selected!
" + if(istype(backstory)) + backstory_text += "Backstory: \[ [backstory.name][backstory.description] \]
" + else + backstory_text += "No backstory selected!
" + return backstory_text diff --git a/tgui/packages/tgui/interfaces/AntagInfoTraitor.tsx b/tgui/packages/tgui/interfaces/AntagInfoTraitor.tsx new file mode 100644 index 000000000000..5b26dc7cf94c --- /dev/null +++ b/tgui/packages/tgui/interfaces/AntagInfoTraitor.tsx @@ -0,0 +1,132 @@ +import { useBackend } from '../backend'; +import { BlockQuote, Section, Stack } from '../components'; +import { BooleanLike } from 'common/react'; +import { Window } from '../layouts'; +import { ObjectivesSection, Objective } from './common/ObjectiveSelection'; +import { AntagInfoHeader } from './common/AntagInfoHeader'; + +const badstyle = { + color: 'red', + fontWeight: 'bold', +}; + +const goalstyle = { + color: 'lightblue', + fontWeight: 'bold', +}; + +type Info = { + antag_name: string; + has_codewords: BooleanLike; + phrases: string; + responses: string; + code: string; + failsafe_code: string; + has_uplink: BooleanLike; + uplink_unlock_info: string; + objectives: Objective[]; +}; + +const UplinkSection = (_props, context) => { + const { data } = useBackend(context); + const { has_uplink, uplink_unlock_info, code, failsafe_code } = data; + return ( +
+ + +
+ Keep this uplink safe, and don't feel like you need to buy everything immediately — you can save your + telecrystals to use whenever you're in a tough situation and need help. +
+
+ + + + {code && Code: {code}} + + {failsafe_code && ( + <> + {code && Code: {code}} + + + )} + +
{uplink_unlock_info}
+
+
+
+
+
+ ); +}; + +const CodewordsSection = (_props, context) => { + const { data } = useBackend(context); + const { has_codewords, phrases, responses } = data; + return ( +
+ + {(!has_codewords && ( +
+ You have not been supplied with codewords. You will have to use alternative methods to find potential allies. + Proceed with caution, however, as everyone is a potential foe. +
+ )) || ( + <> + +
+ Your employer provided you with the following codewords to identify fellow agents. Use the codewords during + regular conversation to identify other agents. Proceed with caution, however, as everyone is a potential foe. +  You have memorized the codewords, allowing you to recognise them when heard. +
+
+ + + + Code Phrases: + + {phrases} + + Code Responses: + + {responses} + + + + + )} +
+
+ ); +}; + +export const AntagInfoTraitorContent = (_props, context) => { + const { data } = useBackend(context); + const { antag_name, objectives } = data; + return ( + + + + + + + + + + + + + + + ); +}; + +export const AntagInfoTraitor = (_props, context) => { + return ( + + + + + + ); +}; diff --git a/tgui/packages/tgui/interfaces/TraitorBackstoryMenu.js b/tgui/packages/tgui/interfaces/TraitorBackstoryMenu.js new file mode 100644 index 000000000000..327a43007ac0 --- /dev/null +++ b/tgui/packages/tgui/interfaces/TraitorBackstoryMenu.js @@ -0,0 +1,562 @@ +import { useBackend, useLocalState } from '../backend'; +import { Button, Dimmer, Stack, Box, Section, Tabs, Flex, Icon, Tooltip } from '../components'; +import { Window } from '../layouts'; +import { AntagInfoTraitorContent } from './AntagInfoTraitor'; + +export const TraitorBackstoryMenu = (_, context) => { + const { data } = useBackend(context); + const { all_backstories = {}, all_factions = {}, backstory, faction } = data; + let has_backstory = all_backstories[backstory]; + let has_faction = all_factions[faction]; + let [ui_phase, set_ui_phase] = useLocalState(context, 'traitor_ui_phase', has_faction ? 2 : 0); + let [tabIndex, setTabIndex] = useLocalState(context, 'traitor_selected_tab', 1); + let [selected_faction, set_selected_faction_backend] = useLocalState(context, 'traitor_selected_faction', 'syndicate'); + let [selected_backstory, set_selected_backstory] = useLocalState(context, 'traitor_selected_backstory', null); + const set_selected_faction = (faction) => { + set_selected_faction_backend(faction); + if (selected_backstory && !all_backstories[selected_backstory].allowed_factions?.includes(faction)) { + set_selected_backstory(null); + } + }; + let windowTitle = 'Traitor Backstory'; + switch (ui_phase) { + case 0: + windowTitle = 'Traitor Backstory: Introduction'; + break; + case 1: + windowTitle = 'Traitor Backstory: Faction Select'; + break; + case 2: + windowTitle = tabIndex === 1 ? 'Traitor Info' : 'Traitor Backstory'; + break; + } + let info_ui = ui_phase === 2 && has_faction; + return ( + + + {ui_phase === 0 && } + {ui_phase === 1 && ( + + )} + {ui_phase === 2 && !has_faction && ( + + )} + {ui_phase === 2 && has_faction && ( + <> + +