diff --git a/aurorastation.dme b/aurorastation.dme
index 68e11b19bc1..5feedecc199 100644
--- a/aurorastation.dme
+++ b/aurorastation.dme
@@ -2423,6 +2423,7 @@
#include "code\modules\nano\modules\alarm_monitor.dm"
#include "code\modules\nano\modules\atmos_control.dm"
#include "code\modules\nano\modules\crew_monitor.dm"
+#include "code\modules\nano\modules\ghost_menu.dm"
#include "code\modules\nano\modules\human_appearance.dm"
#include "code\modules\nano\modules\law_manager.dm"
#include "code\modules\nano\modules\lighting_ctrl.dm"
diff --git a/code/game/antagonist/_antagonist_setup.dm b/code/game/antagonist/_antagonist_setup.dm
index c5379c75085..3494b278531 100644
--- a/code/game/antagonist/_antagonist_setup.dm
+++ b/code/game/antagonist/_antagonist_setup.dm
@@ -77,6 +77,12 @@ var/global/list/bantype_to_antag_age = list()
if(player in antag.pending_antagonists)
return antag
+/proc/player_is_obvious_antag(var/datum/mind/player, var/only_offstation_roles = FALSE)
+ var/datum/antagonist/antag = player_is_antag(player, only_offstation_roles)
+ if(antag)
+ return antag.is_obvious_antag(player)
+ return FALSE
+
/**
* This must be called after map loading is done!
*/
diff --git a/code/game/antagonist/alien/borer.dm b/code/game/antagonist/alien/borer.dm
index 6487bdb8f39..965970e6fe3 100644
--- a/code/game/antagonist/alien/borer.dm
+++ b/code/game/antagonist/alien/borer.dm
@@ -65,4 +65,7 @@ var/datum/antagonist/xenos/borer/borers
var/obj/item/organ/external/head = borer.host.get_organ(BP_HEAD)
head.implants += borer
else
- ..()
\ No newline at end of file
+ ..()
+
+/datum/antagonist/xenos/borer/is_obvious_antag(datum/mind/player)
+ return TRUE
\ No newline at end of file
diff --git a/code/game/antagonist/antagonist_helpers.dm b/code/game/antagonist/antagonist_helpers.dm
index 85f1542fed6..f94114cb1ca 100644
--- a/code/game/antagonist/antagonist_helpers.dm
+++ b/code/game/antagonist/antagonist_helpers.dm
@@ -61,6 +61,9 @@
/datum/antagonist/proc/handle_latelogin(var/mob/user)
return
+/datum/antagonist/proc/is_obvious_antag(var/datum/mind/player)
+ return FALSE
+
/proc/all_random_antag_types()
// No caching as the ANTAG_RANDOM_EXCEPTED flag can be added/removed mid-round.
var/list/antag_candidates = all_antag_types.Copy()
diff --git a/code/game/antagonist/outsider/revenant.dm b/code/game/antagonist/outsider/revenant.dm
index fa89ab21e3c..3b9d99144cf 100644
--- a/code/game/antagonist/outsider/revenant.dm
+++ b/code/game/antagonist/outsider/revenant.dm
@@ -27,6 +27,9 @@ var/datum/antagonist/revenant/revenants = null
if(revenants.rifts_left <= 0)
command_announcement.Announce("Aurora, we aren't detecting any more rift energy signatures. Mop up the rest of the invaders. Good work.", "Bluespace Breach Alert")
+/datum/antagonist/revenant/is_obvious_antag(datum/mind/player)
+ return TRUE
+
/proc/message_all_revenants(var/message)
for(var/thing in human_mob_list)
if(isrevenant(thing))
diff --git a/code/game/antagonist/outsider/technomancer.dm b/code/game/antagonist/outsider/technomancer.dm
index 937fbea2e4d..11280b00688 100644
--- a/code/game/antagonist/outsider/technomancer.dm
+++ b/code/game/antagonist/outsider/technomancer.dm
@@ -83,4 +83,7 @@ var/datum/antagonist/technomancer/technomancers
return TRUE
if(raider_techno.is_antagonist(player))
return TRUE
- return FALSE
\ No newline at end of file
+ return FALSE
+
+/datum/antagonist/technomancer/is_obvious_antag(datum/mind/player)
+ return TRUE
diff --git a/code/game/antagonist/station/changeling/changeling.dm b/code/game/antagonist/station/changeling/changeling.dm
index 8d5fb722113..0785a2e97d7 100644
--- a/code/game/antagonist/station/changeling/changeling.dm
+++ b/code/game/antagonist/station/changeling/changeling.dm
@@ -95,4 +95,9 @@
if(.)
player.current.verbs -= /datum/changeling/proc/EvolutionMenu
for(var/datum/power/changeling/P in powerinstances)
- player.current.verbs -= P.verbpath
\ No newline at end of file
+ player.current.verbs -= P.verbpath
+
+/datum/antagonist/changeling/is_obvious_antag(datum/mind/player)
+ if(istype(player.current, /mob/living/simple_animal/hostile/lesser_changeling))
+ return TRUE
+ return FALSE
\ No newline at end of file
diff --git a/code/game/antagonist/station/changeling/morph.dm b/code/game/antagonist/station/changeling/morph.dm
index 6d90842d3f1..fac79d12b5c 100644
--- a/code/game/antagonist/station/changeling/morph.dm
+++ b/code/game/antagonist/station/changeling/morph.dm
@@ -17,4 +17,7 @@ var/datum/antagonist/morph/morphs = null
/datum/antagonist/morph/greet(datum/mind/player)
. = ..()
to_chat(player.current, SPAN_DANGER("As a morph, you can disguise as objects by alt-clicking on them."))
- to_chat(player.current, SPAN_DANGER("You can eat people and items by clicking on them, but only if they're dead."))
\ No newline at end of file
+ to_chat(player.current, SPAN_DANGER("You can eat people and items by clicking on them, but only if they're dead."))
+
+/datum/antagonist/morph/is_obvious_antag(datum/mind/player)
+ return TRUE
\ No newline at end of file
diff --git a/code/game/antagonist/station/cultist.dm b/code/game/antagonist/station/cultist.dm
index 5506431838b..0424ffd5e87 100644
--- a/code/game/antagonist/station/cultist.dm
+++ b/code/game/antagonist/station/cultist.dm
@@ -126,4 +126,11 @@ var/datum/antagonist/cultist/cult
if(!cult.can_become_antag(target.mind) || jobban_isbanned(target, "cultist") || player_is_antag(target.mind))
to_chat(usr, SPAN_CULT("You get the sense that [target] would be an unworthy offering."))
else
- to_chat(usr, SPAN_CULT("You get the sense that your master would be pleased to welcome [target] into the cult."))
\ No newline at end of file
+ to_chat(usr, SPAN_CULT("You get the sense that your master would be pleased to welcome [target] into the cult."))
+
+/datum/antagonist/cultist/is_obvious_antag(datum/mind/player)
+ if(istype(player.current, /mob/living/simple_animal/construct))
+ return TRUE
+ else if(istype(player.current, /mob/living/simple_animal/shade))
+ return TRUE
+ return FALSE
\ No newline at end of file
diff --git a/code/modules/mob/abstract/observer/observer.dm b/code/modules/mob/abstract/observer/observer.dm
index cffddc25fc5..1e1939c6878 100644
--- a/code/modules/mob/abstract/observer/observer.dm
+++ b/code/modules/mob/abstract/observer/observer.dm
@@ -394,14 +394,13 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp
stop_following()
usr.forceMove(pick(L))
-/mob/abstract/observer/verb/follow(input in getmobs())
+/mob/abstract/observer/verb/follow()
set category = "Ghost"
set name = "Follow" // "Haunt"
set desc = "Follow and haunt a mob."
- var/target = getmobs()[input]
- if(!target) return
- ManualFollow(target)
+ var/datum/vueui_module/ghost_menu/GM = new /datum/vueui_module/ghost_menu(usr)
+ GM.ui_interact(usr)
// This is the ghost's follow verb with an argument
/mob/abstract/observer/proc/ManualFollow(var/atom/movable/target)
diff --git a/code/modules/nano/modules/ghost_menu.dm b/code/modules/nano/modules/ghost_menu.dm
new file mode 100644
index 00000000000..b9ec795689a
--- /dev/null
+++ b/code/modules/nano/modules/ghost_menu.dm
@@ -0,0 +1,66 @@
+/datum/vueui_module/ghost_menu/ui_interact(var/mob/user)
+ var/datum/vueui/ui = SSvueui.get_open_ui(user, src)
+ if(!ui)
+ ui = new(user, src, "misc-ghostmenu", 800, 450, "Ghost Menu", state = interactive_state)
+ ui.open()
+
+/datum/vueui_module/ghost_menu/Topic(ref, href_list)
+ if(..())
+ return TRUE
+
+ var/mob/abstract/observer/ghost = usr
+ if(!istype(ghost))
+ return TRUE
+
+ if(href_list["follow_target"])
+ ghost.ManualFollow(locate(href_list["follow_target"]) in mob_list)
+
+ return FALSE
+
+/datum/vueui_module/ghost_menu/vueui_data_change(var/list/data, var/mob/user, var/datum/vueui/ui)
+ if(!data)
+ data = list()
+
+ var/list/menu_info = list()
+
+ var/list/names = list()
+ var/list/namecounts = list()
+ for(var/mob/M in mob_list)
+ var/category
+ var/name = M.name
+ if(name in names)
+ namecounts[name]++
+ name = "[name] ([namecounts[name]])"
+ else
+ names.Add(name)
+ namecounts[name] = 1
+ if(M.real_name && M.real_name != M.name)
+ name += " \[[M.real_name]\]"
+ if(!M.mind)
+ if(M.stat == DEAD)
+ category = "Dead NPCs"
+ else
+ category = "NPCs"
+ else if(M.stat == DEAD)
+ if(isobserver(M))
+ name += " \[Ghost\]"
+ category = "Observer"
+ else
+ name += " \[Dead\]"
+ category = "Dead"
+ else
+ if(player_is_obvious_antag(M.mind))
+ category = M.mind.special_role
+ else if(isobserver(user))
+ var/mob/abstract/observer/O = user
+ if(O.antagHUD && player_is_antag(M.mind))
+ category = M.mind.special_role
+ if(!category)
+ category = "Alive"
+ if(menu_info[category])
+ menu_info[category] += list(list("name" = name, "ref" = ref(M))) // this is how to add a list to a list without them merging
+ else
+ menu_info[category] = list(list("name" = name, "ref" = ref(M)))
+
+ data["menuinfo"] = menu_info
+ return data
\ No newline at end of file
diff --git a/html/changelogs/geeves-orbit_menu.yml b/html/changelogs/geeves-orbit_menu.yml
new file mode 100644
index 00000000000..79b08a9a800
--- /dev/null
+++ b/html/changelogs/geeves-orbit_menu.yml
@@ -0,0 +1,6 @@
+author: Geeves
+
+delete-after: True
+
+changes:
+ - rscadd: "The standard ghost follow menu has been replaced with an upgraded VueUI version."
\ No newline at end of file
diff --git a/vueui/src/components/view/misc/ghostmenu.vue b/vueui/src/components/view/misc/ghostmenu.vue
new file mode 100644
index 00000000000..d4d353bd38e
--- /dev/null
+++ b/vueui/src/components/view/misc/ghostmenu.vue
@@ -0,0 +1,16 @@
+
+