diff --git a/code/modules/admin/verbs/admingame.dm b/code/modules/admin/verbs/admingame.dm
index 0f7e2eed297..23bfc44c999 100644
--- a/code/modules/admin/verbs/admingame.dm
+++ b/code/modules/admin/verbs/admingame.dm
@@ -8,6 +8,14 @@ ADMIN_VERB_ONLY_CONTEXT_MENU(show_player_panel, R_ADMIN, "Show Player Panel", mo
to_chat(user, span_warning("You seem to be selecting a mob that doesn't exist anymore."), confidential = TRUE)
return
+ // BUBBER EDIT ADDITION START
+ if(user.prefs.read_preference(/datum/preference/toggle/use_tgui_player_panel) && !GLOB.tgui_fallback_enabled)
+ if(isnull(player.mob_panel))
+ player.create_player_panel()
+ player.mob_panel.ui_interact(user.mob)
+ return
+ // BUBBER EDIT ADDITION END
+
var/body = "
Options for [player.key]"
body += "Options panel for [player]"
if(player.client)
diff --git a/modular_zubbers/code/_globalvars/tgui_fallback.dm b/modular_zubbers/code/_globalvars/tgui_fallback.dm
new file mode 100644
index 00000000000..e56b14b53d1
--- /dev/null
+++ b/modular_zubbers/code/_globalvars/tgui_fallback.dm
@@ -0,0 +1,2 @@
+/// If true, TGUI will use the old fallback UI instead of the default TGUI interface.
+GLOBAL_VAR(tgui_fallback_enabled)
diff --git a/modular_zubbers/code/modules/admin/player_panel.dm b/modular_zubbers/code/modules/admin/player_panel.dm
new file mode 100644
index 00000000000..4bb98782c2c
--- /dev/null
+++ b/modular_zubbers/code/modules/admin/player_panel.dm
@@ -0,0 +1,540 @@
+GLOBAL_LIST_INIT(mute_bits, list(
+ list(name = "IC", bitflag = MUTE_IC),
+ list(name = "OOC", bitflag = MUTE_OOC),
+ list(name = "LOOC", bitflag = MUTE_LOOC),
+ list(name = "Pray", bitflag = MUTE_PRAY),
+ list(name = "Ahelp", bitflag = MUTE_ADMINHELP),
+ list(name = "Deadchat", bitflag = MUTE_DEADCHAT)
+))
+
+GLOBAL_DATUM_INIT(admin_state, /datum/ui_state/admin_state, new)
+
+/datum/ui_state/admin_state/can_use_topic(src_object, mob/user)
+ if(check_rights_for(user.client, R_ADMIN))
+ return UI_INTERACTIVE
+ return UI_CLOSE
+
+GLOBAL_LIST_INIT(pp_limbs, list(
+ "Head" = BODY_ZONE_HEAD,
+ "Left leg" = BODY_ZONE_L_LEG,
+ "Right leg" = BODY_ZONE_R_LEG,
+ "Left arm" = BODY_ZONE_L_ARM,
+ "Right arm" = BODY_ZONE_R_ARM
+))
+
+/datum/player_panel
+ var/mob/target_mob
+ var/client/target_client
+
+/datum/player_panel/New(mob/target)
+ . = ..()
+ target_mob = target
+
+/datum/player_panel/Destroy(force, ...)
+ target_mob = null
+ target_client = null
+
+ SStgui.close_uis(src)
+ return ..()
+
+/datum/player_panel/ui_interact(mob/user, datum/tgui/ui)
+ if(!target_mob)
+ return
+
+ ui = SStgui.try_update_ui(user, src, ui)
+ if (!ui)
+ ui = new(user, src, "PlayerPanel", "[target_mob.real_name] Player Panel")
+ ui.open()
+
+/datum/player_panel/ui_state(mob/user)
+ return GLOB.admin_state
+
+/datum/player_panel/ui_data(mob/user)
+ . = list()
+ .["mob_name"] = target_mob.real_name
+ .["mob_type"] = target_mob.type
+ .["admin_mob_type"] = user.client?.mob.type
+ .["godmode"] = HAS_TRAIT(user, TRAIT_GODMODE)
+
+ var/mob/living/living_mob = target_mob
+ if (istype(living_mob))
+ .["is_frozen"] = living_mob.admin_frozen
+ .["is_slept"] = living_mob.admin_sleeping
+ .["mob_scale"] = living_mob.current_size
+
+ if(target_mob.client)
+ target_client = target_mob.client
+ .["client_ckey"] = target_client.ckey
+ .["client_muted"] = target_client.prefs.muted
+ .["client_rank"] = target_client.holder ? target_client.holder.ranks : "Player"
+ else
+ target_client = null
+ .["client_ckey"] = null
+
+ if (target_mob.ckey)
+ .["last_ckey"] = copytext(target_mob.ckey, 2)
+
+/datum/player_panel/ui_static_data()
+ . = list()
+
+ .["transformables"] = GLOB.pp_transformables
+ .["glob_limbs"] = GLOB.pp_limbs
+ .["glob_mute_bits"] = GLOB.mute_bits
+ .["current_time"] = time2text(world.timeofday, "YYYY-MM-DD hh:mm:ss")
+
+ if(target_client)
+ var/byond_version = "Unknown"
+ if(target_client.byond_version)
+ byond_version = "[target_client.byond_version].[target_client.byond_build ? target_client.byond_build : "xxx"]"
+ .["data_byond_version"] = byond_version
+ .["data_player_join_date"] = target_client.player_join_date
+ .["data_account_join_date"] = target_client.account_join_date
+ .["data_related_cid"] = target_client.related_accounts_cid
+ .["data_related_ip"] = target_client.related_accounts_ip
+ /* // Find relevant PR maybe?
+ var/datum/player_details/deets = GLOB.player_details[target_client.ckey]
+ .["data_old_names"] = deets.get_played_names() || null
+ */
+ var/list/player_ranks = list()
+ if(SSplayer_ranks.is_donator(target_client, admin_bypass = FALSE))
+ player_ranks += "Donator"
+ if(SSplayer_ranks.is_mentor(target_client, admin_bypass = FALSE))
+ player_ranks += "Mentor"
+ if(SSplayer_ranks.is_vetted(target_client, admin_bypass = FALSE))
+ player_ranks += "Vetted"
+ .["ranks"] = length(player_ranks) ? player_ranks.Join(", ") : null
+
+ if(CONFIG_GET(flag/use_exp_tracking))
+ .["playtimes_enabled"] = TRUE
+ .["playtime"] = target_mob.client.get_exp_living()
+
+/datum/player_panel/ui_act(action, params, datum/tgui/ui)
+ . = ..()
+
+ var/mob/admin_mob = ui.user
+ var/client/admin_client = admin_mob.client
+
+ if(. || !check_rights_for(admin_client, R_ADMIN))
+ message_admins(span_adminhelp("WARNING: NON-ADMIN [ADMIN_LOOKUPFLW(admin_mob)] ATTEMPTED TO ACCESS ADMIN PANEL. NOTIFY Casper3044."))
+ to_chat(admin_client, "Error: you are not an admin!")
+ return
+
+ switch(action)
+ // If this mob used to be player controlled but isn't anymore, this action will open the player panel for the mob that player is now controlling.
+ if ("open_latest_panel")
+ if (target_mob.client || !target_mob.ckey)
+ return
+
+ // Remove '@' from the start of the ckey.
+ var/ckey = copytext(target_mob.ckey, 2)
+ var/mob/latest_mob = get_mob_by_ckey(ckey)
+
+ if(!latest_mob)
+ to_chat(admin_client, span_warning("That ckey is not controlling a mob."))
+ return
+
+ if(target_mob == latest_mob)
+ return
+
+ to_chat(admin_client, span_notice("New mob found for player: [target_mob.ckey] ([latest_mob])."))
+ SSadmin_verbs.dynamic_invoke_verb(admin_client, /datum/admin_verb/show_player_panel, latest_mob)
+
+ /// Edits player Rank
+ if ("edit_rank")
+ if (!target_mob.client?.ckey)
+ return
+
+ var/list/context = list()
+
+ context["key"] = target_mob.client.ckey
+
+ if (GLOB.admin_datums[target_mob.client.ckey] || GLOB.deadmins[target_mob.client.ckey])
+ context["editrights"] = "rank"
+ else
+ context["editrights"] = "add"
+
+ admin_client.holder.edit_rights_topic(context)
+
+ /// Opens the view variables list
+ if ("access_variables")
+ admin_client.debug_variables(target_mob)
+
+ /// Sees selected player/client playtime
+ if ("access_playtimes")
+ if (target_mob.client)
+ admin_client.holder.cmd_show_exp_panel(target_mob.client)
+
+ /// Privately messages player
+ if ("private_message")
+ SSadmin_verbs.dynamic_invoke_verb(admin_client, /datum/admin_verb/cmd_admin_pm_context, target_mob)
+
+ /// Subtly messages selected mob (requires target to have a headset)
+ if ("subtle_message")
+ var/list/subtle_message_options = list("Voice in head", RADIO_CHANNEL_CENTCOM, RADIO_CHANNEL_SYNDICATE)
+ var/sender = tgui_input_list(admin_client, "Choose the method of subtle messaging", "Subtle Message", subtle_message_options)
+ if (!sender)
+ return
+
+ var/msg = input("Contents of the message", text("Subtle PM to [target_mob.key]")) as text
+ if (!msg)
+ return
+
+ if (sender == "Voice in head")
+ to_chat(target_mob, "You hear a voice in your head... [msg]")
+ else
+ var/mob/living/carbon/human/selected_mob = target_mob
+
+ if(!istype(selected_mob))
+ to_chat(admin_client, "The person you are trying to contact is not human. Unsent message: [msg]")
+ return
+
+ if(!istype(selected_mob.ears, /obj/item/radio/headset))
+ to_chat(admin_client, "The person you are trying to contact is not wearing a headset. Unsent message: [msg]")
+ return
+
+ to_chat(selected_mob, "You hear something crackle in your ears for a moment before a voice speaks. \"Please stand by for a message from [sender == RADIO_CHANNEL_SYNDICATE ? "your benefactor" : "Central Command"]. Message as follows[sender == RADIO_CHANNEL_SYNDICATE ? ", agent." : ":"] [msg]. Message ends.\"")
+
+
+ log_admin("SubtlePM ([sender]): [key_name(admin_client)] -> [key_name(target_mob)] : [msg]")
+ msg = span_adminnotice(" SubtleMessage ([sender]): [key_name_admin(admin_client)] -> [key_name_admin(target_mob)] : [msg]")
+ message_admins(msg)
+ admin_ticket_log(target_mob, msg)
+
+ /// Forces a name change on selected player
+ if ("set_name")
+ target_mob.vv_auto_rename(params["name"])
+
+ /// Admin heals
+ if ("heal")
+ SSadmin_verbs.dynamic_invoke_verb(admin_client, /datum/admin_verb/cmd_admin_rejuvenate, target_mob)
+
+ /// Forces selected player/client into ghost, disconnecting them from mob.
+ if ("ghost")
+ if(target_mob.client)
+ log_admin("[key_name(admin_client)] ejected [key_name(target_mob)] from their body.")
+ message_admins("[key_name_admin(admin_client)] ejected [key_name_admin(target_mob)] from their body.")
+ to_chat(target_mob, span_danger("An admin has ejected you from your body."))
+ target_mob.ghostize(FALSE)
+
+ /// offers control to ghosts for selected mob/body
+ if ("offer_control")
+ offer_control(target_mob)
+
+ /// Steals control from selected client's body
+ if ("take_control")
+ // Disassociates observer mind from the body mind
+ if(target_mob.client)
+ target_mob.ghostize(FALSE)
+ else
+ for(var/mob/dead/observer/ghost in GLOB.dead_mob_list)
+ if(target_mob.mind == ghost.mind)
+ ghost.mind = null
+
+ target_mob.ckey = admin_mob.ckey
+ qdel(admin_mob)
+
+ message_admins(span_adminnotice("[key_name_admin(admin_client)] took control of [target_mob]."))
+ log_admin("[key_name(admin_client)] took control of [target_mob].")
+ addtimer(CALLBACK(target_mob.mob_panel, TYPE_PROC_REF(/datum, ui_interact), target_mob), 0.1 SECONDS)
+
+ /// Smites selected Client/Target
+ if ("smite")
+ SSadmin_verbs.dynamic_invoke_verb(admin_client, /datum/admin_verb/admin_smite, target_mob)
+
+ /// Brings selected Client/target
+ if ("bring")
+ SSadmin_verbs.dynamic_invoke_verb(admin_client, /datum/admin_verb/get_mob, target_mob)
+
+ /// Orbits arround selected Target
+ if ("orbit")
+ if(!isobserver(admin_mob))
+ SSadmin_verbs.dynamic_invoke_verb(admin_client, /datum/admin_verb/admin_ghost)
+ var/mob/dead/observer/satellite = admin_client.mob
+ satellite.ManualFollow(target_mob)
+
+ /// Jumps to selected mob
+ if ("jump_to")
+ SSadmin_verbs.dynamic_invoke_verb(admin_client, /datum/admin_verb/jump_to_mob, target_mob)
+
+ /// Forces the selected mob/client to stop moving
+ if ("freeze")
+ var/mob/living/living_mob = target_mob
+ if (istype(living_mob))
+ living_mob.toggle_admin_freeze(admin_client)
+
+ /// Forces slected mob/client to sleep
+ if ("sleep")
+ var/mob/living/living_mob = target_mob
+ if (istype(living_mob))
+ living_mob.toggle_admin_sleep(admin_client)
+
+ /// Yeets target client to the lobby (only works on ghosts)
+ if ("lobby")
+ if(!isobserver(target_mob))
+ to_chat(admin_client, span_notice("You can only send ghost players back to the Lobby."))
+ return
+
+ if(!target_mob.client)
+ to_chat(admin_client, span_warning("[target_mob] doesn't seem to have an active client."))
+ return
+
+ log_admin("[key_name(admin_client)] has sent [key_name(target_mob)] back to the Lobby.")
+ message_admins("[key_name(admin_client)] has sent [key_name(target_mob)] back to the Lobby.")
+
+ var/mob/dead/new_player/new_connected_player = new()
+ new_connected_player.ckey = target_mob.ckey
+ qdel(target_mob)
+
+ /// Selects admin equipmeent via Equipment UI on the selected player/mob
+ if ("select_equipment")
+ SSadmin_verbs.dynamic_invoke_verb(admin_client, /datum/admin_verb/select_equipment, target_mob)
+
+ /// Forces selecteed client to drop all their stuff (SPREAD YOUR SHIT)
+ if ("strip")
+ for(var/obj/item/begone_items in target_mob)
+ target_mob.dropItemToGround(begone_items, TRUE) //The TRUE forces all items to drop, since this is an admin undress.
+
+ /// Forces selected client into cryo storage
+ if ("cryo")
+ target_mob.vv_send_cryo()
+
+ /// Forces selected client to say things against their will
+ if ("force_say")
+ target_mob.say(params["to_say"], forced="admin")
+
+ /// Forces selected client to emote against their will
+ if ("force_emote")
+ if (params["to_emote"])
+ QUEUE_OR_CALL_VERB_FOR(VERB_CALLBACK(target_mob, TYPE_PROC_REF(/mob, emote), "me", EMOTE_VISIBLE|EMOTE_AUDIBLE, params["to_emote"], TRUE), SSspeech_controller)
+
+ /// Sends the offender to SUPERJAIL known as SPACE PRISON (admin prison)
+ if ("prison")
+ if(isAI(target_mob))
+ to_chat(admin_client, "This cannot be used on instances of type /mob/living/silicon/ai.")
+ return
+
+ target_mob.forceMove(pick(GLOB.prisonwarp))
+ to_chat(target_mob, span_userdanger("You have been sent to Prison!"))
+
+ log_admin("[key_name(admin_client)] has sent [key_name(target_mob)] to Prison!")
+ message_admins("[key_name_admin(admin_client)] has sent [key_name_admin(target_mob)] to Prison!")
+
+ /// Boots the offending client from the server
+ if ("kick")
+ if(!check_if_greater_rights_than(target_client))
+ to_chat(admin_client, span_danger("Error: They have more rights than you do."), confidential = TRUE)
+ return
+ if(tgui_alert(admin_mob, "Kick [key_name(target_mob)]?", "Confirm", list("Yes", "No")) != "Yes")
+ return
+ if(!target_mob)
+ to_chat(admin_client, span_danger("Error: [target_mob] no longer exists!"), confidential = TRUE)
+ return
+ if(!target_client)
+ to_chat(admin_client, span_danger("Error: [target_mob] no longer has a client!"), confidential = TRUE)
+ return
+ to_chat(target_mob, span_danger("You have been kicked from the server by [admin_client.holder.fakekey ? "an Administrator" : "[admin_client.key]"]."), confidential = TRUE)
+ log_admin("[key_name(admin_client)] kicked [key_name(target_mob)].")
+ message_admins(span_adminnotice("[key_name_admin(admin_client)] kicked [key_name_admin(target_mob)]."))
+ qdel(target_client)
+
+ /// Bans target
+ if ("ban")
+ var/player_key = target_mob.key
+ var/player_ip = target_mob.client.address
+ var/player_cid = target_mob.client.computer_id
+ admin_client.holder.ban_panel(player_key, player_ip, player_cid)
+
+ /// Stickbans target
+ if ("sticky_ban")
+ var/list/ban_settings = list()
+ if(target_mob.client)
+ ban_settings["ckey"] = target_mob.ckey
+ admin_client.holder.stickyban("add", ban_settings)
+
+ /// Opens selected target's Notes
+ if ("notes")
+ if (target_mob.client)
+ browse_messages(target_ckey = ckey(target_mob.ckey))
+
+ /// Opens selected target's logs
+ if ("logs")
+ var/source = target_mob.client ? LOGSRC_CKEY : LOGSRC_MOB
+ show_individual_logging_panel(target_mob, source)
+
+ /// Just mutes
+ if ("mute")
+ if(!target_mob.client)
+ return
+
+ target_mob.client.prefs.muted = text2num(params["mute_flag"])
+ log_admin("[key_name(admin_client)] set the mute flags for [key_name(target_mob)] to [target_mob.client.prefs.muted].")
+
+ /// MUTES EVERYBODY (NO ONE GETS TALKING STICK!!!)
+ if ("mute_all")
+ if(!target_mob.client)
+ return
+
+ for(var/bit in GLOB.mute_bits)
+ target_mob.client.prefs.muted |= bit["bitflag"]
+
+ log_admin("[key_name(admin_client)] mass-muted [key_name(target_mob)].")
+
+ /// Unmutes EVERYBODY
+ if ("unmute_all")
+ if(!target_mob.client)
+ return
+
+ for(var/bit in GLOB.mute_bits)
+ target_mob.client.prefs.muted &= ~bit["bitflag"]
+
+ log_admin("[key_name(admin_client)] mass-unmuted [key_name(target_mob)].")
+
+ /// Looks for related account data to the selected mob
+ if ("related_accounts")
+ if(target_mob.client)
+ var/related_accounts
+ if (params["related_thing"] == "CID")
+ related_accounts = target_mob.client.related_accounts_cid
+ else
+ related_accounts = target_mob.client.related_accounts_ip
+
+ related_accounts = splittext(related_accounts, ", ")
+
+ var/list/dat = list("Related accounts by [params["related_thing"]]:")
+ dat += related_accounts
+ admin_client << browse(dat.Join("
"), "window=related_[target_mob.client];size=420x300")
+
+ /// Transforms the selected mob
+ if ("transform")
+ var/choice = params["newType"]
+ if (choice == "/mob/living")
+ choice = tgui_input_list(admin_client, "What should this mob transform into", "Mob Transform", subtypesof(choice))
+ if (!choice)
+ return
+
+ admin_client.holder.transform_mob(target_mob, admin_mob, choice, params["newTypeName"])
+
+ /// Gives targeted mob GOD (its only invulnerability)
+ if ("toggle_godmode")
+ admin_client.cmd_admin_godmode(target_mob)
+
+ /// Gives targeted mob spells (shadow wizard money gang)
+ if ("spell")
+ SSadmin_verbs.dynamic_invoke_verb(admin_client, /datum/admin_verb/give_spell, target_mob)
+
+ /// Gives targeted mob quirks
+ if ("martial_art")
+ admin_client.teach_martial_art(target_mob)
+
+ /// Sets targeted mob's quirks
+ if ("quirk")
+ admin_client.toggle_quirk(target_mob)
+
+ /// Sets targeted mob's species
+ if ("species")
+ admin_client.set_species(target_mob)
+
+ /// Delimbs targeted mob (SNAAAAAAAAAAKE!!!!)
+ if ("limb")
+ if(!params["limbs"] || !ishuman(target_mob))
+ return
+
+ var/mob/living/carbon/human/punished_mob = target_mob
+
+ for(var/limb in params["limbs"])
+ if (!limb)
+ continue
+
+ if (params["delimb_mode"])
+ var/obj/item/bodypart/targeted_limb = punished_mob.get_bodypart(limb)
+ if (!targeted_limb)
+ continue
+ targeted_limb.dismember()
+ playsound(punished_mob, 'sound/effects/bamf.ogg', 70)
+ else
+ punished_mob.regenerate_limb(limb)
+
+ /// Assigns selected olayer/client's scale
+ if ("scale")
+ var/mob/living/local_mob_data = target_mob
+ if(!isnull(params["new_scale"]) && istype(local_mob_data))
+ local_mob_data.vv_edit_var("current_size", params["new_scale"])
+
+ /// Explodes the selected player with assigned power and blasts (for the funny of course!)
+ if ("explode")
+ var/power = text2num(params["power"])
+ var/emp_mode = text2num(params["emp_mode"])
+
+
+ var/turf/target_turf = get_turf(admin_mob)
+ message_admins("[ADMIN_LOOKUPFLW(admin_client)] created an admin [emp_mode ? "EMP" : "explosion"] at [ADMIN_VERBOSEJMP(target_turf)].")
+ log_admin("[key_name(admin_client)] created an admin [emp_mode ? "EMP" : "explosion"] at [admin_mob.loc].")
+
+ if (emp_mode)
+ empulse(admin_mob, power, power / 2, TRUE)
+ else
+ explosion(admin_mob, power / 3, power / 2, power, power, ignorecap = TRUE)
+
+ /// Narrates typed texxt to the selected client's chatboxx
+ if ("narrate")
+ var/list/raw_styles = params["classes"]
+
+ var/styles = ""
+ for(var/style in raw_styles)
+ styles += "[style]:[raw_styles[style]];"
+
+ if (params["mode_global"])
+ to_chat(world, "[params["message"]]")
+ log_admin("GlobalNarrate: [key_name(admin_client)] : [params["message"]]")
+ message_admins(span_adminnotice("[key_name_admin(admin_client)] Sent a global narrate"))
+ else
+ for(var/mob/individual_mob in view(params["range"], admin_mob))
+ to_chat(individual_mob, "[params["message"]]")
+
+ log_admin("LocalNarrate: [key_name(admin_client)] at [AREACOORD(admin_mob)]: [params["message"]]")
+ message_admins(span_adminnotice(" LocalNarrate: [key_name_admin(admin_client)] at [ADMIN_VERBOSEJMP(admin_mob)]: [params["message"]]
"))
+
+ /// Opens languages panel for the selected player/client
+ if ("languages")
+ var/datum/language_holder/selected_character = target_mob.get_language_holder()
+ selected_character.open_language_menu(admin_mob)
+
+ /// Opens the Traitor Panel for the selected player/client
+ if ("traitor_panel")
+ SSadmin_verbs.dynamic_invoke_verb(admin_client, /datum/admin_verb/show_traitor_panel, target_mob)
+
+ /// Opens the selected player/client's skills panel
+ if ("skill_panel")
+ SSadmin_verbs.dynamic_invoke_verb(admin_client, /datum/admin_verb/show_skill_panel, target_mob)
+
+ /// Forces a commendation to selected client/player
+ if ("commend")
+ if(!target_mob.ckey)
+ to_chat(admin_client, span_warning("This mob either no longer exists or no longer is being controlled by someone!"))
+ return
+
+ switch(tgui_alert(admin_mob, "Would you like the effects to apply immediately or at the end of the round? Applying them now will make it clear it was an admin commendation.", "<3?", list("Apply now", "Apply at round end", "Cancel")))
+ if("Apply now")
+ target_mob.receive_heart(admin_mob, instant = TRUE)
+ if("Apply at round end")
+ target_mob.receive_heart(admin_mob)
+
+ /// Plays a selected sound to target client
+ if ("play_sound_to")
+ var/sound_file = input("", "Select a sound file",) as null|sound
+
+ if(sound_file && target_mob)
+ SSadmin_verbs.dynamic_invoke_verb(admin_client, /datum/admin_verb/play_direct_mob_sound, sound_file, target_mob)
+
+ /// Applies selected client's quirks
+ if ("apply_client_quirks")
+ var/mob/living/carbon/human/specified_humanoid = target_mob
+ if(!istype(specified_humanoid))
+ to_chat(admin_client, "this can only be used on instances of type /mob/living/carbon/human.", confidential = TRUE)
+ return
+ if(!specified_humanoid.client)
+ to_chat(admin_client, "[specified_humanoid] has no client!", confidential = TRUE)
+ return
+ SSquirks.AssignQuirks(specified_humanoid, specified_humanoid.client)
+ log_admin("[key_name(admin_client)] applied client quirks to [key_name(specified_humanoid)].")
+ message_admins(span_adminnotice("[key_name_admin(admin_client)] applied client quirks to [key_name_admin(specified_humanoid)]."))
diff --git a/modular_zubbers/code/modules/admin/transform.dm b/modular_zubbers/code/modules/admin/transform.dm
new file mode 100644
index 00000000000..06a9ba45efa
--- /dev/null
+++ b/modular_zubbers/code/modules/admin/transform.dm
@@ -0,0 +1,127 @@
+// Stuff that helps the TGUI player panel transform section to work
+
+GLOBAL_LIST_INIT(pp_transformables, list(
+ list(
+ name = "Common",
+ color = "",
+ types = list(
+ list(
+ name = "Human",
+ key = /mob/living/carbon/human
+ ),
+ list(
+ name = "Monkey",
+ key = /mob/living/carbon/human/species/monkey
+ ),
+ list(
+ name = "Cyborg",
+ key = /mob/living/silicon/robot
+ )
+ )
+ ),
+ list(
+ name = "Xenomorph",
+ color = "purple",
+ types = list(
+ list(
+ name = "Larva",
+ key = /mob/living/carbon/alien/larva
+ ),
+ list(
+ name = "Drone",
+ key = /mob/living/carbon/alien/adult/drone
+ ),
+ list(
+ name = "Hunter",
+ key = /mob/living/carbon/alien/adult/hunter
+ ),
+ list(
+ name = "Sentinel",
+ key = /mob/living/carbon/alien/adult/sentinel
+ ),
+ list(
+ name = "Praetorian",
+ key = /mob/living/carbon/alien/adult/royal/praetorian
+ ),
+ list(
+ name = "Queen",
+ key = /mob/living/carbon/alien/adult/royal/queen
+ )
+ )
+ ),
+ list(
+ name = "Lavaland",
+ color = "orange",
+ types = list(
+ list(
+ name = "Goliath",
+ key = /mob/living/basic/mining/goliath
+ ),
+ list(
+ name = "Legion",
+ key = /mob/living/simple_animal/hostile/megafauna/legion/small
+ ),
+ list(
+ name = "Blood-Drunk Miner",
+ key = /mob/living/simple_animal/hostile/megafauna/blood_drunk_miner
+ ),
+ list(
+ name = "Gladiator",
+ key = /mob/living/simple_animal/hostile/megafauna/gladiator
+ ),
+ list(
+ name = "Dragon",
+ key = /mob/living/simple_animal/hostile/megafauna/dragon
+ ),
+ list(
+ name = "Legion Hivelord",
+ key = /mob/living/simple_animal/hostile/asteroid/elite/legionnaire
+ )
+ )
+ ),
+ list(
+ name = "Cultist",
+ color = "violet",
+ types = list(
+ list(
+ name = "Shade",
+ key = /mob/living/basic/shade
+ ),
+ list(
+ name = "Artificer",
+ key = /mob/living/basic/construct/artificer
+ ),
+ list(
+ name = "Wraith",
+ key = /mob/living/basic/construct/wraith
+ ),
+ list(
+ name = "Juggernaut",
+ key = /mob/living/basic/construct/juggernaut
+ )
+ )
+ )
+))
+
+// selected_mob: Mob to change
+// new_type: Path of new type e.g: /mob/living/carbon/alien/humanoid/drone
+// new_type_name (optional): Name of the new type (used in logging): e.g: "Drone"
+/datum/admins/proc/transform_mob(mob/selected_mob, mob/admin_mob, new_type, new_type_name)
+ if(!check_rights(R_SPAWN))
+ return
+
+ if(!ismob(selected_mob))
+ to_chat(usr, "This can only be used on instances of type /mob.")
+ return
+
+ if (!new_type_name)
+ new_type_name = new_type
+
+ log_admin("[key_name(usr)] transformed [key_name(selected_mob)] into a [new_type_name].")
+ message_admins(span_adminnotice("[key_name_admin(usr)] transformed [key_name_admin(selected_mob)] into a [new_type_name]."))
+
+ var/mob/new_mob = selected_mob.change_mob_type(new_type, delete_old_mob = TRUE)
+
+ if (selected_mob == admin_mob)
+ admin_mob = new_mob
+ addtimer(CALLBACK(new_mob.mob_panel, PROC_REF(ui_interact), admin_mob), 0.1 SECONDS)
diff --git a/modular_zubbers/code/modules/admin/verbs/debug.dm b/modular_zubbers/code/modules/admin/verbs/debug.dm
index b15b8bd9a00..0119ec857e9 100644
--- a/modular_zubbers/code/modules/admin/verbs/debug.dm
+++ b/modular_zubbers/code/modules/admin/verbs/debug.dm
@@ -97,3 +97,20 @@ ADMIN_VERB(find_nullspaced_objects, R_DEBUG, "Find Nullspaced Objects", "Popup a
var/title = "List of all objects with a loc of null:
"
user << browse(HTML_SKELETON_TITLE(title, "[jointext(strings, "
")]"), "window=maintenace_report")
+
+ADMIN_VERB(toggle_tgui_fallback, R_DEBUG, "Toggle Old TGUI Globally", "ONLY use this in the case of TGUI being broken. This will toggle it for everyone.", ADMIN_CATEGORY_DEBUG)
+ if(isnull(GLOB.tgui_fallback_enabled))
+ GLOB.tgui_fallback_enabled = FALSE
+
+ GLOB.tgui_fallback_enabled = !GLOB.tgui_fallback_enabled
+
+ if(GLOB.tgui_fallback_enabled)
+ log_admin("[key_name(usr)] enabled old UI globally.")
+ message_admins("Old UI has been ENABLED globally.")
+ for(var/client/connected_clients in GLOB.clients)
+ winset(connected_clients, "output_selector.legacy_output_selector", "left=output_legacy")
+ else
+ log_admin("[key_name(usr)] disabled old UI globally.")
+ message_admins("Old UI has been DISABLED globally.")
+ for(var/client/connected_clients in GLOB.clients)
+ winset(connected_clients, "output_selector.output", "left=output")
diff --git a/modular_zubbers/code/modules/client/client_procs.dm b/modular_zubbers/code/modules/client/client_procs.dm
new file mode 100644
index 00000000000..1a5b2a03a64
--- /dev/null
+++ b/modular_zubbers/code/modules/client/client_procs.dm
@@ -0,0 +1,65 @@
+
+//// Toggles Selected quirks on selected mobs
+/client/proc/toggle_quirk(mob/living/carbon/human/selected_mob)
+ if (!istype(selected_mob))
+ to_chat(usr, "This can only be used on /mob/living/carbon/human.")
+ return
+
+ var/list/options = list("Clear"="Clear")
+ for(var/quirk_variable in subtypesof(/datum/quirk))
+ var/datum/quirk/applicable_quirk = quirk_variable
+ var/quirk_name = initial(applicable_quirk.name)
+ options[selected_mob.has_quirk(applicable_quirk) ? "[quirk_name] (Remove)" : "[quirk_name] (Add)"] = applicable_quirk
+
+ var/result = tgui_input_list(usr, "Choose quirk to add/remove", "Mob Quirks", options)
+
+ if(QDELETED(selected_mob))
+ to_chat(usr, "Mob doesn't exist anymore")
+ return
+
+ if(result)
+ if(result == "Clear")
+ for(var/datum/quirk/selected_quirk in selected_mob.quirks)
+ selected_mob.remove_quirk(selected_quirk.type)
+ else
+ var/toggle_quirk = options[result]
+ if(selected_mob.has_quirk(toggle_quirk))
+ selected_mob.remove_quirk(toggle_quirk)
+ else
+ selected_mob.add_quirk(toggle_quirk,TRUE)
+
+//// "Teaches" Martial arts to the selected mob
+/client/proc/teach_martial_art(mob/living/carbon/selected_mob)
+ if (!istype(selected_mob))
+ to_chat(usr, "This can only be used on /mob/living/carbon.")
+ return
+
+ var/list/art_paths = subtypesof(/datum/martial_art)
+ var/list/art_names = list()
+ for(var/martial_art_skill in art_paths)
+ var/datum/martial_art/martial_skill = martial_art_skill
+ art_names[initial(martial_skill.name)] = martial_skill
+ var/result = tgui_input_list(usr, "Choose the martial art to teach", "JUDO CHOP", art_names)
+ if(isnull(result))
+ return
+
+ if(QDELETED(selected_mob))
+ to_chat(usr, "Mob doesn't exist anymore")
+ return
+ if(result)
+ var/chosen_art = art_names[result]
+ var/datum/martial_art/martial_skill = new chosen_art
+ martial_skill.teach(selected_mob)
+ log_admin("[key_name(usr)] has taught [martial_skill] to [key_name(selected_mob)].")
+ message_admins(span_notice("[key_name_admin(usr)] has taught [martial_skill] to [key_name_admin(selected_mob)]."))
+
+//// Sets species of the selected client
+/client/proc/set_species(mob/living/carbon/human/selected_mob)
+ if (istype(selected_mob))
+ var/result = tgui_input_list(usr, "Choose a new species","Species", GLOB.species_list)
+ if(QDELETED(selected_mob))
+ to_chat(usr, "Mob doesn't exist anymore")
+ return
+ if(result)
+ admin_ticket_log("[key_name_admin(usr)] has modified the bodyparts of [selected_mob] to [result]")
+ selected_mob.set_species(GLOB.species_list[result])
diff --git a/modular_zubbers/code/modules/client/preferences/player_panel.dm b/modular_zubbers/code/modules/client/preferences/player_panel.dm
new file mode 100644
index 00000000000..daf63302ba0
--- /dev/null
+++ b/modular_zubbers/code/modules/client/preferences/player_panel.dm
@@ -0,0 +1,11 @@
+/datum/preference/toggle/use_tgui_player_panel
+ category = PREFERENCE_CATEGORY_GAME_PREFERENCES
+ savefile_key = "use_tgui_player_panel"
+ savefile_identifier = PREFERENCE_PLAYER
+ default_value = FALSE
+
+/datum/preference/toggle/use_tgui_player_panel/is_accessible(datum/preferences/preferences)
+ if (!..(preferences))
+ return FALSE
+
+ return is_admin(preferences.parent)
diff --git a/modular_zubbers/code/modules/mob/living/living.dm b/modular_zubbers/code/modules/mob/living/living.dm
index 2939ad3e741..162838cf44d 100644
--- a/modular_zubbers/code/modules/mob/living/living.dm
+++ b/modular_zubbers/code/modules/mob/living/living.dm
@@ -2,3 +2,31 @@
. = ..()
AddComponent(/datum/component/status_indicator)
*/
+
+/// Toggle admin frozen
+/mob/living/proc/toggle_admin_freeze(client/admin)
+ admin_frozen = !admin_frozen
+
+ if(admin_frozen)
+ SetStun(INFINITY, ignore_canstun = TRUE)
+ else
+ SetStun(0, ignore_canstun = TRUE)
+
+ if(client && admin)
+ to_chat(src, span_userdanger("An admin has [!admin_frozen ? "un" : ""]frozen you."))
+ log_admin("[key_name(admin)] toggled admin-freeze on [key_name(src)].")
+ message_admins("[key_name_admin(admin)] toggled admin-freeze on [key_name_admin(src)].")
+
+/// Toggle admin sleeping
+/mob/living/proc/toggle_admin_sleep(client/admin)
+ admin_sleeping = !admin_sleeping
+
+ if(admin_sleeping)
+ SetSleeping(INFINITY)
+ else
+ SetSleeping(0)
+
+ if(client && admin)
+ to_chat(src, span_userdanger("An admin has [!admin_sleeping ? "un": ""]slept you."))
+ log_admin("[key_name(admin)] toggled admin-sleep on [key_name(src)].")
+ message_admins("[key_name_admin(admin)] toggled admin-sleep on [key_name_admin(src)].")
diff --git a/modular_zubbers/code/modules/mob/living/living_defines.dm b/modular_zubbers/code/modules/mob/living/living_defines.dm
new file mode 100644
index 00000000000..96e800f5256
--- /dev/null
+++ b/modular_zubbers/code/modules/mob/living/living_defines.dm
@@ -0,0 +1,4 @@
+/mob/living
+ /// Player Panel Code
+ var/admin_frozen = FALSE
+ var/admin_sleeping = FALSE
diff --git a/modular_zubbers/code/modules/mob/mob.dm b/modular_zubbers/code/modules/mob/mob.dm
new file mode 100644
index 00000000000..09feadfdd0b
--- /dev/null
+++ b/modular_zubbers/code/modules/mob/mob.dm
@@ -0,0 +1,14 @@
+/// Player Panel Proc calling for new player panel creation
+/mob/proc/create_player_panel()
+ if(mob_panel)
+ QDEL_NULL(mob_panel)
+
+ mob_panel = new(src)
+
+/mob/Initialize()
+ . = ..()
+ create_player_panel()
+
+/mob/Destroy()
+ QDEL_NULL(mob_panel)
+ return ..()
diff --git a/modular_zubbers/code/modules/mob/mob_defines.dm b/modular_zubbers/code/modules/mob/mob_defines.dm
new file mode 100644
index 00000000000..86f47c2b7da
--- /dev/null
+++ b/modular_zubbers/code/modules/mob/mob_defines.dm
@@ -0,0 +1,3 @@
+/mob
+ // Admin player panel for this mob
+ var/datum/player_panel/mob_panel
diff --git a/tgstation.dme b/tgstation.dme
index 171b9ef86a9..e60c8a834b8 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -8840,6 +8840,7 @@
#include "modular_zubbers\code\__DEFINES\span.dm"
#include "modular_zubbers\code\__DEFINES\techweb_nodes.dm"
#include "modular_zubbers\code\__DEFINES\traits.dm"
+#include "modular_zubbers\code\_globalvars\tgui_fallback.dm"
#include "modular_zubbers\code\_globalvars\lists\_maintenance_loot.dm"
#include "modular_zubbers\code\_globalvars\lists\maintenance_loot_common.dm"
#include "modular_zubbers\code\_globalvars\lists\maintenance_loot_oddity.dm"
@@ -9106,6 +9107,8 @@
#include "modular_zubbers\code\game\turfs\open\sand.dm"
#include "modular_zubbers\code\jobs\mining\voucher_sets.dm"
#include "modular_zubbers\code\modules\_defines.dm"
+#include "modular_zubbers\code\modules\admin\player_panel.dm"
+#include "modular_zubbers\code\modules\admin\transform.dm"
#include "modular_zubbers\code\modules\admin\smites\pie.dm"
#include "modular_zubbers\code\modules\admin\verbs\admin.dm"
#include "modular_zubbers\code\modules\admin\verbs\debug.dm"
@@ -9233,11 +9236,13 @@
#include "modular_zubbers\code\modules\changeling_zombies\virus.dm"
#include "modular_zubbers\code\modules\character_preview_background\character_preview_background.dm"
#include "modular_zubbers\code\modules\client\client_color.dm"
+#include "modular_zubbers\code\modules\client\client_procs.dm"
#include "modular_zubbers\code\modules\client\preferences.dm"
#include "modular_zubbers\code\modules\client\ssd.dm"
#include "modular_zubbers\code\modules\client\autopunctuation\preferences.dm"
#include "modular_zubbers\code\modules\client\flavor_text\flavor_text.dm"
#include "modular_zubbers\code\modules\client\preferences\permanent_limp.dm"
+#include "modular_zubbers\code\modules\client\preferences\player_panel.dm"
#include "modular_zubbers\code\modules\client\preferences\sounds.dm"
#include "modular_zubbers\code\modules\client\verbs\character_directory.dm"
#include "modular_zubbers\code\modules\clothing\_job.dm"
@@ -9449,6 +9454,8 @@
#include "modular_zubbers\code\modules\mkultra\reaction.dm"
#include "modular_zubbers\code\modules\mkultra\reagent.dm"
#include "modular_zubbers\code\modules\mkultra\vocal_cords.dm"
+#include "modular_zubbers\code\modules\mob\mob.dm"
+#include "modular_zubbers\code\modules\mob\mob_defines.dm"
#include "modular_zubbers\code\modules\mob\dead\new_player\sprite_accessories.dm"
#include "modular_zubbers\code\modules\mob\dead\new_player\body_markings\body_marking_gloss.dm"
#include "modular_zubbers\code\modules\mob\dead\new_player\body_markings\body_markings.dm"
@@ -9467,6 +9474,7 @@
#include "modular_zubbers\code\modules\mob\living\emote.dm"
#include "modular_zubbers\code\modules\mob\living\examine.dm"
#include "modular_zubbers\code\modules\mob\living\living.dm"
+#include "modular_zubbers\code\modules\mob\living\living_defines.dm"
#include "modular_zubbers\code\modules\mob\living\basic\guardian\guardian_types\holoparasite_timestop.dm"
#include "modular_zubbers\code\modules\mob\living\basic\lightgeist\lightgeist.dm"
#include "modular_zubbers\code\modules\mob\living\basic\moonstation\cazador.dm"
diff --git a/tgui/packages/tgui/interfaces/PlayerPanel.tsx b/tgui/packages/tgui/interfaces/PlayerPanel.tsx
new file mode 100644
index 00000000000..b7e99777b90
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PlayerPanel.tsx
@@ -0,0 +1,1144 @@
+import { useState } from 'react';
+import {
+ Box,
+ Button,
+ Collapsible,
+ Dropdown,
+ Flex,
+ Input,
+ LabeledList,
+ NoticeBox,
+ NumberInput,
+ Section,
+ Slider,
+ Tabs,
+ Tooltip,
+} from 'tgui-core/components';
+
+import { useBackend } from '../backend';
+import { Window } from '../layouts';
+
+type Data = {
+ mob_name: string;
+ mob_type: string;
+ admin_mob_type: string;
+ client_ckey: string;
+ client_rank: string;
+ ranks: string;
+ last_ckey: string;
+ playtimes_enabled: boolean;
+ playtime: string;
+ godmode: boolean;
+ is_frozen: boolean;
+ is_slept: boolean;
+ client_muted: number;
+ current_time: string;
+ data_related_ip: string;
+ data_related_cid: string;
+ data_player_join_date: string;
+ data_account_join_date: string;
+ data_byond_version: string;
+ data_old_names: string;
+
+ glob_mute_bits: {
+ name: string;
+ bitflag: number;
+ }[];
+
+ glob_limbs: {
+ [key: string]: string;
+ };
+
+ transformables: {
+ name: string;
+ color: string;
+ types: {
+ name: string;
+ key: string;
+ }[];
+ }[];
+};
+
+const PAGES = [
+ {
+ title: 'General',
+ component: () => GeneralActions,
+ color: 'green',
+ icon: 'tools',
+ },
+ {
+ title: 'Mob',
+ component: () => PhysicalActions,
+ color: 'yellow',
+ icon: 'bolt',
+ canAccess: (data) => {
+ return !!data.mob_type.includes('/mob/living');
+ },
+ },
+ {
+ title: 'Transform',
+ component: () => TransformActions,
+ color: 'orange',
+ icon: 'exchange-alt',
+ },
+ {
+ title: 'Punish',
+ component: () => PunishmentActions,
+ color: 'red',
+ icon: 'gavel',
+ },
+ {
+ title: 'Fun',
+ component: () => FunActions,
+ color: 'blue',
+ icon: 'laugh',
+ },
+ {
+ title: 'Other',
+ component: () => OtherActions,
+ color: 'blue',
+ icon: 'crosshairs',
+ },
+];
+
+export const PlayerPanel = () => {
+ const { act, data } = useBackend();
+ const [pageIndex, setPageIndex] = useState(0);
+ const PageComponent = PAGES[pageIndex].component();
+
+ const {
+ mob_name,
+ mob_type,
+ client_ckey,
+ client_rank,
+ ranks,
+ last_ckey,
+ playtimes_enabled,
+ playtime,
+ } = data;
+
+ return (
+
+
+
+
+
+
+
+ {PAGES.map((page, i) => {
+ if (page.canAccess && !page.canAccess(data)) {
+ return;
+ }
+
+ return (
+ setPageIndex(i)}
+ >
+ {page.title}
+
+ );
+ })}
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const GeneralActions = () => {
+ const { act, data } = useBackend();
+ const { client_ckey, mob_type, admin_mob_type } = data;
+ return (
+
+
+
+
+
+
+
+
+
+
+ act('bring')}
+ >
+ Bring
+
+
+ act('jump_to')}
+ >
+ Jump To
+
+
+
+
+
+
+
+ act('strip')}
+ >
+ Drop All Items
+
+
+
+
+
+ act('cryo')}
+ >
+ Send To Cryo
+
+
+ act('lobby')}
+ >
+ Send To Lobby
+
+
+
+
+
+ act('ghost')}
+ >
+ Eject Ghost
+
+ act('take_control')}
+ >
+ Take Control
+
+ act('offer_control')}
+ >
+ Offer Control
+
+
+
+
+ );
+};
+
+const PhysicalActions = () => {
+ const { act, data } = useBackend();
+ const { glob_limbs, godmode, mob_type } = data;
+ const [mobScale, setMobScale] = useState(1);
+ const limbs = Object.keys(glob_limbs);
+ const limb_flags = limbs.map((_, i) => 1 << i);
+ const [delimbOption, setDelimbOption] = useState(0);
+
+ return (
+
+ act('toggle_godmode')}
+ >
+ God Mode
+
+ }
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+ {limbs.map((val, index) => (
+
+ setDelimbOption(
+ delimbOption & limb_flags[index]
+ ? delimbOption & ~limb_flags[index]
+ : delimbOption | limb_flags[index],
+ )
+ }
+ >
+ {val}
+
+ ))}
+
+ }
+ >
+
+
+ act('limb', {
+ limbs: limb_flags.map(
+ (val, index) =>
+ !!(delimbOption & val) && glob_limbs[limbs[index]],
+ ),
+ delimb_mode: true,
+ })
+ }
+ >
+ Delimb
+
+
+ act('limb', {
+ limbs: limb_flags.map(
+ (val, index) =>
+ !!(delimbOption & val) && glob_limbs[limbs[index]],
+ ),
+ })
+ }
+ >
+ Relimb
+
+
+
+ {
+ setMobScale(1);
+ act('scale', { new_scale: 1 });
+ }}
+ >
+ Reset
+
+ }
+ >
+
+ {
+ setMobScale(value); // Update slider value
+ act('scale', { new_scale: value }); // Update mob's value
+ }}
+ unit="x"
+ />
+
+
+
+
+ );
+};
+
+const TransformActions = () => {
+ const { act, data } = useBackend();
+ const { transformables, mob_type } = data;
+ return (
+
+
+
+ {transformables.map((transformables_category) => {
+ return (
+
+
+ {transformables_category.types.map((transformables_type) => {
+ return (
+
+
+ act('transform', {
+ newType: transformables_type.key,
+ newTypeName: transformables_type.name,
+ })
+ }
+ >
+ {transformables_type.name}
+
+
+ );
+ })}
+
+
+ );
+ })}
+
+ );
+};
+
+const PunishmentActions = () => {
+ const { act, data } = useBackend();
+ const {
+ client_ckey,
+ mob_type,
+ is_frozen,
+ is_slept,
+ glob_mute_bits,
+ client_muted,
+ data_related_cid,
+ data_related_ip,
+ data_byond_version,
+ data_player_join_date,
+ data_account_join_date,
+ data_old_names,
+ current_time,
+ } = data;
+ return (
+
+
+
+
+
+
+
+
+ act('prison')}
+ >
+ Admin Prison
+
+
+
+
+
+
+ act('kick')}
+ >
+ Kick
+
+
+ act('sticky_ban')}
+ >
+ Sticky Ban
+
+
+
+
+
+
+
+ >
+ }
+ >
+
+ {glob_mute_bits.map((bit, i) => {
+ const isMuted = client_muted && client_muted & bit.bitflag;
+ return (
+
+ );
+ })}
+
+
+
+
+ Related accounts by:
+
+
+
+
+ }
+ >
+
+
+
+ {current_time}
+
+
+ {data_account_join_date}
+
+
+ {data_player_join_date}
+
+
+ {data_byond_version}
+
+
+ {data_old_names}
+
+
+
+
+
+ );
+};
+
+const FunActions = () => {
+ const { act } = useBackend();
+
+ const colours = {
+ White: '#a4bad6',
+ Dark: '#42474D',
+ Red: '#c51e1e',
+ 'Red Bright': '#FF0000',
+ Velvet: '#660015',
+ Green: '#059223',
+ Blue: '#6685f5',
+ Purple: '#800080',
+ 'Purple Dark': '#5000A0',
+ Narsie: '#973e3b',
+ Ratvar: '#BE8700',
+ };
+
+ const [lockExplode, setLockExplode] = useState(true);
+ const [empMode, setEmpMode] = useState(false);
+ const [expPower, setExpPower] = useState(8);
+ const [narrateSize, setNarrateSize] = useState(1);
+ const [narrateMessage, setNarrateMessage] = useState('');
+ const [narrateColour, setNarrateColour] = useState(Object.keys(colours)[0]);
+ const [narrateFont, setNarrateFont] = useState('Verdana');
+ const [narrateBold, setNarrateBold] = useState(false);
+ const [narrateItalic, setNarrateItalic] = useState(false);
+ const [narrateGlobal, setNarrateGlobal] = useState(false);
+ const [narrateRange, setNarrateRange] = useState(7);
+
+ const narrateStyles = {
+ color: colours[narrateColour],
+ 'font-size': narrateSize + 'rem',
+ 'font-weight': narrateBold ? 'bold' : '',
+ 'font-family': narrateFont,
+ 'font-style': narrateItalic ? 'italic' : '',
+ };
+
+ return (
+
+
+ These features are centred on YOUR viewport
+
+
+
+ setEmpMode(!empMode)}
+ >
+ EMP Mode
+
+
+ >
+ }
+ >
+
+
+
+
+
+ setExpPower(value)}
+ ranges={{
+ green: [0, 8],
+ orange: [8, 15],
+ red: [15, 30],
+ }}
+ minValue={1}
+ maxValue={30}
+ height="100%"
+ />
+
+
+
+
+
+ );
+};
+
+const OtherActions = () => {
+ const { act, data } = useBackend();
+ const { mob_type, client_ckey } = data;
+
+ return (
+
+
+
+
+
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/admin.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/admin.tsx
index 43e79daa3b6..eb1bf52519c 100644
--- a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/admin.tsx
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/admin.tsx
@@ -6,6 +6,15 @@ import {
} from '../base';
import { FeatureDropdownInput } from '../dropdowns';
+// BUBBER EDIT ADDITION START
+export const use_tgui_player_panel: FeatureToggle = {
+ name: 'Use modern player panel',
+ category: 'ADMIN',
+ description: 'Whether to use the new TGUI player panel or the old HTML one.',
+ component: CheckboxInput,
+};
+// BUBBER EDIT ADDITION END
+
export const asaycolor: Feature = {
name: 'Admin chat color',
category: 'ADMIN',