From 4aebc85937e1a9c709775e6e57e6d75c93579dd7 Mon Sep 17 00:00:00 2001
From: Bloop <13398309+vinylspiders@users.noreply.github.com>
Date: Tue, 4 Nov 2025 12:29:53 -0500
Subject: [PATCH] Adds a new admin verb, Mass Modify Traits (#93767)
## About The Pull Request
Adds a verb, Mass Modify Traits, which allows an admin to apply or
remove a specific trait from all living mobs.
This lets you do things like give all mobs the `TRAIT_NOBREATHING`, or
making all mobs able to fly.
You can specify only cliented mobs, as well as the trait source for
removals.
results

## Why It's Good For The Game
More tools for admins to get creative with is always a good thing, I
think! The trait system uses macros so it's difficult to use SQL to
mass-apply, so here's an easy verb for it instead.
## Changelog
:cl:
admin: added new admin verb, 'Mass Modify Traits', which can be used to
add or remove a trait to/from every living mob.
/:cl:
---------
Co-authored-by: Zephyr <12817816+ZephyrTFA@users.noreply.github.com>
---
code/__DEFINES/traits/sources.dm | 2 +
code/modules/admin/verbs/admin.dm | 2 +-
code/modules/admin/verbs/adminfun.dm | 90 ++++++++++++++++++++++++++++
3 files changed, 93 insertions(+), 1 deletion(-)
diff --git a/code/__DEFINES/traits/sources.dm b/code/__DEFINES/traits/sources.dm
index 14bf919dc62..f68a242e565 100644
--- a/code/__DEFINES/traits/sources.dm
+++ b/code/__DEFINES/traits/sources.dm
@@ -30,6 +30,8 @@
#define PERSONALITY_TRAIT "personality_trait"
/// (B)admins only.
#define ADMIN_TRAIT "admin"
+/// Traits that were granted via modify_traits in vv or the mass_modify_traits verb
+#define TRAIT_ADMIN_GRANTED "adminabuse"
/// Any traits given through a smite.
#define SMITE_TRAIT "smite"
#define CHANGELING_TRAIT "changeling"
diff --git a/code/modules/admin/verbs/admin.dm b/code/modules/admin/verbs/admin.dm
index c63abb8888b..b7b6d6a0ac1 100644
--- a/code/modules/admin/verbs/admin.dm
+++ b/code/modules/admin/verbs/admin.dm
@@ -125,7 +125,7 @@ ADMIN_VERB(cmd_admin_check_player_exp, R_ADMIN, "Player Playtime", "View player
return
chosen_trait = available_traits[chosen_trait]
- var/source = "adminabuse"
+ var/source = TRAIT_ADMIN_GRANTED
switch(add_or_remove)
if("Add") //Not doing source choosing here intentionally to make this bit faster to use, you can always vv it.
if(GLOB.movement_type_trait_to_flag[chosen_trait]) //include the required element.
diff --git a/code/modules/admin/verbs/adminfun.dm b/code/modules/admin/verbs/adminfun.dm
index 4da149a9c18..883eb6949ef 100644
--- a/code/modules/admin/verbs/adminfun.dm
+++ b/code/modules/admin/verbs/adminfun.dm
@@ -165,6 +165,96 @@ ADMIN_VERB(polymorph_all, R_ADMIN, "Polymorph All", "Applies the effects of the
message_admins("Mass polymorph started by [who_did_it] is complete.")
+/// Allow admin to mass add or remove a trait across all mobs
+ADMIN_VERB(mass_modify_traits, R_FUN, "Mass Modify Traits", "Adds or removes a trait from every mob.", ADMIN_CATEGORY_FUN)
+
+ var/choice = tgui_alert(user, "Add or Remove Trait?", "Mass Add/Remove Trait", list("Add", "Remove"))
+ if(isnull(choice))
+ return
+ var/is_add = (choice == "Add")
+ var/lower_choice = LOWER_TEXT(choice)
+
+ // Build list of valid traits that can be applied to mobs
+ var/list/available_traits = list()
+ for(var/key in GLOB.admin_visible_traits)
+ if(ispath(/mob, key)) // so we get atom and atom/movable traits too, which doing (istype(key, /mob)) would skip
+ available_traits += GLOB.admin_visible_traits[key]
+ if(!length(available_traits))
+ return
+
+ available_traits = sort_list(available_traits, GLOBAL_PROC_REF(cmp_typepaths_asc)) // sort alphabetically
+
+ var/mob_trait = tgui_input_list(user, "Select a trait to [lower_choice].", "Mass [choice] Trait", available_traits)
+ if(isnull(mob_trait))
+ return
+ mob_trait = available_traits[mob_trait]
+
+ var/target_scope = tgui_alert(user, "[choice] [lower_choice == "add" ? "to" : "from"] all mobs, or only cliented ones?", "Scope", list("All", "Cliented"))
+ if(!target_scope)
+ return
+ var/cliented_only = (target_scope == "Cliented")
+
+ // So we get readable trait name to display in the uis
+ if(!GLOB.admin_trait_name_map)
+ GLOB.admin_trait_name_map = generate_admin_trait_name_map()
+ var/trait_name = GLOB.admin_trait_name_map[mob_trait] || mob_trait
+
+ // Ask for confirmation first
+ var/action_word = is_add ? "to" : "from"
+ var/confirm = tgui_alert(
+ user,
+ "Please confirm you want to [lower_choice] [trait_name] [action_word] every [cliented_only ? "cliented" : ""] mob?",
+ "Confirm Mass [choice] Trait",
+ list("Yes", "No")
+ )
+ if(confirm != "Yes")
+ return
+
+ // Perform operation
+ var/affected = 0
+ if(is_add) // Adding trait
+ var/needs_movetype = GLOB.movement_type_trait_to_flag[mob_trait]
+ for(var/mob/mob_to_modify as anything in GLOB.alive_mob_list)
+ if(cliented_only && !mob_to_modify.client)
+ continue
+ if(needs_movetype)
+ mob_to_modify.AddElement(/datum/element/movetype_handler)
+ ADD_TRAIT(mob_to_modify, mob_trait, TRAIT_ADMIN_GRANTED)
+ affected++
+
+ else // Removing trait
+ var/source = null
+ var/remove_mode = tgui_alert(user, "Remove from specific source?", "Mass Remove Trait", list("All", "Admin-Granted Traits", "Specific"))
+ if(isnull(remove_mode))
+ return
+
+ switch(remove_mode)
+ if("Admin-Granted Traits") source = TRAIT_ADMIN_GRANTED
+ if("Specific")
+ source = LOWER_TEXT(tgui_input_text(user, "Enter source", "Mass Remove Trait", max_length = MAX_NAME_LEN))
+ if(isnull(source))
+ return
+
+ for(var/mob/mob_to_modify as anything in GLOB.alive_mob_list)
+ if(cliented_only && !mob_to_modify.client)
+ continue
+ REMOVE_TRAIT(mob_to_modify, mob_trait, source)
+ affected++
+
+ if(affected)
+ var/plural = affected == 1 ? "mob" : "mobs"
+ var/log_msg = "[key_name_admin(user)] mass [lower_choice][is_add ? "ed" : "d"] [trait_name] [action_word] [affected] [plural]."
+ message_admins(log_msg)
+ log_admin(log_msg)
+
+/// Returns only traits that apply to mobs
+/proc/get_mob_admin_traits()
+ var/list/out = list()
+ for(var/key in GLOB.admin_visible_traits)
+ if(ispath(key, /mob)) // key is a mob type or subtype
+ out += GLOB.admin_visible_traits[key]
+ return out
+
ADMIN_VERB_AND_CONTEXT_MENU(admin_smite, R_ADMIN|R_FUN, "Smite", "Smite a player with divine power.", ADMIN_CATEGORY_FUN, mob/living/target in world)
var/punishment = tgui_input_list(user, "Choose a punishment", "DIVINE SMITING", GLOB.smites)