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,
));