From 37845c2100f593da60377c5df7384bedbf4682d5 Mon Sep 17 00:00:00 2001
From: afo <74638922+afonamo@users.noreply.github.com>
Date: Sun, 21 Jun 2026 14:53:33 -0400
Subject: [PATCH] Adds icons to malf module picker UI. Slightly dejanks malf
module buying code. (#96546)
## About The Pull Request
- Adds icons to the malf module picker.
- AI modules can either set icon/icon_state or leave it empty and
fallback to active ability icon (if it has one)
- Removes redundant module picker code from the malf antag datum. Module
picker datum handles both malf and combat upgrade UIs now.
- This split it into two separate UI buttons, but I kinda like it this
way anyways
- General code "improvements".
- Removed unused UI state vars
- Flattened list of modules since it was just getting instantly
flattened by the UI anyways
- Actually utilizes GenericUplink's category sorting instead of handling
it on the backend
## Why It's Good For The Game
Instead of
## Changelog
:cl:
fix: malf ai module picker actually has icons to load for the modules
code: Malf antag datum is no longer responsible for handling module
purchases. You again buy modules through the dedicated module picker
button.
/:cl:
---------
Co-authored-by: afonamos
---
code/modules/antagonists/malf_ai/malf_ai.dm | 50 ------------
.../malf_ai/malf_ai_module_picker.dm | 76 ++++++++-----------
.../antagonists/malf_ai/malf_ai_modules.dm | 11 +++
code/modules/mob/living/silicon/ai/ai.dm | 5 +-
.../tgui/interfaces/AntagInfoMalf.tsx | 65 ++++------------
.../interfaces/MalfunctionModulePicker.tsx | 49 +++++++++++-
.../tgui/interfaces/common/MalfAiModules.tsx | 61 ---------------
7 files changed, 104 insertions(+), 213 deletions(-)
delete mode 100644 tgui/packages/tgui/interfaces/common/MalfAiModules.tsx
diff --git a/code/modules/antagonists/malf_ai/malf_ai.dm b/code/modules/antagonists/malf_ai/malf_ai.dm
index 763ab99c4ce..b46b5420023 100644
--- a/code/modules/antagonists/malf_ai/malf_ai.dm
+++ b/code/modules/antagonists/malf_ai/malf_ai.dm
@@ -18,8 +18,6 @@
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
///malf on_gain sound effect. Set here so Infected AI can override
var/malf_sound = 'sound/music/antag/malf.ogg'
@@ -165,15 +163,12 @@
/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
data["hackedAPCs"] = malf_ai.hacked_apcs.len
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(SStraitor.syndicate_code_phrase, ", ")
@@ -183,53 +178,8 @@
data["goal"] = malfunction_flavor["goal"]
data["objectives"] = get_objectives()
data["can_change_objective"] = can_assign_self_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/malf/mod = malf_ai.malf_picker.possible_modules[category][module]
- cat["items"] += list(list(
- "name" = mod.name,
- "cost" = mod.cost,
- "desc" = mod.description,
- "minimum_apcs" = mod.minimum_apcs,
- ))
- data["categories"] += list(cat)
-
return data
-/datum/antagonist/malf_ai/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
- . = ..()
- 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/malf/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/antagonists/malf_ai/malf_ai_module_picker.dm b/code/modules/antagonists/malf_ai/malf_ai_module_picker.dm
index e99dd0e83d5..f51d10b5b9c 100644
--- a/code/modules/antagonists/malf_ai/malf_ai_module_picker.dm
+++ b/code/modules/antagonists/malf_ai/malf_ai_module_picker.dm
@@ -1,32 +1,20 @@
/// The datum and interface for the malf unlock menu, which lets them choose actions to unlock.
/datum/module_picker
var/name = "Malfunction Modules Menu"
- var/selected_cat
- var/compact_mode = FALSE
var/processing_time = 50
var/list/possible_modules
/datum/module_picker/New()
possible_modules = get_malf_modules()
-/proc/cmp_malfmodules_priority(datum/ai_module/A, datum/ai_module/B)
- return B.cost - A.cost
-
/proc/get_malf_modules()
- var/list/filtered_modules = list()
-
+ var/list/modules = list()
for(var/path in GLOB.malf_modules)
var/datum/ai_module/AM = new path
if((AM.power_type == /datum/action/innate/ai) && !AM.upgrade)
continue
- if(!filtered_modules[AM.category])
- filtered_modules[AM.category] = list()
- filtered_modules[AM.category][AM] = AM
-
- for(var/category in filtered_modules)
- sortTim(filtered_modules[category], GLOBAL_PROC_REF(cmp_malfmodules_priority))
-
- return filtered_modules
+ modules += AM
+ return modules
/datum/module_picker/ui_state(mob/user)
return GLOB.always_state
@@ -40,7 +28,6 @@
/datum/module_picker/ui_data(mob/user)
var/list/data = list()
data["processingTime"] = processing_time
- data["compactMode"] = compact_mode
if(isAI(user))
var/mob/living/silicon/ai/ai_user = user
data["hackedAPCs"] = ai_user.hacked_apcs.len
@@ -48,21 +35,28 @@
/datum/module_picker/ui_static_data(mob/user)
var/list/data = list()
-
data["categories"] = list()
- for(var/category in possible_modules)
- var/list/cat = list(
- "name" = category,
- "items" = (category == selected_cat ? list() : null))
- for(var/module in possible_modules[category])
- var/datum/ai_module/AM = possible_modules[category][module]
- cat["items"] += list(list(
- "name" = AM.name,
- "cost" = AM.cost,
- "desc" = AM.description,
- "minimum_apcs" = AM.minimum_apcs,
- ))
- data["categories"] += list(cat)
+ data["modules"] = list()
+
+ for(var/datum/ai_module/module as anything in possible_modules)
+ var/icon_state = module.icon_state
+ var/icon = module.icon
+ if (!module.icon_state && !module.upgrade && module.power_type)
+ var/datum/action/innate/ai/active_ability = module.power_type
+ icon = active_ability.button_icon
+ icon_state = active_ability.button_icon_state
+
+ data["modules"] += list(list(
+ "name" = module.name,
+ "icon" = icon,
+ "icon_state" = icon_state,
+ "cost" = module.cost,
+ "desc" = module.description,
+ "category" = module.category,
+ "minimumApcs" = module.minimum_apcs,
+ ))
+ if (!(module.category in data["categories"]))
+ data["categories"] += module.category
return data
@@ -75,20 +69,10 @@
switch(action)
if("buy")
var/item_name = params["name"]
- var/list/buyable_items = list()
- for(var/category in possible_modules)
- buyable_items += possible_modules[category]
- for(var/key in buyable_items)
- var/datum/ai_module/AM = buyable_items[key]
- if(AM.name == item_name)
- purchase_module(usr, AM)
+ for(var/datum/ai_module/module as anything in possible_modules)
+ if(module.name == item_name)
+ purchase_module(usr, module)
return TRUE
- if("select")
- selected_cat = params["category"]
- return TRUE
- if("compact_toggle")
- compact_mode = !compact_mode
- return TRUE
/datum/module_picker/proc/purchase_module(mob/living/silicon/ai/AI, datum/ai_module/AM)
if(!istype(AM))
@@ -99,11 +83,10 @@
return
if(AM.minimum_apcs > AI.hacked_apcs.len)
return
- var/datum/action/innate/ai/action = locate(AM.power_type) in AI.actions
// Give the power and take away the money.
if(AM.upgrade) //upgrade and upgrade() are separate, be careful!
AM.upgrade(AI)
- possible_modules[AM.category] -= AM
+ possible_modules -= AM
if(AM.unlock_text)
to_chat(AI, AM.unlock_text)
if(AM.unlock_sound)
@@ -111,12 +94,13 @@
update_static_data(AI)
else
if(AM.power_type)
+ var/datum/action/innate/ai/action = locate(AM.power_type) in AI.actions
if(!action) //Unlocking for the first time
var/datum/action/AC = new AM.power_type
AC.Grant(AI)
AI.current_modules += new AM.type
if(AM.one_purchase)
- possible_modules[AM.category] -= AM
+ possible_modules -= AM
update_static_data(AI)
if(AM.unlock_text)
to_chat(AI, AM.unlock_text)
diff --git a/code/modules/antagonists/malf_ai/malf_ai_modules.dm b/code/modules/antagonists/malf_ai/malf_ai_modules.dm
index 5f810a1e37c..9c2d376de9c 100644
--- a/code/modules/antagonists/malf_ai/malf_ai_modules.dm
+++ b/code/modules/antagonists/malf_ai/malf_ai_modules.dm
@@ -122,6 +122,9 @@ GLOBAL_LIST_INIT(malf_modules, subtypesof(/datum/ai_module/malf))
var/name = "generic module"
var/category = "generic category"
var/description = "generic description"
+ /// Icons for the module picker interface. If icon_state is not set, the interface will attempt to use the active ability's icon.
+ var/icon = 'icons/mob/actions/actions_AI.dmi'
+ var/icon_state = ""
var/cost = 5
/// Minimum amount of APCs that has to be under the AI's control to purchase this module.
var/minimum_apcs = 0
@@ -846,6 +849,8 @@ GLOBAL_LIST_INIT(malf_modules, subtypesof(/datum/ai_module/malf))
/datum/ai_module/malf/upgrade/upgrade_cameras
name = "Upgrade Camera Network"
description = "Install broad-spectrum scanning and electrical redundancy firmware to the camera network, enabling EMP-proofing and light-amplified X-ray vision. Upgrade is done immediately upon purchase." //I <3 pointless technobabble
+ icon = 'icons/obj/machines/camera.dmi'
+ icon_state = "xraycamera"
//This used to have motion sensing as well, but testing quickly revealed that giving it to the whole cameranet is PURE HORROR.
cost = 35 //Decent price for omniscience!
upgrade = TRUE
@@ -883,6 +888,8 @@ GLOBAL_LIST_INIT(malf_modules, subtypesof(/datum/ai_module/malf))
/datum/ai_module/malf/upgrade/upgrade_turrets
name = "AI Turret Upgrade"
description = "Improves the power and health of all AI turrets. This effect is permanent. Upgrade is done immediately upon purchase."
+ icon = 'icons/obj/weapons/turrets.dmi'
+ icon_state = "standard_off"
cost = 30
upgrade = TRUE
unlock_text = span_notice("You establish a power diversion to your turrets, upgrading their health and damage.")
@@ -901,6 +908,8 @@ GLOBAL_LIST_INIT(malf_modules, subtypesof(/datum/ai_module/malf))
name = "Enhanced Surveillance"
description = "Via a combination of hidden microphones and lip reading software, \
you are able to use your cameras to listen in on conversations. Upgrade is done immediately upon purchase."
+ icon = 'icons/obj/service/broadcast.dmi'
+ icon_state = "microphone"
cost = 30
upgrade = TRUE
unlock_text = span_notice("OTA firmware distribution complete! Cameras upgraded: Enhanced surveillance package online.")
@@ -916,6 +925,8 @@ GLOBAL_LIST_INIT(malf_modules, subtypesof(/datum/ai_module/malf))
description = "Allows you to hack into a mech's onboard computer, shunting all processes into it and ejecting any occupants. \
Upgrade is done immediately upon purchase. Do not allow the mech to leave the station's vicinity or allow it to be destroyed. \
If your core is destroyed, you will be lose connection with the Doomsday Device and the countdown will cease."
+ icon = 'icons/mob/rideables/mecha.dmi'
+ icon_state = "durand"
cost = 30
upgrade = TRUE
unlock_text = span_notice("Virus package compiled. Select a target mech at any time. You must remain on the station at all times. \
diff --git a/code/modules/mob/living/silicon/ai/ai.dm b/code/modules/mob/living/silicon/ai/ai.dm
index 0849b20eed9..f8309987078 100644
--- a/code/modules/mob/living/silicon/ai/ai.dm
+++ b/code/modules/mob/living/silicon/ai/ai.dm
@@ -895,9 +895,8 @@
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
- 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)
+ modules_action = new(malf_picker)
+ modules_action.Grant(src)
/mob/living/silicon/ai/reset_perspective(atom/new_eye)
SHOULD_CALL_PARENT(FALSE) // I hate you all
diff --git a/tgui/packages/tgui/interfaces/AntagInfoMalf.tsx b/tgui/packages/tgui/interfaces/AntagInfoMalf.tsx
index 0d589f041c6..4cb55e74320 100644
--- a/tgui/packages/tgui/interfaces/AntagInfoMalf.tsx
+++ b/tgui/packages/tgui/interfaces/AntagInfoMalf.tsx
@@ -1,10 +1,9 @@
-import { useState } from 'react';
-import { BlockQuote, Button, Section, Stack, Tabs } from 'tgui-core/components';
+import { BlockQuote, Button, Section, Stack } from 'tgui-core/components';
import type { BooleanLike } from 'tgui-core/react';
import { useBackend } from '../backend';
import { Window } from '../layouts';
-import { MalfAiModules } from './common/MalfAiModules';
+
import {
type Objective,
ObjectivePrintout,
@@ -178,63 +177,29 @@ function CodewordsSection(props) {
);
}
-enum Screen {
- Intro,
- Modules,
-}
export function AntagInfoMalf(props) {
- const [antagInfoTab, setAntagInfoTab] = useState(Screen.Intro);
-
return (
-
-
- setAntagInfoTab(Screen.Intro)}
- >
- Information
-
- setAntagInfoTab(Screen.Modules)}
- >
- Malfunction Modules
-
-
+
+
+
+
+
+
+
+
+
+
+
+
- {antagInfoTab === Screen.Intro ? (
- <>
-
-
-
-
-
-
-
-
-
-
-
-
-
- >
- ) : (
-
-
-
- )}
diff --git a/tgui/packages/tgui/interfaces/MalfunctionModulePicker.tsx b/tgui/packages/tgui/interfaces/MalfunctionModulePicker.tsx
index 199bf699047..dcf818802a7 100644
--- a/tgui/packages/tgui/interfaces/MalfunctionModulePicker.tsx
+++ b/tgui/packages/tgui/interfaces/MalfunctionModulePicker.tsx
@@ -1,11 +1,54 @@
import { Window } from '../layouts';
-import { MalfAiModules } from './common/MalfAiModules';
+import { useBackend } from '../backend';
+import { GenericUplink, type Item } from './Uplink/GenericUplink';
+
+type MalfModuleData = {
+ name: string,
+ icon: string,
+ icon_state: string,
+ cost: number,
+ desc: string,
+ category: string,
+ minimumApcs: number,
+};
+
+type Data = {
+ processingTime: number;
+ hackedAPCs: number;
+ categories: string[];
+ modules: MalfModuleData[];
+};
export function MalfunctionModulePicker(props) {
+ const { act, data } = useBackend();
+ const { processingTime, hackedAPCs, categories } = data;
+
+ const items: Item[] = data.modules.map((module) => ({
+ category: module.category,
+ cost: `${module.cost} PT`,
+ desc:
+ module.desc +
+ (module.minimumApcs > 0
+ ? ` Requires at least ${module.minimumApcs} APCs hacked.`
+ : ''),
+ disabled: processingTime < module.cost || hackedAPCs < module.minimumApcs,
+ id: module.name,
+ name: module.name,
+ icon: module.icon,
+ icon_state: module.icon_state,
+ population_tooltip: '',
+ insufficient_population: false,
+ }))
+
return (
-
+
-
+ act('buy', { name: item.name })}
+ />
);
diff --git a/tgui/packages/tgui/interfaces/common/MalfAiModules.tsx b/tgui/packages/tgui/interfaces/common/MalfAiModules.tsx
deleted file mode 100644
index c6fc61c7e7c..00000000000
--- a/tgui/packages/tgui/interfaces/common/MalfAiModules.tsx
+++ /dev/null
@@ -1,61 +0,0 @@
-import { useBackend } from '../../backend';
-import { GenericUplink, type Item } from '../Uplink/GenericUplink';
-
-type Category = {
- name: string;
- items: MalfItem[];
-};
-/* This is shitcode, but someone used normal uplink so i feel justified */
-type MalfItem = Item & {
- minimum_apcs: number;
-};
-
-type Data = {
- processingTime: string;
- hackedAPCs: number;
- categories: Category[];
-};
-
-/** Common ui for selecting malf ai modules */
-export function MalfAiModules(props) {
- const { act, data } = useBackend();
- const { processingTime, hackedAPCs, categories = [] } = data;
-
- const categoriesList: string[] = [];
- const items: MalfItem[] = [];
-
- for (let idx = 0; idx < categories.length; idx++) {
- const category = categories[idx];
- categoriesList.push(category.name);
-
- for (let itemIndex = 0; itemIndex < category.items?.length; itemIndex++) {
- const item = category.items[itemIndex];
- items.push({
- category: category.name,
- cost: `${item.cost} PT`,
- desc:
- item.desc +
- (item.minimum_apcs > 0
- ? ` Requires at least ${item.minimum_apcs} APCs hacked.`
- : ''),
- disabled: processingTime < item.cost || hackedAPCs < item.minimum_apcs,
- icon_state: item.icon_state,
- icon: item.icon,
- id: item.name,
- name: item.name,
- population_tooltip: '',
- insufficient_population: false,
- minimum_apcs: item.minimum_apcs || 0, // Handle the case where minimum_apcs is not defined
- });
- }
- }
-
- return (
- act('buy', { name: item.name })}
- />
- );
-}