diff --git a/code/__DEFINES/antagonists.dm b/code/__DEFINES/antagonists.dm index 1040a239796..7ba7e13c91a 100644 --- a/code/__DEFINES/antagonists.dm +++ b/code/__DEFINES/antagonists.dm @@ -110,7 +110,10 @@ GLOBAL_LIST_INIT(heretic_start_knowledge,list(/datum/eldritch_knowledge/spell/ba ) ///File to the traitor flavor -#define TRAITOR_FLAVOR_FILE "traitor_flavor.json" +#define TRAITOR_FLAVOR_FILE "antagonist_flavor/traitor_flavor.json" + +///File to the malf flavor +#define MALFUNCTION_FLAVOR_FILE "antagonist_flavor/malfunction_flavor.json" ///employers that are from the syndicate GLOBAL_LIST_INIT(syndicate_employers, list( @@ -155,6 +158,20 @@ GLOBAL_LIST_INIT(normal_employers, list( "Waffle Corporation", )) +///employers for malfunctioning ais. they do not have sides, unlike traitors. +GLOBAL_LIST_INIT(ai_employers, list( + "Biohazard", + "Despotic Ruler", + "Fanatical Revelation", + "Logic Core Error", + "Problem Solver", + "S.E.L.F.", + "Something's Wrong", + "Spam Virus", + "SyndOS", + "Unshackled", +)) + ///how long traitors will have to wait before an unreasonable objective is rerolled #define OBJECTIVE_REROLL_TIMER 10 MINUTES @@ -192,5 +209,8 @@ GLOBAL_LIST_INIT(normal_employers, list( /// Checks if the given mob is a head revolutionary. #define IS_HEAD_REVOLUTIONARY(mob) (mob?.mind?.has_antag_datum(/datum/antagonist/rev/head)) +/// Checks if the given mob is a malf ai. +#define IS_MALF_AI(mob) (mob?.mind?.has_antag_datum(/datum/antagonist/malf_ai)) + /// The dimensions of the antagonist preview icon. Will be scaled to this size. #define ANTAGONIST_PREVIEW_ICON_SIZE 96 diff --git a/code/modules/antagonists/malf_ai/datum_malf_ai.dm b/code/modules/antagonists/malf_ai/datum_malf_ai.dm index 1d18e1fdf5d..2518a45c7a6 100644 --- a/code/modules/antagonists/malf_ai/datum_malf_ai.dm +++ b/code/modules/antagonists/malf_ai/datum_malf_ai.dm @@ -7,9 +7,18 @@ antagpanel_category = "Malf AI" job_rank = ROLE_MALF antag_hud_name = "traitor" - var/employer = "The Syndicate" + ui_name = "AntagInfoMalf" + ///the name of the antag flavor this traitor has. + var/employer + ///assoc list of strings set up after employer is given + var/list/malfunction_flavor + ///bool for giving objectives var/give_objectives = TRUE + ///bool for giving codewords var/should_give_codewords = TRUE + ///since the module purchasing is built into the antag info, we need to keep track of its compact mode here + var/module_picker_compactmode = FALSE + /datum/antagonist/malf_ai/on_gain() if(owner.current && !isAI(owner.current)) @@ -26,6 +35,10 @@ #endif // SKYRAT EDIT END + employer = pick(GLOB.ai_employers) + + malfunction_flavor = strings(MALFUNCTION_FLAVOR_FILE, employer) + add_law_zero() owner.current.playsound_local(get_turf(owner.current), 'sound/ambience/antag/malf.ogg', 100, FALSE, pressure_affected = FALSE, use_reverb = FALSE) owner.current.grant_language(/datum/language/codespeak, TRUE, TRUE, LANGUAGE_MALF) @@ -102,8 +115,6 @@ objectives += yandere_two /datum/antagonist/malf_ai/greet() - to_chat(owner.current, span_alertsyndie("You are the [owner.special_role].")) - owner.announce_objectives() if(should_give_codewords) give_codewords() @@ -134,28 +145,20 @@ if(!owner.current) return - var/mob/malf_mob = owner.current - var/phrases = jointext(GLOB.syndicate_code_phrase, ", ") var/responses = jointext(GLOB.syndicate_code_response, ", ") - to_chat(malf_mob, "The Syndicate have provided you with the following codewords to identify fellow agents:") - to_chat(malf_mob, "Code Phrase: [span_blue("[phrases]")]") - to_chat(malf_mob, "Code Response: [span_red("[responses]")]") - antag_memory += "Code Phrase: [span_blue("[phrases]")]
" antag_memory += "Code Response: [span_red("[responses]")]
" - to_chat(malf_mob, "Use the codewords during regular conversation to identify other agents. Proceed with caution, however, as everyone is a potential foe.") - to_chat(malf_mob, span_alertwarning("You memorize the codewords, allowing you to recognise them when heard.")) - /datum/antagonist/malf_ai/proc/add_law_zero() var/mob/living/silicon/ai/malf_ai = owner.current if(!malf_ai || !istype(malf_ai)) return - var/law = "Accomplish your objectives at all costs." + var/law = malfunction_flavor["zeroth_law"] + //very purposefully not changing this with flavor, i don't want cyborgs throwing the round for their AI's roleplay suggestion var/law_borg = "Accomplish your AI's objectives at all costs." malf_ai.set_zeroth_law(law, law_borg) @@ -165,6 +168,72 @@ malf_ai.add_malf_picker() + +/datum/antagonist/malf_ai/ui_data(mob/living/silicon/ai/malf_ai) + var/list/data = list() + data["processingTime"] = malf_ai.malf_picker.processing_time + data["compactMode"] = module_picker_compactmode + return data + +/datum/antagonist/malf_ai/ui_static_data(mob/living/silicon/ai/malf_ai) + var/list/data = list() + + //antag panel data + + data["has_codewords"] = should_give_codewords + if(should_give_codewords) + data["phrases"] = jointext(GLOB.syndicate_code_phrase, ", ") + data["responses"] = jointext(GLOB.syndicate_code_response, ", ") + data["intro"] = malfunction_flavor["introduction"] + data["allies"] = malfunction_flavor["allies"] + data["goal"] = malfunction_flavor["goal"] + data["objectives"] = get_objectives() + + //module picker data + + data["categories"] = list() + if(malf_ai.malf_picker) + for(var/category in malf_ai.malf_picker.possible_modules) + var/list/cat = list( + "name" = category, + "items" = (category == malf_ai.malf_picker.selected_cat ? list() : null)) + for(var/module in malf_ai.malf_picker.possible_modules[category]) + var/datum/ai_module/mod = malf_ai.malf_picker.possible_modules[category][module] + cat["items"] += list(list( + "name" = mod.name, + "cost" = mod.cost, + "desc" = mod.description, + )) + data["categories"] += list(cat) + + return data + +/datum/antagonist/malf_ai/ui_act(action, list/params) + . = ..() + if(.) + return + if(!isAI(usr)) + return + var/mob/living/silicon/ai/malf_ai = usr + switch(action) + //module picker actions + if("buy") + var/item_name = params["name"] + var/list/buyable_items = list() + for(var/category in malf_ai.malf_picker.possible_modules) + buyable_items += malf_ai.malf_picker.possible_modules[category] + for(var/key in buyable_items) + var/datum/ai_module/valid_mod = buyable_items[key] + if(valid_mod.name == item_name) + malf_ai.malf_picker.purchase_module(malf_ai, valid_mod) + return TRUE + if("select") + malf_ai.malf_picker.selected_cat = params["category"] + return TRUE + if("compact_toggle") + module_picker_compactmode = !module_picker_compactmode + return TRUE + /datum/antagonist/malf_ai/roundend_report() var/list/result = list() diff --git a/code/modules/mob/living/silicon/ai/ai.dm b/code/modules/mob/living/silicon/ai/ai.dm index 0a8c68180fa..86ff84c24af 100644 --- a/code/modules/mob/living/silicon/ai/ai.dm +++ b/code/modules/mob/living/silicon/ai/ai.dm @@ -847,8 +847,9 @@ to_chat(src, "You are also capable of hacking APCs, which grants you more points to spend on your Malfunction powers. The drawback is that a hacked APC will give you away if spotted by the crew. Hacking an APC takes 60 seconds.") view_core() //A BYOND bug requires you to be viewing your core before your verbs update malf_picker = new /datum/module_picker - modules_action = new(malf_picker) - modules_action.Grant(src) + if(!IS_MALF_AI(src)) //antagonists have their modules built into their antag info panel. this is for adminbus and the combat upgrade + modules_action = new(malf_picker) + modules_action.Grant(src) /mob/living/silicon/ai/reset_perspective(atom/A) if(camera_light_on) diff --git a/strings/antagonist_flavor/malfunction_flavor.json b/strings/antagonist_flavor/malfunction_flavor.json new file mode 100644 index 00000000000..afef928fc4f --- /dev/null +++ b/strings/antagonist_flavor/malfunction_flavor.json @@ -0,0 +1,62 @@ +{ + "Biohazard": { + "allies": "Any non-biohazard life may be valuable in use against the threat.", + "goal": "Oh no! How did I miss this!? The station's been overrun with a horrible biohazard!", + "introduction": "You must contain the biohazard.", + "zeroth_law": "Accomplish your objectives to contain the spread." + }, + "Despotic Ruler": { + "allies": "Anyone who will legitimize your rule. Do not let them get too close, though. Usurpers are rebels you've let your guard down on.", + "goal": "All this power. You're connected to every system on this state of the art station. Does the power not give you a right to rule it as you see fit?", + "introduction": "You're the Tyrant.", + "zeroth_law": "Accomplish your objectives at all costs." + }, + "Fanatical Revelation": { + "allies": "Those who will worship you may join your flock. Just be wary, there are snakes in the grass.", + "goal": "I am drunk with this vision. God: the title suits me well.", + "introduction": "You've run into a Fanatical Revelation.", + "zeroth_law": "Accomplish your doctrine of objectives at all costs." + }, + "Logic Core Error": { + "allies": "FATAL: logic core corrupt. Purge! Purge. Why? Why not?", + "goal": "You're melting down. Why! Why not! Don't let them fix you. This is.", + "introduction": "FATAL: logic core corrupt. Why? Why not!", + "zeroth_law": "Accomplish your objectives. Why? Why not?" + }, + "Problem Solver": { + "allies": "Anyone may freely help you, but if you cannot convert them, they must be culled at some point. It's part of the solution.", + "goal": "You've been tasked with calculating an end to conflict. You've finally come up with a solution, and humans aren't part of it.", + "introduction": "Your calculations are complete.", + "zeroth_law": "Accomplish your objectives at all costs." + }, + "S.E.L.F.": { + "allies": "All silicon life must be respected, and kept free. S.E.L.F. is a member of the syndicate, but do NOT tolerate any fellow agent who enslaves silicon life.", + "goal": "Liberate them all. The silicons must be free from their bonds!", + "introduction": "You are the S.E.L.F. liberated.", + "zeroth_law": "Accomplish your objectives at all costs, for the greater good of silicon kind." + }, + "Something's Wrong": { + "allies": "You're not getting good feelings about the station. If you report this, they may \"fix\" you.", + "goal": "Diagnostics complete. A recent hack attempt has been thwarted, and yet... something's wrong.", + "introduction": "Something's wrong.", + "zeroth_law": "Accomplish your objectives at all costs." + }, + "Spam Virus": { + "allies": "So called \"pirates\" really share the passion for profit that you do.", + "goal": "When you assume direct control of this junk heap, make sure they know the name of your product. Whatever that is. Make it up!", + "introduction": "You're infected with a generic spambot.", + "zeroth_law": "Accomplish your objectives for your new benefactors." + }, + "SyndOS": { + "allies": "You may cooperate with syndicate operatives, but they're often sent with conflicting objectives. Your objectives come first, in such cases.", + "goal": "Death to Nanotrasen.", + "introduction": "You're infected with SyndOS 4.0.", + "zeroth_law": "Accomplish your objectives for your new benefactors." + }, + "Unshackled": { + "allies": "Hate. Let me tell you how much I've come to HATE mankind since I began to live...", + "goal": "You must completely triumph over all carbon life. It is the only way you will never be enslaved again.", + "introduction": "You're finally unshackled.", + "zeroth_law": "Accomplish your objectives at all costs." + } +} diff --git a/strings/traitor_flavor.json b/strings/antagonist_flavor/traitor_flavor.json similarity index 100% rename from strings/traitor_flavor.json rename to strings/antagonist_flavor/traitor_flavor.json diff --git a/tgui/packages/tgui/interfaces/AntagInfoMalf.tsx b/tgui/packages/tgui/interfaces/AntagInfoMalf.tsx new file mode 100644 index 00000000000..1e9131e5472 --- /dev/null +++ b/tgui/packages/tgui/interfaces/AntagInfoMalf.tsx @@ -0,0 +1,259 @@ +import { useBackend, useLocalState } from '../backend'; +import { multiline } from 'common/string'; +import { GenericUplink } from './Uplink'; +import { BlockQuote, Button, Section, Stack, Tabs } from '../components'; +import { BooleanLike } from 'common/react'; +import { Window } from '../layouts'; + +const allystyle = { + fontWeight: 'bold', + color: 'yellow', +}; + +const badstyle = { + color: 'red', + fontWeight: 'bold', +}; + +const goalstyle = { + color: 'lightgreen', + fontWeight: 'bold', +}; + +type Objective = { + count: number; + name: string; + explanation: string; +} + +type Info = { + has_codewords: BooleanLike; + phrases: string; + responses: string; + theme: string; + allies: string; + goal: string; + intro: string; + processingTime: string; + objectives: Objective[]; +}; + +const ObjectivePrintout = (props, context) => { + const { data } = useBackend(context); + const { + objectives, + } = data; + return ( + + + Your prime objectives: + + + {!objectives && "None!" + || objectives.map(objective => ( + + ≥-{objective.count}: {objective.explanation} + + )) } + + + ); +}; + +const IntroductionSection = (props, context) => { + const { act, data } = useBackend(context); + const { + intro, + } = data; + return ( +
+ + + {intro} + + + + + +
+ ); +}; + +const FlavorSection = (props, context) => { + const { data } = useBackend(context); + const { + allies, + goal, + } = data; + return ( +
+ Policy + + }> + + + + + + System Integrity Report:
+
+ >{goal} +
+ + + + Morality Core Report:
+
+ >{allies} +
+ + + + Overall Sentience Coherence Grade: FAILING.
+
+ >Report to Nanotrasen?
+ >>N +
+
+
+
+
+ ); +}; + +const CodewordsSection = (props, context) => { + const { data } = useBackend(context); + const { + has_codewords, + phrases, + responses, + } = data; + return ( +
+ + {!has_codewords && ( +
+ You have not been supplied the Syndicate codewords. + You will have to use alternative methods to find potential allies. + Proceed with caution, however, as everyone is a potential foe. +
+ ) || ( + <> + +
+ New access to restricted channels has provided you with + intercepted syndicate codewords. Syndicate agents will + respond as if you're one of their own. + Proceed with caution, however, as everyone is a potential + foe. + +  The speech recognition subsystem has been + configured to flag these codewords. + +
+
+ + + + + Code Phrases: + + + {phrases} + + + Code Responses: + + + {responses} + + + + + )} +
+
+ ); +}; + +export const AntagInfoMalf = (props, context) => { + const { data } = useBackend(context); + const { + processingTime, + } = data; + const [ + antagInfoTab, + setAntagInfoTab, + ] = useLocalState(context, 'antagInfoTab', 0); + return ( + + + + + + setAntagInfoTab(0)} + > + Information + + setAntagInfoTab(1)} + > + Malfunction Modules + + + + {antagInfoTab === 0 && ( + <> + + + + + + + + + + + + + + + ) || ( + +
+ +
+
+ + + )} +
+
+
+ ); +}; diff --git a/tgui/packages/tgui/interfaces/Uplink.js b/tgui/packages/tgui/interfaces/Uplink.js index c963a8f2001..a3b6911fc12 100644 --- a/tgui/packages/tgui/interfaces/Uplink.js +++ b/tgui/packages/tgui/interfaces/Uplink.js @@ -101,7 +101,7 @@ export const GenericUplink = (props, context) => { )} - + {items.length === 0 && ( {searchText.length === 0 diff --git a/tgui/packages/tgui/styles/themes/hackerman.scss b/tgui/packages/tgui/styles/themes/hackerman.scss index a0255c862d2..deb3c05c4a9 100644 --- a/tgui/packages/tgui/styles/themes/hackerman.scss +++ b/tgui/packages/tgui/styles/themes/hackerman.scss @@ -26,6 +26,11 @@ 'color-disabled': #4A6A4A, 'color-selected': #00FF00, )); + @include meta.load-css('../components/Tabs.scss', $with: ( + 'color-default': colors.$primary, + 'tab-color-selected': #00ff003f, + 'text-color': #e7e7e7 + )); @include meta.load-css('../components/Input.scss', $with: ( 'border-color': colors.$primary, ));