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.

<img width="345" height="138" alt="JvkEnGMkQI"
src="https://github.com/user-attachments/assets/fad89db3-a1a5-42d7-ae54-67ca7ba92301"
/>

You can specify only cliented mobs, as well as the trait source for
removals.

<details><summary>results</summary>


![dreamseeker_K4w1Mmt73N](https://github.com/user-attachments/assets/62025b6a-1c16-4eb5-81ac-e2f5387877dd)

</details>

## 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

🆑
admin: added new admin verb, 'Mass Modify Traits', which can be used to
add or remove a trait to/from every living mob.
/🆑

---------

Co-authored-by: Zephyr <12817816+ZephyrTFA@users.noreply.github.com>
This commit is contained in:
Bloop
2025-11-04 12:29:53 -05:00
committed by GitHub
co-authored by Zephyr
parent ce3e60df72
commit 4aebc85937
3 changed files with 93 additions and 1 deletions
+1 -1
View File
@@ -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.
+90
View File
@@ -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)