From 47c7add3243a45210c5a3886d27daeab72f69a67 Mon Sep 17 00:00:00 2001
From: CHOMPStation2StaffMirrorBot
<94713762+CHOMPStation2StaffMirrorBot@users.noreply.github.com>
Date: Sun, 20 Jul 2025 07:29:37 -0700
Subject: [PATCH] [MIRROR] simple ghost pod find (#11205)
Co-authored-by: Kashargul <144968721+Kashargul@users.noreply.github.com>
---
code/__defines/observer.dm | 2 +
code/_global_vars/__unsorted.dm | 1 +
code/_onclick/hud/ghost.dm | 25 +-
code/datums/ghost_spawn.dm | 224 ++++++++++++
code/datums/ghost_spawn_options.dm | 329 ++++++++++++++++++
code/game/machinery/camera/tracking.dm | 4 +-
.../machinery/virtual_reality/vr_procs.dm | 39 +--
.../objects/effects/job_start_landmarks.dm | 3 +
code/modules/admin/admin_tools.dm | 4 +-
.../modules/admin/verbs/tgui_verbs.dm | 8 +-
code/modules/mob/dead/observer/observer.dm | 58 +--
code/modules/mob/dead/observer/observer_vr.dm | 82 +----
.../silicon/robot/drone/drone_manufacturer.dm | 76 +---
code/modules/vore/eating/inbelly_spawn.dm | 66 ----
.../vore/eating/soulcatcher_observer.dm | 52 ---
.../maps/southern_cross/southern_cross-1.dmm | 35 +-
.../maps/southern_cross/southern_cross-10.dmm | 14 +-
.../maps/southern_cross/southern_cross-5.dmm | 20 +-
.../GhostSpawn/GhostJoin/GhostJoin.tsx | 189 ++++++++++
.../GhostSpawn/GhostJoin/SpawnElement.tsx | 31 ++
.../GhostSpawn/GhostPod/GhostPod.tsx | 50 +++
.../GhostSpawn/GhostPod/PodSelectionList.tsx | 53 +++
.../GhostSpawn/GhostPod/SelectionList.tsx | 105 ++++++
.../GhostSpawn/Vorespawn/Vorespawn.tsx | 139 ++++++++
.../tgui/interfaces/GhostSpawn/functions.ts | 115 ++++++
.../tgui/interfaces/GhostSpawn/index.tsx | 61 ++++
.../tgui/interfaces/GhostSpawn/types.ts | 67 ++++
.../WikiCommon/WikiSearchList.tsx | 4 +-
.../interfaces/RobotChoose/IconSection.tsx | 2 -
.../interfaces/RobotChoose/ModuleSection.tsx | 49 +--
.../interfaces/RobotChoose/SpriteSection.tsx | 51 +--
.../tgui/interfaces/RobotChoose/index.tsx | 40 ++-
vorestation.dme | 5 +-
33 files changed, 1561 insertions(+), 442 deletions(-)
create mode 100644 code/datums/ghost_spawn.dm
create mode 100644 code/datums/ghost_spawn_options.dm
rename {modular_chomp/code => code}/modules/admin/verbs/tgui_verbs.dm (80%)
delete mode 100644 code/modules/vore/eating/soulcatcher_observer.dm
create mode 100644 tgui/packages/tgui/interfaces/GhostSpawn/GhostJoin/GhostJoin.tsx
create mode 100644 tgui/packages/tgui/interfaces/GhostSpawn/GhostJoin/SpawnElement.tsx
create mode 100644 tgui/packages/tgui/interfaces/GhostSpawn/GhostPod/GhostPod.tsx
create mode 100644 tgui/packages/tgui/interfaces/GhostSpawn/GhostPod/PodSelectionList.tsx
create mode 100644 tgui/packages/tgui/interfaces/GhostSpawn/GhostPod/SelectionList.tsx
create mode 100644 tgui/packages/tgui/interfaces/GhostSpawn/Vorespawn/Vorespawn.tsx
create mode 100644 tgui/packages/tgui/interfaces/GhostSpawn/functions.ts
create mode 100644 tgui/packages/tgui/interfaces/GhostSpawn/index.tsx
create mode 100644 tgui/packages/tgui/interfaces/GhostSpawn/types.ts
diff --git a/code/__defines/observer.dm b/code/__defines/observer.dm
index 4a21eb68bb..66a6824b99 100644
--- a/code/__defines/observer.dm
+++ b/code/__defines/observer.dm
@@ -1 +1,3 @@
#define OBSERVER_EVENT_DESTROY "OnDestroy"
+
+#define MOUSE_VENT_NETWORK_LENGTH 20
diff --git a/code/_global_vars/__unsorted.dm b/code/_global_vars/__unsorted.dm
index 4c6cd331e2..6e1576f4c2 100644
--- a/code/_global_vars/__unsorted.dm
+++ b/code/_global_vars/__unsorted.dm
@@ -3,6 +3,7 @@ GLOBAL_DATUM(data_core, /datum/datacore)
//I would upgrade all instances of global.machines to SSmachines.all_machines but it's used in so many places and a search returns so many matches for 'machines' that isn't a use of the global...
GLOBAL_LIST_INIT(machines, SSmachines.all_machines)
+GLOBAL_LIST_EMPTY(all_drone_fabricators)
GLOBAL_LIST_EMPTY(active_diseases)
GLOBAL_LIST_EMPTY(hud_icon_reference)
diff --git a/code/_onclick/hud/ghost.dm b/code/_onclick/hud/ghost.dm
index afd1429178..dd97dfdf21 100644
--- a/code/_onclick/hud/ghost.dm
+++ b/code/_onclick/hud/ghost.dm
@@ -100,7 +100,30 @@
/obj/screen/ghost/vr/Click()
..()
var/mob/observer/dead/G = usr
- G.fake_enter_vr()
+ var/datum/data/record/record_found
+ record_found = find_general_record("name", G.client.prefs.real_name)
+ // Found their record, they were spawned previously. Remind them corpses cannot play games.
+ if(record_found)
+ var/answer = tgui_alert(G, "You seem to have previously joined this round. If you are currently dead, you should not enter VR as this character. Would you still like to proceed?", "Previously spawned",list("Yes", "No"))
+ if(answer != "Yes")
+ return
+
+ var/S = null
+ var/list/vr_landmarks = list()
+ for(var/obj/effect/landmark/virtual_reality/sloc in GLOB.landmarks_list)
+ vr_landmarks += sloc.name
+ if(!LAZYLEN(vr_landmarks))
+ to_chat(G, "There are no available spawn locations in virtual reality.")
+ return
+ S = tgui_input_list(G, "Please select a location to spawn your avatar at:", "Spawn location", vr_landmarks)
+ if(!S)
+ return 0
+ for(var/obj/effect/landmark/virtual_reality/i in GLOB.landmarks_list)
+ if(i.name == S)
+ S = i
+ break
+
+ G.fake_enter_vr(S)
/mob/observer/dead/create_mob_hud(datum/hud/HUD, apply_to_client = TRUE)
..()
diff --git a/code/datums/ghost_spawn.dm b/code/datums/ghost_spawn.dm
new file mode 100644
index 0000000000..f56146e127
--- /dev/null
+++ b/code/datums/ghost_spawn.dm
@@ -0,0 +1,224 @@
+#define GHOST_POD_TAB 0
+#define GHOST_JOIN 1
+#define GHOST_VORE_SPAWN 2
+
+GLOBAL_VAR_INIT(allowed_ghost_spawns, 2)
+
+// Ghost spawn menu
+/datum/tgui_module/ghost_spawn_menu
+ name = "Ghost Spawn Menu"
+ tgui_id = "GhostSpawn"
+ var/active_tab = GHOST_POD_TAB
+
+/datum/tgui_module/ghost_spawn_menu/tgui_state(mob/user)
+ return GLOB.tgui_self_state
+
+/datum/tgui_module/ghost_spawn_menu/tgui_close(mob/user)
+ . = ..()
+ if(isobserver(user))
+ var/mob/observer/dead/observer = user
+ observer.selecting_ghostrole = FALSE
+
+/datum/tgui_module/ghost_spawn_menu/tgui_interact(mob/user, datum/tgui/ui, datum/tgui/parent_ui)
+ . = ..()
+ if(isobserver(user) && ui)
+ var/mob/observer/dead/observer = user
+ observer.selecting_ghostrole = TRUE
+
+/datum/tgui_module/ghost_spawn_menu/tgui_data(mob/user)
+ var/list/data = ..()
+
+ if(active_tab == GHOST_POD_TAB)
+ data["all_ghost_pods"] = compile_pod_data()
+
+ if(active_tab == GHOST_JOIN)
+ data["all_ghost_join_options"] = compile_ghost_join_data(user)
+
+ if(active_tab == GHOST_VORE_SPAWN)
+ data["all_vore_spawns"] = compile_vorespawn_data()
+
+ data["active_tab"] = active_tab
+ data["user_z"] = user.z
+ return data
+
+/datum/tgui_module/ghost_spawn_menu/tgui_act(action, params, datum/tgui/ui)
+ . = ..()
+ if(.)
+ return
+
+ var/mob/observer/dead/observer = ui.user
+
+ switch(action)
+ if("select_pod")
+ if(istype(observer))
+ jump_to_pod(ui.user, params["selected_pod"])
+ . = TRUE
+ if("set_tab")
+ var/new_tab = text2num(params["val"])
+ if(isnum(new_tab))
+ active_tab = new_tab
+ . = TRUE
+ if("soulcatcher_spawn")
+ soulcatcher_spawn(ui.user, params["selected_player"])
+ close_ui()
+ . = TRUE
+ if("soulcatcher_vore_spawn")
+ soulcatcher_vore_spawn(ui.user, params["selected_player"])
+ close_ui()
+ . = TRUE
+ if("bellyspawn")
+ vore_belly_spawn(ui.user, params["selected_player"])
+ close_ui()
+ . = TRUE
+ if("mouse_spawn")
+ become_mouse(ui.user)
+ . = TRUE
+ if("drone_spawn")
+ become_drone(ui.user, params["fabricator"])
+ . = TRUE
+ if("vr_spawn")
+ join_vr(ui.user, params["landmark"])
+ . = TRUE
+ if("corgi_spawn")
+ join_corgi(ui.user)
+ . = TRUE
+ if("lost_drone_spawn")
+ join_lost(ui.user)
+ . = TRUE
+ if("maintpred_spawn")
+ join_maintpred(ui.user)
+ . = TRUE
+ if("gravekeeper_spawn")
+ join_grave(ui.user)
+ . = TRUE
+ if("morph_spawn")
+ join_morpth(ui.user)
+ . = TRUE
+
+/datum/tgui_module/ghost_spawn_menu/proc/compile_pod_data()
+ var/list/compiled_pods = list()
+ for(var/atom/movable/spawn_object in GLOB.active_ghost_pods)
+ var/enabled = TRUE
+ if(istype(spawn_object, /obj/structure/ghost_pod/manual))
+ var/obj/structure/ghost_pod/manual/man = spawn_object
+ if(!man.remains_active)
+ enabled = FALSE
+ var/type = "Other"
+ if(ismouse(spawn_object))
+ type = "Mouse"
+ else if(ismob(spawn_object))
+ type = "Mob"
+ else if(isstructure(spawn_object))
+ type = "Structure"
+ UNTYPED_LIST_ADD(compiled_pods, list("pod_type" = type, "pod_name" = spawn_object.name, "z_level" = spawn_object.z, "ref" = REF(spawn_object), "remains_active" = enabled))
+ return compiled_pods
+
+/datum/tgui_module/ghost_spawn_menu/proc/compile_ghost_join_data(mob/user)
+ var/ghost_spawn_exists = FALSE
+ for(var/obj/effect/landmark/L in GLOB.landmarks_list)
+ if(L.name == JOB_GHOSTROLES)
+ ghost_spawn_exists = TRUE
+ break
+ var/deathtime = world.time - user.timeofdeath
+ var/time_diff = 15 MINUTES - deathtime
+ var/timedifference_text = time_diff > 0 ? time2text(time_diff, "mm:ss") : ""
+ var/list/ghost_join_data = list(
+ "mouse_data" = get_mouse_data(user),
+ "drone_data" = get_drone_data(user),
+ "vr_data" = get_vr_data(user),
+ "ghost_banned" = jobban_isbanned(user, JOB_CYBORG),
+ "cyborg_banned" = jobban_isbanned(user, JOB_GHOSTROLES),
+ "may_respawn" = user.MayRespawn(),
+ "special_role_respawn" = timedifference_text,
+ "existing_ghost_spawnpoints" = ghost_spawn_exists,
+ "remaining_ghost_roles" = GLOB.allowed_ghost_spawns
+ )
+ return ghost_join_data
+
+/datum/tgui_module/ghost_spawn_menu/proc/get_mouse_data(mob/user)
+ var/turf/T = get_turf(user)
+ var/timedifference_mouse = world.time - user.client.time_died_as_mouse
+ var/timedifference_mouse_text = ""
+ if(user.client.time_died_as_mouse && timedifference_mouse <= CONFIG_GET(number/mouse_respawn_time) MINUTES)
+ timedifference_mouse_text = time2text(CONFIG_GET(number/mouse_respawn_time) MINUTES - timedifference_mouse,"mm:ss")
+ var/found_vents = FALSE
+ for(var/obj/machinery/atmospherics/unary/vent_pump/v in GLOB.machines)
+ if(!v.welded && v.z == T.z && v.network && v.network.normal_members.len > MOUSE_VENT_NETWORK_LENGTH)
+ found_vents = TRUE
+ break
+
+ return list(
+ "disabled" = CONFIG_GET(flag/disable_player_mice),
+ "bad_turf" = (!T || (T.z in using_map.admin_levels)),
+ "respawn_time" = timedifference_mouse_text,
+ "found_vents" = found_vents
+ )
+
+/datum/tgui_module/ghost_spawn_menu/proc/get_drone_data(mob/user)
+ var/time_till_play
+ if(CONFIG_GET(flag/use_age_restriction_for_jobs) && isnum(user.client.player_age))
+ time_till_play = max(0, 3 - user.client.player_age)
+ var/deathtime = world.time - user.timeofdeath
+ var/time_diff = 5 MINUTES - deathtime
+ var/timedifference_text = time_diff > 0 ? time2text(time_diff, "mm:ss") : ""
+ var/list/all_fabricators = list()
+ for(var/obj/machinery/drone_fabricator/DF in GLOB.all_drone_fabricators)
+ if(DF.stat & NOPOWER || !DF.produce_drones)
+ continue
+ if(DF.drone_progress >= 100)
+ all_fabricators += list(REF(DF) = DF.fabricator_tag)
+
+ return list(
+ "disabled" = !CONFIG_GET(flag/allow_drone_spawn),
+ "days_to_play" = time_till_play,
+ "respawn_time" = timedifference_text,
+ "fabricators" = all_fabricators
+ )
+
+/datum/tgui_module/ghost_spawn_menu/proc/get_vr_data(mob/user)
+ var/datum/data/record/record_found = find_general_record("name", user.client.prefs.real_name)
+ var/list/vr_landmarks = list()
+ for(var/obj/effect/landmark/virtual_reality/sloc in GLOB.landmarks_list)
+ vr_landmarks += list(REF(sloc) = sloc.name)
+
+ return list(
+ "record_found" = !!record_found,
+ "vr_landmarks" = vr_landmarks
+ )
+
+/datum/tgui_module/ghost_spawn_menu/proc/compile_vorespawn_data()
+ var/list/compiled_spawn_data = list()
+ for(var/mob/living/player in GLOB.player_list)
+ if(!player.client || player.stat)
+ continue
+ var/soulcatcher_active = FALSE
+ var/soulcatcher_vore_active = FALSE
+ var/vorespawn_active = FALSE
+ if(ishuman(player))
+ var/mob/living/carbon/human/H = player
+ if(H.nif)
+ var/datum/nifsoft/soulcatcher/SC = H.nif.imp_check(NIF_SOULCATCHER)
+ if(SC)
+ soulcatcher_active = TRUE
+ if(player.soulgem?.flag_check(SOULGEM_ACTIVE | SOULGEM_CATCHING_GHOSTS, TRUE))
+ soulcatcher_vore_active = TRUE
+
+ if(!player.no_vore && (get_z(player) in using_map.station_levels))
+ if(ishuman(player))
+ var/mob/living/carbon/human/H = player
+ if(H.latejoin_vore)
+ vorespawn_active = TRUE
+ else if(issilicon(player))
+ var/mob/living/silicon/S = player
+ if(!isAI(S) && S.latejoin_vore)
+ vorespawn_active = TRUE
+ if(isanimal(player))
+ var/mob/living/simple_mob/SM = player
+ if(SM.vore_active && SM.latejoin_vore)
+ vorespawn_active = TRUE
+ compiled_spawn_data += list(REF(player) = list("player" = player.name, "soulcatcher" = soulcatcher_active, "soulcatcher_vore" = soulcatcher_vore_active, "vorespawn" = vorespawn_active))
+ return compiled_spawn_data
+
+#undef GHOST_POD_TAB
+#undef GHOST_JOIN
+#undef GHOST_VORE_SPAWN
diff --git a/code/datums/ghost_spawn_options.dm b/code/datums/ghost_spawn_options.dm
new file mode 100644
index 0000000000..a219504484
--- /dev/null
+++ b/code/datums/ghost_spawn_options.dm
@@ -0,0 +1,329 @@
+/datum/tgui_module/ghost_spawn_menu/proc/jump_to_pod(mob/observer/dead/user, selected_pod)
+ var/atom/movable/target = locate(selected_pod) in GLOB.active_ghost_pods
+ if(!target)
+ to_chat(user, span_warning("Invalid ghost pod selected!"))
+ return
+
+ var/turf/T = get_turf(target) //Turf of the destination mob
+
+ if(T && isturf(T)) //Make sure the turf exists, then move the source to that destination.
+ user.stop_following()
+ user.forceMove(T)
+ else
+ to_chat(user, span_filter_notice("This ghost pod is not located in the game world."))
+
+/datum/tgui_module/ghost_spawn_menu/proc/become_mouse(mob/observer/dead/user)
+ if(CONFIG_GET(flag/disable_player_mice))
+ to_chat(user, span_warning("Spawning as a mouse is currently disabled."))
+ return
+
+ if(jobban_isbanned(user, JOB_GHOSTROLES))
+ to_chat(user, span_warning("You cannot become a mouse because you are banned from playing ghost roles."))
+ return
+
+ if(!user.MayRespawn(1))
+ return
+
+ var/turf/T = get_turf(user)
+ if(!T || (T.z in using_map.admin_levels))
+ to_chat(user, span_warning("You may not spawn as a mouse on this Z-level."))
+ return
+
+ var/timedifference = world.time - user.client.time_died_as_mouse
+ if(user.client.time_died_as_mouse && timedifference <= CONFIG_GET(number/mouse_respawn_time) MINUTES)
+ var/timedifference_text = time2text(CONFIG_GET(number/mouse_respawn_time) MINUTES - timedifference,"mm:ss")
+ to_chat(user, span_warning("You may only spawn again as a mouse more than [CONFIG_GET(number/mouse_respawn_time)] minutes after your death. You have [timedifference_text] left."))
+ return
+
+ //find a viable mouse candidate
+ var/mob/living/simple_mob/animal/passive/mouse/host
+ var/obj/machinery/atmospherics/unary/vent_pump/vent_found
+ var/list/found_vents = list()
+ for(var/obj/machinery/atmospherics/unary/vent_pump/v in GLOB.machines)
+ if(!v.welded && v.z == T.z && v.network && v.network.normal_members.len > MOUSE_VENT_NETWORK_LENGTH)
+ found_vents.Add(v)
+ if(found_vents.len)
+ vent_found = pick(found_vents)
+ host = new /mob/living/simple_mob/animal/passive/mouse(vent_found)
+ else
+ to_chat(user, span_warning("Unable to find any unwelded vents to spawn mice at."))
+
+ if(host)
+ if(CONFIG_GET(flag/uneducated_mice))
+ host.universal_understand = 0
+ announce_ghost_joinleave(user, 0, "They are now a mouse.")
+ host.ckey = user.ckey
+ host.add_ventcrawl(vent_found)
+ to_chat(host, span_info("You are now a mouse. Try to avoid interaction with players, and do not give hints away that you are more than a simple rodent."))
+
+/datum/tgui_module/ghost_spawn_menu/proc/become_drone(mob/observer/dead/user, fabricator)
+ if(ticker.current_state < GAME_STATE_PLAYING)
+ to_chat(user, span_danger("The game hasn't started yet!"))
+ return
+
+ if(!CONFIG_GET(flag/allow_drone_spawn))
+ to_chat(user, span_danger("That verb is not currently permitted."))
+ return
+
+ if(jobban_isbanned(user, JOB_CYBORG))
+ to_chat(user, span_danger("You are banned from playing synthetics and cannot spawn as a drone."))
+ return
+
+ if(CONFIG_GET(flag/use_age_restriction_for_jobs) && isnum(user.client.player_age))
+ var/time_till_play = max(0, 3 - user.client.player_age)
+ if(time_till_play)
+ to_chat(user, span_danger("You have not been playing on the server long enough to join as drone."))
+ return
+
+ if(!user.MayRespawn(1))
+ return
+
+ var/deathtime = world.time - user.timeofdeath
+ var/deathtimeminutes = round(deathtime / (1 MINUTE))
+ var/pluralcheck = "minute"
+ if(deathtimeminutes == 0)
+ pluralcheck = ""
+ else if(deathtimeminutes == 1)
+ pluralcheck = " [deathtimeminutes] minute and"
+ else if(deathtimeminutes > 1)
+ pluralcheck = " [deathtimeminutes] minutes and"
+ var/deathtimeseconds = round((deathtime - deathtimeminutes * 1 MINUTE) / 10,1)
+
+ if (deathtime < 5 MINUTES)
+ to_chat(user, "You have been dead for[pluralcheck] [deathtimeseconds] seconds.")
+ to_chat(user, "You must wait 5 minutes to respawn as a drone!")
+ return
+
+ var/obj/machinery/drone_fabricator/chosen_fabricator = locate(fabricator) in GLOB.all_drone_fabricators
+
+ if(!chosen_fabricator)
+ return
+ if(chosen_fabricator.stat & NOPOWER || !chosen_fabricator.produce_drones)
+ return
+ if(!chosen_fabricator.drone_progress >= 100)
+ return
+
+ chosen_fabricator.create_drone(user.client)
+
+/datum/tgui_module/ghost_spawn_menu/proc/join_vr(mob/observer/dead/user, landmark)
+ var/S = locate(landmark) in GLOB.landmarks_list
+
+ user.fake_enter_vr(S)
+
+/datum/tgui_module/ghost_spawn_menu/proc/soulcatcher_spawn(mob/observer/dead/user, selected_player)
+ var/mob/living/target = locate(selected_player) in GLOB.player_list
+ //Didn't pick anyone or picked a null
+ if(!target)
+ to_chat(user, span_warning("Invalid player selected!"))
+ return
+
+ //Good choice testing and some instance-grabbing
+ if(!ishuman(target))
+ to_chat(user, span_warning("[target] isn't in a humanoid mob at the moment."))
+ return
+
+ var/mob/living/carbon/human/H = target
+
+ if(H.stat || !H.client)
+ to_chat(user, span_warning("[H] isn't awake/alive at the moment."))
+ return
+
+ if(!H.nif)
+ to_chat(user, span_warning("[H] doesn't have a NIF installed."))
+ return
+
+ var/datum/nifsoft/soulcatcher/SC = H.nif.imp_check(NIF_SOULCATCHER)
+ if(!SC)
+ to_chat(user, span_warning("[H] doesn't have the Soulcatcher NIFSoft installed, or their NIF is unpowered."))
+ return
+
+ //Fine fine, we can ask.
+ var/obj/item/nif/nif = H.nif
+ to_chat(user, span_notice("Request sent to [H]."))
+
+ var/req_time = world.time
+ nif.notify("Transient mindstate detected, analyzing...")
+ addtimer(CALLBACK(src, PROC_REF(finish_soulcatcher_spawn), user, H, SC, req_time), 1.5 SECONDS, TIMER_DELETE_ME)
+
+/datum/tgui_module/ghost_spawn_menu/proc/finish_soulcatcher_spawn(mob/observer/dead/user, mob/living/carbon/human/H, datum/nifsoft/soulcatcher/SC, req_time)
+ var/response = tgui_alert(H,"[user] ([user.key]) wants to join into your Soulcatcher.","Soulcatcher Request", list("Deny", "Allow"), timeout = 1 MINUTE)
+
+ if(!response || response == "Deny")
+ to_chat(user, span_warning("[H] denied your request."))
+ return
+
+ if((world.time - req_time) > 1 MINUTE)
+ to_chat(H, span_warning("The request had already expired. (1 minute waiting max)"))
+ return
+
+ //Final check since we waited for input a couple times.
+ if(H && user && user.key && !H.stat && H.nif && SC)
+ if(!user.mind) //No mind yet, aka haven't played in this round.
+ user.mind = new(user.key)
+
+ user.mind.name = name
+ user.mind.current = user
+ user.mind.active = TRUE
+
+ SC.catch_mob(user) //This will result in us being deleted so...
+
+/datum/tgui_module/ghost_spawn_menu/proc/soulcatcher_vore_spawn(mob/observer/dead/user, selected_player)
+ var/mob/living/target = locate(selected_player) in GLOB.player_list
+ if(!target)
+ to_chat(user, span_warning("Invalid player selected!"))
+ return
+
+ if(!ismob(target))
+ to_chat(user, span_warning("Target is no mob."))
+ return
+
+ var/mob/M = target
+
+ var/obj/soulgem/gem = M.soulgem
+
+ if(!gem?.flag_check(SOULGEM_ACTIVE))
+ to_chat(user, span_warning("[M] has no enabled Soulcatcher."))
+ return
+
+ var/req_time = world.time
+ gem.notify_holder("Transient mindstate detected, analyzing...")
+ addtimer(CALLBACK(src, PROC_REF(finish_soulcatcher_vore_spawn), user, M, gem, req_time), 1.5 SECONDS, TIMER_DELETE_ME)
+
+/datum/tgui_module/ghost_spawn_menu/proc/finish_soulcatcher_vore_spawn(mob/observer/dead/user, mob/M, obj/soulgem/gem, req_time)
+ if(tgui_alert(M, "[user.name] wants to join into your Soulcatcher.","Soulcatcher Request",list("Deny", "Allow"), timeout=1 MINUTES) != "Allow")
+ to_chat(user, span_warning("[M] has denied your request."))
+ return
+
+ if((world.time - req_time) > 1 MINUTES)
+ to_chat(M, span_warning("The request had already expired. (1 minute waiting max)"))
+ return
+
+ //Final check since we waited for input a couple times.
+ if(M && user && user.key && !M.stat && gem?.flag_check(SOULGEM_ACTIVE | SOULGEM_CATCHING_GHOSTS, TRUE))
+ if(!user.mind) //No mind yet, aka haven't played in this round.
+ user.mind = new(user.key)
+
+ user.mind.name = name
+ user.mind.current = user
+ user.mind.active = TRUE
+
+ gem.catch_mob(user) //This will result in us being deleted so...
+
+
+/datum/tgui_module/ghost_spawn_menu/proc/vore_belly_spawn(mob/observer/dead/user, selected_player)
+ var/mob/living/target = locate(selected_player) in GLOB.player_list
+
+ if(!target)
+ to_chat(user, span_warning("Invalid player selected!"))
+ return
+
+ to_chat(user, span_notice("Inbelly spawn request sent to predator."))
+ to_chat(target, span_notice("Incoming belly spawn request."))
+ addtimer(CALLBACK(src, PROC_REF(finish_vore_belly_spawn), user, target), 1.5 SECONDS, TIMER_DELETE_ME)
+
+/datum/tgui_module/ghost_spawn_menu/proc/finish_vore_belly_spawn(mob/observer/dead/user, mob/living/L)
+ L.inbelly_spawn_prompt(user.client) // Hand reins over to them
+
+/datum/tgui_module/ghost_spawn_menu/proc/join_corgi(mob/observer/dead/user)
+ if(jobban_isbanned(user, JOB_GHOSTROLES))
+ to_chat(user, span_danger("You are banned from playing ghost roles and cannot spawn as a corgi."))
+ return
+
+ if(GLOB.allowed_ghost_spawns <= 0)
+ to_chat(user, span_warning("There're no free ghost join slots."))
+ return
+
+ var/obj/effect/landmark/spawnspot = get_ghost_role_spawn()
+ if(!spawnspot)
+ to_chat(user, span_warning("No spawnpoint available."))
+ return
+
+ GLOB.allowed_ghost_spawns--
+ announce_ghost_joinleave(user, 0, "They are now a corgi.")
+ var/obj/structure/ghost_pod/manual/corgi/corg = new(get_turf(spawnspot))
+ corg.create_occupant(user)
+
+/datum/tgui_module/ghost_spawn_menu/proc/join_lost(mob/observer/dead/user)
+ if(jobban_isbanned(user, JOB_CYBORG))
+ to_chat(user, span_danger("You are banned from playing synthetics and cannot spawn as a drone."))
+ return
+
+ if(GLOB.allowed_ghost_spawns <= 0)
+ to_chat(user, span_warning("There're no free ghost join slots."))
+ return
+
+ var/obj/effect/landmark/spawnspot = get_ghost_role_spawn()
+ if(!spawnspot)
+ to_chat(user, span_warning("No spawnpoint available."))
+ return
+
+ GLOB.allowed_ghost_spawns--
+ announce_ghost_joinleave(user, 0, "They are now a lost drone.")
+ var/obj/structure/ghost_pod/manual/lost_drone/dogborg/lost = new(get_turf(spawnspot))
+ lost.create_occupant(user)
+
+/datum/tgui_module/ghost_spawn_menu/proc/join_maintpred(mob/observer/dead/user)
+ if(jobban_isbanned(user, JOB_GHOSTROLES))
+ to_chat(user, span_danger("You are banned from playing ghost roles and cannot spawn as a maint pred."))
+ return
+
+ if(GLOB.allowed_ghost_spawns <= 0)
+ to_chat(user, span_warning("There're no free ghost join slots."))
+ return
+
+ var/obj/effect/landmark/spawnspot = get_ghost_role_spawn()
+ if(!spawnspot)
+ to_chat(user, span_warning("No spawnpoint available."))
+ return
+
+ GLOB.allowed_ghost_spawns--
+ announce_ghost_joinleave(user, 0, "They are now a maint pred.")
+ var/obj/structure/ghost_pod/ghost_activated/maintpred/no_announce/mpred = new(get_turf(spawnspot))
+ mpred.create_occupant(user)
+
+/datum/tgui_module/ghost_spawn_menu/proc/join_grave(mob/observer/dead/user)
+ if(jobban_isbanned(user, JOB_CYBORG))
+ to_chat(user, span_danger("You are banned from playing synthetics and cannot spawn as a gravekeeper."))
+ return
+
+ if(GLOB.allowed_ghost_spawns <= 0)
+ to_chat(user, span_warning("There're no free ghost join slots."))
+ return
+
+ var/obj/effect/landmark/spawnspot = get_ghost_role_spawn()
+ if(!spawnspot)
+ to_chat(user, span_warning("No spawnpoint available."))
+ return
+
+ GLOB.allowed_ghost_spawns--
+ announce_ghost_joinleave(user, 0, "They are now a gravekeeper drone.")
+ var/obj/structure/ghost_pod/automatic/gravekeeper_drone/grave = new(get_turf(spawnspot))
+ grave.create_occupant(user)
+
+/datum/tgui_module/ghost_spawn_menu/proc/join_morpth(mob/observer/dead/user)
+ if(jobban_isbanned(user, JOB_GHOSTROLES))
+ to_chat(user, span_danger("You are banned from playing ghost roles and cannot spawn as a morph."))
+ return
+
+ if(GLOB.allowed_ghost_spawns <= 0)
+ to_chat(user, span_warning("There're no free ghost join slots."))
+ return
+
+ var/obj/effect/landmark/spawnspot = get_ghost_role_spawn()
+ if(!spawnspot)
+ to_chat(user, span_warning("No spawnpoint available."))
+ return
+
+ GLOB.allowed_ghost_spawns--
+ announce_ghost_joinleave(user, 0, "They are now a morph.")
+ var/obj/structure/ghost_pod/ghost_activated/morphspawn/no_announce/morph = new(get_turf(spawnspot))
+ morph.create_occupant(user)
+
+/datum/tgui_module/ghost_spawn_menu/proc/get_ghost_role_spawn()
+ var/list/possibleSpawnspots = list()
+ for(var/obj/effect/landmark/L in GLOB.landmarks_list)
+ if(L.name == JOB_GHOSTROLES)
+ possibleSpawnspots += L
+ if(possibleSpawnspots.len)
+ return pick(possibleSpawnspots)
+ return null
diff --git a/code/game/machinery/camera/tracking.dm b/code/game/machinery/camera/tracking.dm
index b87bafabb6..660f3ffe4f 100644
--- a/code/game/machinery/camera/tracking.dm
+++ b/code/game/machinery/camera/tracking.dm
@@ -102,12 +102,12 @@
var/list/cameras = list()
/mob/living/silicon/ai/proc/trackable_mobs()
- if(src.stat == 2) //ChompEDIT usr --> src
+ if(src.stat == 2)
return list()
var/datum/trackable/TB = new()
for(var/mob/living/M in GLOB.mob_list)
- if(M == src) //ChompEDIT usr --> src
+ if(M == src)
continue
if(M.tracking_status() != TRACKING_POSSIBLE)
continue
diff --git a/code/game/machinery/virtual_reality/vr_procs.dm b/code/game/machinery/virtual_reality/vr_procs.dm
index 33c4b53e63..5a952d48a5 100644
--- a/code/game/machinery/virtual_reality/vr_procs.dm
+++ b/code/game/machinery/virtual_reality/vr_procs.dm
@@ -54,44 +54,11 @@
drop_from_inventory(I)
qdel(src)
-/mob/observer/dead/verb/fake_enter_vr()
- set name = "Join virtual reality"
- set category = "Ghost.Join"
- set desc = "Log into NanoTrasen's local virtual reality server."
-
-/* Temp removal while I figure out how to reduce the respawn time to 1 minute
- var/time_till_respawn = time_till_respawn()
- if(time_till_respawn == -1) // Special case, never allowed to respawn
- to_chat(usr, span_warning("Respawning is not allowed!"))
+/mob/observer/dead/proc/fake_enter_vr(landmark)
+ if(!landmark)
return
- if(time_till_respawn) // Nonzero time to respawn
- to_chat(usr, span_warning("You can't do that yet! You died too recently. You need to wait another [round(time_till_respawn/10/60, 0.1)] minutes."))
- return
-*/
- var/datum/data/record/record_found
- record_found = find_general_record("name", client.prefs.real_name)
- // Found their record, they were spawned previously. Remind them corpses cannot play games.
- if(record_found)
- var/answer = tgui_alert(src,"You seem to have previously joined this round. If you are currently dead, you should not enter VR as this character. Would you still like to proceed?","Previously spawned",list("Yes","No"))
- if(answer != "Yes")
- return
- var/S = null
- var/list/vr_landmarks = list()
- for(var/obj/effect/landmark/virtual_reality/sloc in GLOB.landmarks_list)
- vr_landmarks += sloc.name
- if(!LAZYLEN(vr_landmarks))
- to_chat(src, "There are no available spawn locations in virtual reality.")
- return
- S = tgui_input_list(usr, "Please select a location to spawn your avatar at:", "Spawn location", vr_landmarks)
- if(!S)
- return 0
- for(var/obj/effect/landmark/virtual_reality/i in GLOB.landmarks_list)
- if(i.name == S)
- S = i
- break
-
- var/mob/living/carbon/human/avatar = new(get_turf(S), client.prefs.species)
+ var/mob/living/carbon/human/avatar = new(get_turf(landmark), client.prefs.species)
if(!avatar)
to_chat(src, "Something went wrong and spawning failed.")
return
diff --git a/code/game/objects/effects/job_start_landmarks.dm b/code/game/objects/effects/job_start_landmarks.dm
index 766ad8dd8c..dcd6e65a9e 100644
--- a/code/game/objects/effects/job_start_landmarks.dm
+++ b/code/game/objects/effects/job_start_landmarks.dm
@@ -96,6 +96,9 @@
name = JOB_TALON_PILOT
/obj/effect/landmark/start/talonminer
name = JOB_TALON_MINER
+
+/obj/effect/landmark/start/ghost_roles
+ name = JOB_GHOSTROLES
/obj/effect/landmark/start/outsider
name = JOB_OUTSIDER
/obj/effect/landmark/start/anomaly
diff --git a/code/modules/admin/admin_tools.dm b/code/modules/admin/admin_tools.dm
index 5660182f9d..27ed63aa24 100644
--- a/code/modules/admin/admin_tools.dm
+++ b/code/modules/admin/admin_tools.dm
@@ -4,6 +4,8 @@
set desc = "Check a player's attack logs."
show_cmd_admin_check_player_logs(M)
+ show_cmd_admin_check_player_logs(M)
+
//Views specific attack logs belonging to one player.
/client/proc/show_cmd_admin_check_player_logs(mob/living/M)
var/dat = span_bold("[M]'s Attack Log:
")
@@ -45,8 +47,8 @@
set desc = "Check a player's dialogue logs."
show_cmd_admin_check_dialogue_logs(M)
-/client/proc/show_cmd_admin_check_dialogue_logs(mob/living/M)
//Views specific dialogue logs belonging to one player.
+/client/proc/show_cmd_admin_check_dialogue_logs(mob/living/M)
var/dat = span_bold("[M]'s Dialogue Log:
")
dat += span_bold("Viewing say and emote logs of [M]") + " - (Played by ([key_name(M)]).
"
if(M.mind)
diff --git a/modular_chomp/code/modules/admin/verbs/tgui_verbs.dm b/code/modules/admin/verbs/tgui_verbs.dm
similarity index 80%
rename from modular_chomp/code/modules/admin/verbs/tgui_verbs.dm
rename to code/modules/admin/verbs/tgui_verbs.dm
index 0fd9c8416d..75539b5a0b 100644
--- a/modular_chomp/code/modules/admin/verbs/tgui_verbs.dm
+++ b/code/modules/admin/verbs/tgui_verbs.dm
@@ -9,7 +9,7 @@
var/list/modification_options = list(TGUI_VIEW_ATTACK_LOGS, TGUI_VIEW_DIALOG_LOGS, TGUI_RESIZE)
- var/tgui_list_choice = tgui_input_list(usr, "Select the verb you would like to use with a tgui input","Choice", modification_options)
+ var/tgui_list_choice = tgui_input_list(src, "Select the verb you would like to use with a tgui input","Choice", modification_options)
if(!tgui_list_choice || tgui_list_choice == "Cancel")
return
@@ -18,12 +18,12 @@
switch(tgui_list_choice)
if(TGUI_VIEW_ATTACK_LOGS)
- var/mob/living/L = tgui_input_list(usr, "Check a player's attack logs.", "Check Player Attack Logs", GLOB.mob_list)
+ var/mob/living/L = tgui_input_list(src, "Check a player's attack logs.", "Check Player Attack Logs", GLOB.mob_list)
show_cmd_admin_check_player_logs(L)
if(TGUI_VIEW_DIALOG_LOGS)
- var/mob/living/L = tgui_input_list(usr, "Check a player's dialogue logs.", "Check Player Dialogue Logs", GLOB.mob_list)
+ var/mob/living/L = tgui_input_list(src, "Check a player's dialogue logs.", "Check Player Dialogue Logs", GLOB.mob_list)
show_cmd_admin_check_dialogue_logs(L)
if(TGUI_RESIZE)
- var/mob/living/L = tgui_input_list(usr, "Resizes any living mob without any restrictions on size.", "Resize", GLOB.mob_list)
+ var/mob/living/L = tgui_input_list(src, "Resizes any living mob without any restrictions on size.", "Resize", GLOB.mob_list)
if(L)
do_resize(L)
diff --git a/code/modules/mob/dead/observer/observer.dm b/code/modules/mob/dead/observer/observer.dm
index daa117c252..1ef40d8cbb 100644
--- a/code/modules/mob/dead/observer/observer.dm
+++ b/code/modules/mob/dead/observer/observer.dm
@@ -41,6 +41,7 @@
var/ghost_sprite = null
var/last_revive_notification = null // world.time of last notification, used to avoid spamming players from defibs or cloners.
var/cleanup_timer // Refernece to a timer that will delete this mob if no client returns
+ var/selecting_ghostrole = FALSE
invisibility = INVISIBILITY_OBSERVER
layer = BELOW_MOB_LAYER
@@ -654,61 +655,6 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp
var/rads = SSradiation.get_rads_at_turf(t)
to_chat(src, span_notice("Radiation level: [rads ? rads : "0"] Bq."))
-
-/mob/observer/dead/verb/become_mouse()
- set name = "Become mouse"
- set category = "Ghost.Join"
-
- if(CONFIG_GET(flag/disable_player_mice))
- to_chat(src, span_warning("Spawning as a mouse is currently disabled."))
- return
-
- //VOREStation Add Start
- if(jobban_isbanned(src, JOB_GHOSTROLES))
- to_chat(src, span_warning("You cannot become a mouse because you are banned from playing ghost roles."))
- return
- //VOREStation Add End
-
- if(!MayRespawn(1))
- return
-
- var/turf/T = get_turf(src)
- if(!T || (T.z in using_map.admin_levels))
- to_chat(src, span_warning("You may not spawn as a mouse on this Z-level."))
- return
-
- var/timedifference = world.time - client.time_died_as_mouse
- if(client.time_died_as_mouse && timedifference <= CONFIG_GET(number/mouse_respawn_time) MINUTES)
- var/timedifference_text
- timedifference_text = time2text(CONFIG_GET(number/mouse_respawn_time) MINUTES - timedifference,"mm:ss")
- to_chat(src, span_warning("You may only spawn again as a mouse more than [CONFIG_GET(number/mouse_respawn_time)] minutes after your death. You have [timedifference_text] left."))
- return
-
- var/response = tgui_alert(src, "Are you -sure- you want to become a mouse? You will have no rights or OOC protections.","Are you sure you want to squeek? You will have no rights or OOC protections.",list("Squeek!","Nope!")) //CHOMP Edit
- if(response != "Squeek!") return //Hit the wrong key...again.
-
-
- //find a viable mouse candidate
- var/mob/living/simple_mob/animal/passive/mouse/host
- var/obj/machinery/atmospherics/unary/vent_pump/vent_found
- var/list/found_vents = list()
- for(var/obj/machinery/atmospherics/unary/vent_pump/v in GLOB.machines)
- if(!v.welded && v.z == T.z && v.network && v.network.normal_members.len > 20)
- found_vents.Add(v)
- if(found_vents.len)
- vent_found = pick(found_vents)
- host = new /mob/living/simple_mob/animal/passive/mouse(vent_found)
- else
- to_chat(src, span_warning("Unable to find any unwelded vents to spawn mice at."))
-
- if(host)
- if(CONFIG_GET(flag/uneducated_mice))
- host.universal_understand = 0
- announce_ghost_joinleave(src, 0, "They are now a mouse.")
- host.ckey = src.ckey
- host.add_ventcrawl(vent_found)
- to_chat(host, span_info("You are now a mouse. Try to avoid interaction with players, and do not give hints away that you are more than a simple rodent."))
-
/mob/observer/dead/verb/view_manfiest()
set name = "Show Crew Manifest"
set category = "Ghost.Game"
@@ -922,7 +868,7 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp
return 0
if(mind && mind.current && mind.current.stat != DEAD && can_reenter_corpse)
if(feedback)
- to_chat(src, span_warning("Your non-dead body prevent you from respawning."))
+ to_chat(src, span_warning("Your non-dead body prevents you from respawning."))
return 0
if(CONFIG_GET(flag/antag_hud_restricted) && has_enabled_antagHUD == 1)
if(feedback)
diff --git a/code/modules/mob/dead/observer/observer_vr.dm b/code/modules/mob/dead/observer/observer_vr.dm
index 861fb5e1f3..c12d080fee 100644
--- a/code/modules/mob/dead/observer/observer_vr.dm
+++ b/code/modules/mob/dead/observer/observer_vr.dm
@@ -1,62 +1,3 @@
-/mob/observer/dead/verb/nifjoin()
- set category = "Ghost.Join"
- set name = "Join Into Soulcatcher"
- set desc = "Select a player with a working NIF + Soulcatcher NIFSoft to join into it."
-
- var/picked = tgui_input_list(src, "Pick a friend with NIF and Soulcatcher to join into. Harrass strangers, get banned. Not everyone has a NIF w/ Soulcatcher.","Select a player", GLOB.player_list)
-
- //Didn't pick anyone or picked a null
- if(!picked)
- return
-
- //Good choice testing and some instance-grabbing
- if(!ishuman(picked))
- to_chat(src,span_warning("[picked] isn't in a humanoid mob at the moment."))
- return
-
- var/mob/living/carbon/human/H = picked
-
- if(H.stat || !H.client)
- to_chat(src,span_warning("[H] isn't awake/alive at the moment."))
- return
-
- if(!H.nif)
- to_chat(src,span_warning("[H] doesn't have a NIF installed."))
- return
-
- var/datum/nifsoft/soulcatcher/SC = H.nif.imp_check(NIF_SOULCATCHER)
- if(!SC)
- to_chat(src,span_warning("[H] doesn't have the Soulcatcher NIFSoft installed, or their NIF is unpowered."))
- return
-
- //Fine fine, we can ask.
- var/obj/item/nif/nif = H.nif
- to_chat(src,span_notice("Request sent to [H]."))
-
- var/req_time = world.time
- nif.notify("Transient mindstate detected, analyzing...")
- sleep(15) //So if they are typing they get interrupted by sound and message, and don't type over the box
- var/response = tgui_alert(H,"[src] ([src.key]) wants to join into your Soulcatcher.","Soulcatcher Request",list("Deny","Allow"), timeout = 1 MINUTE)
-
- if(!response || response == "Deny")
- to_chat(src,span_warning("[H] denied your request."))
- return
-
- if((world.time - req_time) > 1 MINUTE)
- to_chat(H,span_warning("The request had already expired. (1 minute waiting max)"))
- return
-
- //Final check since we waited for input a couple times.
- if(H && src && src.key && !H.stat && nif && SC)
- if(!mind) //No mind yet, aka haven't played in this round.
- mind = new(key)
-
- mind.name = name
- mind.current = src
- mind.active = TRUE
-
- SC.catch_mob(src) //This will result in us being deleted so...
-
/mob/observer/dead/verb/backup_ping()
set category = "Ghost.Join"
set name = "Notify Transcore"
@@ -106,30 +47,17 @@
/mob/observer/dead/verb/findghostpod() //Moves the ghost instead of just changing the ghosts's eye -Nodrak
set category = "Ghost.Join"
- set name = "Find Ghost Pod"
- set desc = "Find an active ghost pod"
- set popup_menu = FALSE
+ set name = "Ghost Spawn"
+ set desc = "Open Ghost Spawn Menu"
if(!isobserver(src)) //Make sure they're an observer!
return
- var/input = tgui_input_list(src, "Select a ghost pod:", "Ghost Jump", observe_list_format(GLOB.active_ghost_pods))
- if(!input)
- to_chat(src, span_filter_notice("No active ghost pods detected."))
+ if(selecting_ghostrole)
return
- var/target = observe_list_format(GLOB.active_ghost_pods)[input]
- if (!target)//Make sure we actually have a target
- return
- else
- var/obj/O = target //Destination mob
- var/turf/T = get_turf(O) //Turf of the destination mob
-
- if(T && isturf(T)) //Make sure the turf exists, then move the source to that destination.
- forceMove(T)
- stop_following()
- else
- to_chat(src, span_filter_notice("This ghost pod is not located in the game world."))
+ var/datum/tgui_module/ghost_spawn_menu/ui = new(src)
+ ui.tgui_interact(src)
/mob/observer/dead/verb/findautoresleever()
set category = "Ghost.Join"
diff --git a/code/modules/mob/living/silicon/robot/drone/drone_manufacturer.dm b/code/modules/mob/living/silicon/robot/drone/drone_manufacturer.dm
index ac6ebe096e..201d67492b 100644
--- a/code/modules/mob/living/silicon/robot/drone/drone_manufacturer.dm
+++ b/code/modules/mob/living/silicon/robot/drone/drone_manufacturer.dm
@@ -34,6 +34,15 @@
fabricator_tag = "Upper Level Mining"
drone_type = /mob/living/silicon/robot/drone/mining
+/obj/machinery/drone_fabricator/Initialize(mapload)
+ . = ..()
+ GLOB.all_drone_fabricators += src
+
+
+/obj/machinery/drone_fabricator/Destroy()
+ GLOB.all_drone_fabricators -= src
+ . = ..()
+
/obj/machinery/drone_fabricator/power_change()
..()
if (stat & NOPOWER)
@@ -88,70 +97,3 @@
new_drone.transfer_personality(player)
return new_drone
-
-/mob/observer/dead/verb/join_as_drone()
-
- set category = "Ghost.Join"
- set name = "Join As Drone"
- set desc = "If there is a powered, enabled fabricator in the game world with a prepared chassis, join as a maintenance drone."
-
- if(ticker.current_state < GAME_STATE_PLAYING)
- to_chat(src, span_danger("The game hasn't started yet!"))
- return
-
- if(!CONFIG_GET(flag/allow_drone_spawn))
- to_chat(src, span_danger("That verb is not currently permitted."))
- return
-
- if (!src.stat)
- return
-
- if (usr != src)
- return 0 //something is terribly wrong
-
- if(jobban_isbanned(src,JOB_CYBORG))
- to_chat(src, span_danger("You are banned from playing synthetics and cannot spawn as a drone."))
- return
-
- // VOREStation Addition Start
- if(CONFIG_GET(flag/use_age_restriction_for_jobs) && isnum(src.client.player_age))
- var/time_till_play = max(0, 3 - src.client.player_age)
- if(time_till_play)
- to_chat(src, span_danger("You have not been playing on the server long enough to join as drone."))
- return
- // VOREStation Addition End
-
- if(!MayRespawn(1))
- return
-
- var/deathtime = world.time - src.timeofdeath
- var/deathtimeminutes = round(deathtime / (1 MINUTE))
- var/pluralcheck = "minute"
- if(deathtimeminutes == 0)
- pluralcheck = ""
- else if(deathtimeminutes == 1)
- pluralcheck = " [deathtimeminutes] minute and"
- else if(deathtimeminutes > 1)
- pluralcheck = " [deathtimeminutes] minutes and"
- var/deathtimeseconds = round((deathtime - deathtimeminutes * 1 MINUTE) / 10,1)
-
- if (deathtime < 5 MINUTES)
- to_chat(src, "You have been dead for[pluralcheck] [deathtimeseconds] seconds.")
- to_chat(src, "You must wait 5 minutes to respawn as a drone!")
- return
-
- var/list/all_fabricators = list()
- for(var/obj/machinery/drone_fabricator/DF in GLOB.machines)
- if(DF.stat & NOPOWER || !DF.produce_drones)
- continue
- if(DF.drone_progress >= 100)
- all_fabricators[DF.fabricator_tag] = DF
-
- if(!all_fabricators.len)
- to_chat(src, span_danger("There are no available drone spawn points, sorry."))
- return
-
- var/choice = tgui_input_list(src, "Which fabricator do you wish to use?", "Fabricator Choice", all_fabricators)
- if(choice)
- var/obj/machinery/drone_fabricator/chosen_fabricator = all_fabricators[choice]
- chosen_fabricator.create_drone(src.client)
diff --git a/code/modules/vore/eating/inbelly_spawn.dm b/code/modules/vore/eating/inbelly_spawn.dm
index f04dd24d8e..04cee308d3 100644
--- a/code/modules/vore/eating/inbelly_spawn.dm
+++ b/code/modules/vore/eating/inbelly_spawn.dm
@@ -1,68 +1,3 @@
-/mob/observer/dead/verb/spawn_in_belly()
- set category = "Ghost.Join"
- set name = "Spawn In Belly"
- set desc = "Spawn in someone's belly."
-
- if(!client)
- return
-
- // If any ghost-side restrictions are desired, they'll go here
-
- tgui_alert(src,{"
-This verb allows you to spawn inside someone's belly when they are in round.
-Make sure you to coordinate with your predator OOCly as well as roleplay approprietly.
-You are considered to have been in the belly entire time the predator was around and are not added to crew lists.
-This is not intended to be used for mechanical advantage or providing assistance, but for facilitating longterm scenes.
-Please do not abuse this ability.
-"},"OOC Warning") // Warning.
-
- var/list/eligible_targets = list()
-
- for(var/mob/living/pred in GLOB.living_mob_list)
- if(!istype(pred) || !pred.client) // Ignore preds that aren't living mobs or player controlled
- continue
- if(pred.no_vore) // No vore, no bellies, no inbelly spawning
- continue
- if(!(get_z(pred) in using_map.station_levels)) // No explo reinforcements
- continue
- if(ishuman(pred))
- var/mob/living/carbon/human/H = pred
- if(!H.latejoin_vore)
- continue
- eligible_targets += H
- continue
- if(issilicon(pred))
- var/mob/living/silicon/S = pred
- if(isAI(S))
- continue // Sorry, AI buddies. Your vore works too differently.
- if(!S.latejoin_vore)
- continue
- eligible_targets += S
- continue
- if(isanimal(pred))
- var/mob/living/simple_mob/SM = pred
- if(!SM.vore_active) // No vore, no bellies, no inbelly spawning
- continue
- if(!SM.latejoin_vore)
- continue
- eligible_targets += SM
- continue
-
- // Only humans, simple_mobs and non-AI silicons are included. Obscure stuff like bots is skipped.
-
- if(!eligible_targets.len)
- to_chat(src, span_notice("No eligible preds were found.")) // :(
- return
-
- var/mob/living/target = tgui_input_list(src, "Please specify which character you want to spawn inside of.", "Predator", eligible_targets) // Offer the list of things we gathered.
-
- if(!target || !client) // Did out target cease to exist? Or did we?
- return
-
- // Notify them that its now pred's turn
- to_chat(src, span_notice("Inbelly spawn request sent to predator."))
- target.inbelly_spawn_prompt(client) // Hand reins over to them
-
/mob/living/proc/inbelly_spawn_prompt(client/potential_prey)
if(!potential_prey || !istype(potential_prey)) // Did our prey cease to exist?
return
@@ -127,7 +62,6 @@ Please do not abuse this ability.
else
to_chat(potential_prey, span_notice("Inbelly spawn cancelled."))
to_chat(src, span_notice("Prey cancelled their inbelly spawn request."))
- return
/proc/inbelly_spawn(client/prey, mob/living/pred, obj/belly/target_belly, var/absorbed = FALSE)
// All this is basically admin late spawn-in, but skipping all parts related to records and equipment and with predteremined location
diff --git a/code/modules/vore/eating/soulcatcher_observer.dm b/code/modules/vore/eating/soulcatcher_observer.dm
deleted file mode 100644
index ccd53c5238..0000000000
--- a/code/modules/vore/eating/soulcatcher_observer.dm
+++ /dev/null
@@ -1,52 +0,0 @@
-/mob/observer/dead/verb/soulcatcherjoin()
- set category = "Ghost.Join"
- set name = "Join Into Soulcatcher(Vore)"
- set desc = "Select a player with enabled Soulcatcher to join."
-
- var/list/valid_players = list()
- for(var/mob/player in GLOB.player_list)
- if(player.soulgem?.flag_check(SOULGEM_ACTIVE | SOULGEM_CATCHING_GHOSTS, TRUE))
- valid_players += player
-
- if(!valid_players.len)
- to_chat(src, span_warning("No valid players found."))
- return
-
- var/picked = tgui_input_list(usr, "Pick a friend with enabled Soulcatcher to join into. Harrass strangers, get banned.","Select a player", valid_players)
-
- if(!picked)
- return
-
- if(!ismob(picked))
- to_chat(src, span_warning("Target is no mob."))
- return
-
- var/mob/M = picked
-
- var/obj/soulgem/gem = M.soulgem
-
- if(!gem?.flag_check(SOULGEM_ACTIVE))
- to_chat(src, span_warning("[M] has no enabled Soulcatcher."))
- return
-
- var/req_time = world.time
- gem.notify_holder("Transient mindstate detected, analyzing...")
- sleep(15) //So if they are typing they get interrupted by sound and message, and don't type over the box
- if(tgui_alert(M,"[src.name] wants to join into your Soulcatcher.","Soulcatcher Request",list("Deny","Allow"), timeout=1 MINUTES) != "Allow")
- to_chat(src, span_warning("[M] has denied your request."))
- return
-
- if((world.time - req_time) > 1 MINUTES)
- to_chat(M, span_warning("The request had already expired. (1 minute waiting max)"))
- return
-
- //Final check since we waited for input a couple times.
- if(M && src && src.key && !M.stat && gem?.flag_check(SOULGEM_ACTIVE | SOULGEM_CATCHING_GHOSTS, TRUE))
- if(!mind) //No mind yet, aka haven't played in this round.
- mind = new(key)
-
- mind.name = name
- mind.current = src
- mind.active = TRUE
-
- gem.catch_mob(src) //This will result in us being deleted so...
diff --git a/modular_chomp/maps/southern_cross/southern_cross-1.dmm b/modular_chomp/maps/southern_cross/southern_cross-1.dmm
index ea9b352f1a..42479e354e 100644
--- a/modular_chomp/maps/southern_cross/southern_cross-1.dmm
+++ b/modular_chomp/maps/southern_cross/southern_cross-1.dmm
@@ -163,6 +163,11 @@
},
/turf/simulated/floor/tiled/white,
/area/maintenance/abfirstaid)
+"ax" = (
+/obj/effect/floor_decal/rust,
+/obj/effect/landmark/start/ghost_roles,
+/turf/simulated/floor/plating/turfpack/station,
+/area/maintenance/zeroport)
"ay" = (
/obj/machinery/door/firedoor/border_only,
/obj/effect/floor_decal/industrial/arrows/yellow{
@@ -175,6 +180,28 @@
/obj/item/tape/atmos,
/turf/simulated/floor/tiled/techmaint,
/area/maintenance/zeroaft)
+"az" = (
+/obj/effect/landmark/start/ghost_roles,
+/turf/simulated/floor/plating/turfpack/station,
+/area/maintenance/thrift)
+"aA" = (
+/obj/structure/cable{
+ icon_state = "4-8"
+ },
+/obj/machinery/atmospherics/pipe/simple/hidden/supply{
+ dir = 4
+ },
+/obj/machinery/atmospherics/pipe/simple/hidden/scrubbers{
+ dir = 4
+ },
+/obj/effect/landmark/start/ghost_roles,
+/turf/simulated/floor/plating/turfpack/station,
+/area/maintenance/zerostarboard)
+"aB" = (
+/obj/random/trash,
+/obj/effect/landmark/start/ghost_roles,
+/turf/simulated/floor/plating/turfpack/station,
+/area/maintenance/zerofore)
"aC" = (
/obj/effect/floor_decal/rust,
/obj/structure/table/rack,
@@ -32389,7 +32416,7 @@ sV
sV
ua
Nc
-PK
+ax
ua
TJ
TJ
@@ -43709,7 +43736,7 @@ wZ
wZ
wZ
Qy
-TY
+aB
je
Lc
IH
@@ -54550,7 +54577,7 @@ wZ
wZ
Ky
JA
-MZ
+aA
sW
CP
KM
@@ -58180,7 +58207,7 @@ XE
AR
TG
Pd
-AR
+az
wX
vp
wS
diff --git a/modular_chomp/maps/southern_cross/southern_cross-10.dmm b/modular_chomp/maps/southern_cross/southern_cross-10.dmm
index 8f5808ea8f..4505bae699 100644
--- a/modular_chomp/maps/southern_cross/southern_cross-10.dmm
+++ b/modular_chomp/maps/southern_cross/southern_cross-10.dmm
@@ -15,6 +15,10 @@
"ae" = (
/turf/simulated/floor/outdoors/grass/sif/forest/planetuse,
/area/surface/outside/wilderness/deep)
+"af" = (
+/obj/effect/landmark/start/ghost_roles,
+/turf/simulated/floor/outdoors/grass/sif/planetuse,
+/area/surface/outside/wilderness/normal)
"ag" = (
/obj/effect/zone_divider,
/turf/simulated/floor/outdoors/grass/sif/forest/planetuse,
@@ -32,6 +36,10 @@
/obj/effect/zone_divider,
/turf/simulated/mineral/sif,
/area/surface/outside/wilderness/mountains)
+"al" = (
+/obj/effect/landmark/start/ghost_roles,
+/turf/simulated/floor/outdoors/grass/sif/forest/planetuse,
+/area/surface/outside/wilderness/normal)
"am" = (
/turf/simulated/floor/water,
/area/surface/outside/river/svartan)
@@ -5178,7 +5186,7 @@ at
at
at
at
-at
+al
ac
ac
ac
@@ -5435,7 +5443,7 @@ at
at
at
at
-at
+al
at
ac
ac
@@ -59875,7 +59883,7 @@ aM
aM
aM
aM
-aG
+af
ac
ac
ac
diff --git a/modular_chomp/maps/southern_cross/southern_cross-5.dmm b/modular_chomp/maps/southern_cross/southern_cross-5.dmm
index 29e11027dc..1deda415c6 100644
--- a/modular_chomp/maps/southern_cross/southern_cross-5.dmm
+++ b/modular_chomp/maps/southern_cross/southern_cross-5.dmm
@@ -808,6 +808,10 @@
},
/turf/simulated/floor/wood,
/area/surface/outpost/main/dorms/dorm_2)
+"bA" = (
+/obj/effect/landmark/start/ghost_roles,
+/turf/simulated/floor/outdoors/grass/sif/forest/planetuse,
+/area/surface/outside/plains/normal)
"bB" = (
/obj/machinery/atmospherics/pipe/simple/hidden/scrubbers{
dir = 4
@@ -818,6 +822,10 @@
/obj/machinery/hologram/holopad,
/turf/simulated/floor/wood,
/area/surface/outpost/civilian/sauna)
+"bC" = (
+/obj/effect/landmark/start/ghost_roles,
+/turf/simulated/floor/outdoors/grass/sif/planetuse,
+/area/surface/outside/plains/normal)
"bD" = (
/obj/structure/bed/chair/comfy/black{
dir = 1
@@ -835,6 +843,10 @@
/obj/item/extinguisher/mini,
/turf/simulated/floor/tiled,
/area/surface/outpost/mining_main)
+"bF" = (
+/obj/effect/landmark/start/ghost_roles,
+/turf/simulated/floor/outdoors/dirt/sif/planetuse,
+/area/surface/outside/plains/normal)
"bG" = (
/obj/effect/floor_decal/industrial/outline/red,
/turf/simulated/floor/tiled/monotile,
@@ -29066,7 +29078,7 @@ yj
yj
yj
yj
-yj
+bA
XE
XE
XE
@@ -43777,7 +43789,7 @@ yj
yj
yj
yj
-yj
+bA
XE
Vg
"}
@@ -67770,7 +67782,7 @@ yT
yT
Ey
Ey
-Ey
+bC
bv
bv
rV
@@ -88923,7 +88935,7 @@ yT
yT
yT
yT
-yT
+bF
XE
XE
XE
diff --git a/tgui/packages/tgui/interfaces/GhostSpawn/GhostJoin/GhostJoin.tsx b/tgui/packages/tgui/interfaces/GhostSpawn/GhostJoin/GhostJoin.tsx
new file mode 100644
index 0000000000..5120c00be9
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/GhostSpawn/GhostJoin/GhostJoin.tsx
@@ -0,0 +1,189 @@
+import { useState } from 'react';
+import { useBackend } from 'tgui/backend';
+import {
+ Button,
+ Dropdown,
+ LabeledList,
+ NoticeBox,
+ Section,
+ Stack,
+} from 'tgui-core/components';
+
+import {
+ describeDroneData,
+ describeMouseData,
+ describeSpecialData,
+} from '../functions';
+import type { describeReturnData, GhostJoinData } from '../types';
+import { SpawnElement } from './SpawnElement';
+
+export const GhostJoin = (props: { all_ghost_join_options: GhostJoinData }) => {
+ const { act } = useBackend();
+ const { all_ghost_join_options } = props;
+
+ const {
+ mouse_data,
+ drone_data,
+ vr_data,
+ may_respawn,
+ ghost_banned,
+ cyborg_banned,
+ existing_ghost_spawnpoints,
+ remaining_ghost_roles,
+ special_role_respawn,
+ } = all_ghost_join_options;
+
+ const [selectedFab, setSelectedFab] = useState('');
+ const [selectedLandmark, setSelectedLandmark] = useState('');
+
+ const describedMouse = describeMouseData(mouse_data, !!ghost_banned);
+ const describeDrone = describeDroneData(drone_data, !!cyborg_banned);
+
+ const specialRoles: describeReturnData[] = [];
+
+ specialRoles[0] = describeSpecialData(
+ 'Corgi',
+ !!ghost_banned,
+ remaining_ghost_roles,
+ !!existing_ghost_spawnpoints,
+ special_role_respawn,
+ 'corgi_spawn',
+ );
+ specialRoles[1] = describeSpecialData(
+ 'Lost Drone',
+ !!cyborg_banned,
+ remaining_ghost_roles,
+ !!existing_ghost_spawnpoints,
+ special_role_respawn,
+ 'lost_drone_spawn',
+ );
+ specialRoles[2] = describeSpecialData(
+ 'Maint Pred',
+ !!ghost_banned,
+ remaining_ghost_roles,
+ !!existing_ghost_spawnpoints,
+ special_role_respawn,
+ 'maintpred_spawn',
+ );
+ specialRoles[3] = describeSpecialData(
+ 'Gravekeeper Drone',
+ !!cyborg_banned,
+ remaining_ghost_roles,
+ !!existing_ghost_spawnpoints,
+ special_role_respawn,
+ 'gravekeeper_spawn',
+ );
+ specialRoles[4] = describeSpecialData(
+ 'Morph',
+ !!ghost_banned,
+ remaining_ghost_roles,
+ !!existing_ghost_spawnpoints,
+ special_role_respawn,
+ 'morph_spawn',
+ );
+
+ const playerDropdown = Object.entries(drone_data.fabricators).map((entry) => {
+ return { displayText: entry[1], value: entry[0] };
+ });
+
+ const landmarkDropdown = Object.entries(vr_data.vr_landmarks).map((entry) => {
+ return { displayText: entry[1], value: entry[0] };
+ });
+ return (
+
+
+
+
+ {!!vr_data.record_found && (
+
+
+ You seem to have previously joined this round. If you are
+ currently dead, you should not enter VR as this character.
+
+
+ )}
+
+
+
+
+
+
+
+ act('drone_spawn', { fabricator: selectedFab })
+ }
+ >
+ Become Drone?
+
+
+
+ setSelectedFab(value)}
+ options={playerDropdown}
+ selected={drone_data.fabricators[selectedFab]}
+ />
+
+
+
+
+
+
+
+ act('vr_spawn', { landmark: selectedLandmark })
+ }
+ >
+ Enter VR?
+
+
+
+ setSelectedLandmark(value)}
+ options={landmarkDropdown}
+ selected={vr_data.vr_landmarks[selectedLandmark]}
+ />
+
+
+
+
+
+
+
+
+
+
+
+ {specialRoles.map((role) => (
+
+ ))}
+
+
+
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/GhostSpawn/GhostJoin/SpawnElement.tsx b/tgui/packages/tgui/interfaces/GhostSpawn/GhostJoin/SpawnElement.tsx
new file mode 100644
index 0000000000..2d7de71239
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/GhostSpawn/GhostJoin/SpawnElement.tsx
@@ -0,0 +1,31 @@
+import { useBackend } from 'tgui/backend';
+import { Button, LabeledList, Stack } from 'tgui-core/components';
+
+export const SpawnElement = (props: {
+ title: string;
+ disabled: boolean;
+ tooltip: string;
+ action: string;
+}) => {
+ const { act } = useBackend();
+ const { title, disabled, tooltip, action } = props;
+
+ return (
+
+
+
+ act(action)}
+ >
+ {'Become ' + title + '?'}
+
+
+
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/GhostSpawn/GhostPod/GhostPod.tsx b/tgui/packages/tgui/interfaces/GhostSpawn/GhostPod/GhostPod.tsx
new file mode 100644
index 0000000000..f6534a3344
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/GhostSpawn/GhostPod/GhostPod.tsx
@@ -0,0 +1,50 @@
+import { useEffect, useState } from 'react';
+import { Stack } from 'tgui-core/components';
+
+import type { PodData } from '../types';
+import { PodSelectionList } from './PodSelectionList';
+import { SelectionList } from './SelectionList';
+
+export const GhostPod = (props: {
+ all_ghost_pods: PodData[];
+ user_z: number;
+}) => {
+ const [selectedType, setSelectedType] = useState('');
+ const [selectedPod, setSelectedPod] = useState('');
+
+ const { all_ghost_pods, user_z } = props;
+
+ const podTypes = [...new Set(all_ghost_pods.map((pod) => pod.pod_type))].sort(
+ (a, b) => a.localeCompare(b),
+ );
+ const selectedGhostPods = all_ghost_pods.filter(
+ (ghostPod) => ghostPod.pod_type === selectedType,
+ );
+
+ useEffect(() => {
+ if (podTypes.length) {
+ setSelectedType(podTypes[0]);
+ }
+ }, []);
+
+ return (
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/GhostSpawn/GhostPod/PodSelectionList.tsx b/tgui/packages/tgui/interfaces/GhostSpawn/GhostPod/PodSelectionList.tsx
new file mode 100644
index 0000000000..03596f41f8
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/GhostSpawn/GhostPod/PodSelectionList.tsx
@@ -0,0 +1,53 @@
+import { useState } from 'react';
+import { Button, Input, Section, Stack } from 'tgui-core/components';
+import { createSearch } from 'tgui-core/string';
+
+export const PodSelectionList = (props: {
+ podTypes: string[];
+ selectedPod: string;
+ onSelectedPod: React.Dispatch>;
+}) => {
+ const { podTypes, selectedPod, onSelectedPod } = props;
+
+ const [searchText, setSearchText] = useState('');
+
+ const searcher = createSearch(searchText, (element: string) => {
+ return element;
+ });
+
+ const filtered = podTypes?.filter(searcher);
+
+ return (
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/GhostSpawn/GhostPod/SelectionList.tsx b/tgui/packages/tgui/interfaces/GhostSpawn/GhostPod/SelectionList.tsx
new file mode 100644
index 0000000000..661ffe2875
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/GhostSpawn/GhostPod/SelectionList.tsx
@@ -0,0 +1,105 @@
+import { useEffect, useState } from 'react';
+import { useBackend } from 'tgui/backend';
+import { Button, Input, Section, Stack } from 'tgui-core/components';
+import { createSearch } from 'tgui-core/string';
+
+import type { PodData } from '../types';
+
+export const SelectionList = (props: {
+ allPods: PodData[];
+ userZ: number;
+ selectedType: string;
+ selectedPod: string;
+ onSelectedPod: React.Dispatch>;
+}) => {
+ const { act } = useBackend();
+ const { allPods, userZ, selectedType, selectedPod, onSelectedPod } = props;
+
+ const [searchText, setSearchText] = useState('');
+ const [usePlayerZ, setUsePlayerZ] = useState(false);
+
+ const searcher = createSearch(searchText, (element: PodData) => {
+ return element.pod_name;
+ });
+
+ const filtered = allPods
+ ?.filter(searcher)
+ .sort((a, b) =>
+ a.remains_active === b.remains_active
+ ? 0
+ : a.remains_active
+ ? -1
+ : 1 || a.z_level - b.z_level,
+ )
+ .filter((pod) => {
+ if (usePlayerZ) {
+ return pod.z_level === userZ;
+ }
+ return true;
+ });
+
+ useEffect(() => {
+ if (filtered.length) {
+ onSelectedPod(filtered[0].ref);
+ }
+ }, [selectedType]);
+
+ return (
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/GhostSpawn/Vorespawn/Vorespawn.tsx b/tgui/packages/tgui/interfaces/GhostSpawn/Vorespawn/Vorespawn.tsx
new file mode 100644
index 0000000000..6e8868a61c
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/GhostSpawn/Vorespawn/Vorespawn.tsx
@@ -0,0 +1,139 @@
+import { useState } from 'react';
+import { useBackend } from 'tgui/backend';
+import { Box, Button, Input, Section, Stack } from 'tgui-core/components';
+import { createSearch } from 'tgui-core/string';
+
+import type { DropdownEntry, VoreSpawnData } from '../types';
+
+export const Vorespawn = (props: {
+ all_vore_spawns: Record;
+}) => {
+ const { act } = useBackend();
+ const { all_vore_spawns } = props;
+
+ const [selectedPlayer, setSelectedPlayer] = useState('');
+ const [searchText, setSearchText] = useState('');
+
+ const playerDropdown = Object.entries(all_vore_spawns).map((entry) => {
+ return { displayText: entry[1].player, value: entry[0] };
+ });
+
+ const allowSoulcatcher = all_vore_spawns[selectedPlayer]?.soulcatcher;
+ const allowSoulcatcherVore =
+ all_vore_spawns[selectedPlayer]?.soulcatcher_vore;
+ const allowBellySpawn = all_vore_spawns[selectedPlayer]?.vorespawn;
+
+ const searcher = createSearch(searchText, (element: DropdownEntry) => {
+ return element.displayText;
+ });
+
+ const filtered = playerDropdown?.filter(searcher);
+
+ return (
+
+
+
+ {
+ "This verb allows you to spawn inside someone's belly when they are in round. Make sure you to coordinate with your predator OOCly as well as roleplay approprietly. You are considered to have been in the belly entire time the predator was around and are not added to crew lists. This is not intended to be used for mechanical advantage or providing assistance, but for facilitating longterm scenes. Please do not abuse this ability."
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ act('soulcatcher_spawn', {
+ selected_player: selectedPlayer,
+ })
+ }
+ tooltip="Select a player with a working NIF + Soulcatcher NIFSoft to join into it."
+ >
+ Soulcatcher
+
+
+
+
+
+
+
+
+ act('soulcatcher_vore_spawn', {
+ selected_player: selectedPlayer,
+ })
+ }
+ tooltip="Select a player with enabled Vore Soulcatcher to join."
+ >
+ Soulcatcher (Vore)
+
+
+
+
+
+
+
+
+ act('bellyspawn', { selected_player: selectedPlayer })
+ }
+ >
+ Spawn In Belly
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/GhostSpawn/functions.ts b/tgui/packages/tgui/interfaces/GhostSpawn/functions.ts
new file mode 100644
index 0000000000..fc39100f67
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/GhostSpawn/functions.ts
@@ -0,0 +1,115 @@
+import type { describeReturnData, DroneData, MouseData } from './types';
+
+export function describeMouseData(
+ data: MouseData,
+ banned: boolean,
+): describeReturnData {
+ const returnData = { text: 'Spawn as mouse.', state: true };
+ if (data.disabled) {
+ returnData.text = 'Spawning as a mouse is currently disabled.';
+ returnData.state = false;
+ return returnData;
+ }
+ if (banned) {
+ returnData.text =
+ 'You cannot become a mouse because you are banned from playing ghost roles.';
+ returnData.state = false;
+ return returnData;
+ }
+ if (data.bad_turf) {
+ returnData.text = 'You may not spawn as a mouse on this Z-level.';
+ returnData.state = false;
+ return returnData;
+ }
+ if (data.respawn_time) {
+ returnData.text =
+ 'You must wait ' +
+ data.respawn_time +
+ ' before being able to respawn again.';
+ returnData.state = false;
+ return returnData;
+ }
+ if (!data.found_vents) {
+ returnData.text = 'Unable to find any unwelded vents to spawn mice at.';
+ returnData.state = false;
+ return returnData;
+ }
+ return returnData;
+}
+
+export function describeDroneData(
+ data: DroneData,
+ banned: boolean,
+): describeReturnData {
+ const returnData = {
+ text: 'If there is a powered, enabled fabricator in the game world with a prepared chassis, join as a maintenance drone.',
+ state: true,
+ };
+ if (data.disabled) {
+ returnData.text = 'Spawning as a drone is currently disabled.';
+ returnData.state = false;
+ return returnData;
+ }
+ if (banned) {
+ returnData.text =
+ 'You are banned from playing synthetics and cannot spawn as a drone.';
+ returnData.state = false;
+ return returnData;
+ }
+ if (data.days_to_play) {
+ returnData.text =
+ 'You have not been playing on the server long enough to join as drone.';
+ returnData.state = false;
+ return returnData;
+ }
+ if (data.respawn_time) {
+ returnData.text =
+ 'You must wait ' +
+ data.respawn_time +
+ ' before being able to respawn again.';
+ returnData.state = false;
+ return returnData;
+ }
+ if (!Object.keys(data.fabricators).length) {
+ returnData.text = 'There are no available drone spawn points, sorry.';
+ returnData.state = false;
+ return returnData;
+ }
+ return returnData;
+}
+
+export function describeSpecialData(
+ role: string,
+ banned: boolean,
+ slots: number,
+ spawn_exists: boolean,
+ respawn: string,
+ action: string,
+): describeReturnData {
+ const text = 'Span as ' + role.toLowerCase() + '.';
+ const returnData = { text: text, state: true, name: role, action: action };
+ if (banned) {
+ returnData.text =
+ 'You are banned from playing as ' + role.toLowerCase() + '.';
+ returnData.state = false;
+ return returnData;
+ }
+ if (!spawn_exists) {
+ returnData.text =
+ 'There are no available ' + role.toLowerCase() + ' spawn points, sorry.';
+ returnData.state = false;
+ return returnData;
+ }
+ if (respawn) {
+ returnData.text =
+ 'You must wait ' + respawn + ' before being able to respawn again.';
+ returnData.state = false;
+ return returnData;
+ }
+ if (slots < 0) {
+ returnData.text = 'There are no available ghost role slots, sorry.';
+ returnData.state = false;
+ return returnData;
+ }
+ return returnData;
+}
diff --git a/tgui/packages/tgui/interfaces/GhostSpawn/index.tsx b/tgui/packages/tgui/interfaces/GhostSpawn/index.tsx
new file mode 100644
index 0000000000..cc0142c10a
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/GhostSpawn/index.tsx
@@ -0,0 +1,61 @@
+import { useBackend } from 'tgui/backend';
+import { Window } from 'tgui/layouts';
+import { Stack, Tabs } from 'tgui-core/components';
+
+import { GhostJoin } from './GhostJoin/GhostJoin';
+import { GhostPod } from './GhostPod/GhostPod';
+import type { Data } from './types';
+import { Vorespawn } from './Vorespawn/Vorespawn';
+
+export const GhostSpawn = (props) => {
+ const { act, data } = useBackend();
+ const {
+ all_ghost_pods,
+ all_vore_spawns,
+ all_ghost_join_options,
+ user_z,
+ active_tab,
+ } = data;
+
+ const tabs: (React.JSX.Element | undefined)[] = [];
+
+ tabs[0] = all_ghost_pods && (
+
+ );
+ tabs[1] = all_ghost_join_options && (
+
+ );
+ tabs[2] = all_vore_spawns && ;
+
+ return (
+
+
+
+
+
+ act('set_tab', { val: 0 })}
+ >
+ Ghost Pods
+
+ act('set_tab', { val: 1 })}
+ >
+ Ghost Join
+
+ act('set_tab', { val: 2 })}
+ >
+ Vorespawn
+
+
+
+ {tabs[active_tab] || 'Invalid Tab Data'}
+
+
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/GhostSpawn/types.ts b/tgui/packages/tgui/interfaces/GhostSpawn/types.ts
new file mode 100644
index 0000000000..6e3d0383ef
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/GhostSpawn/types.ts
@@ -0,0 +1,67 @@
+import type { BooleanLike } from 'tgui-core/react';
+
+export type Data = {
+ user_z: number;
+ active_tab: number;
+ all_ghost_pods?: PodData[];
+ all_vore_spawns?: Record;
+ all_ghost_join_options?: GhostJoinData;
+};
+
+export type PodData = {
+ pod_type: string;
+ pod_name: string;
+ z_level: number;
+ ref: string;
+ remains_active: BooleanLike;
+};
+
+export type VoreSpawnData = {
+ player: string;
+ soulcatcher: BooleanLike;
+ soulcatcher_vore: BooleanLike;
+ vorespawn: BooleanLike;
+};
+
+export type DropdownEntry = {
+ displayText: string;
+ value: string;
+};
+
+export type GhostJoinData = {
+ mouse_data: MouseData;
+ drone_data: DroneData;
+ vr_data: VRData;
+ cyborg_banned: BooleanLike;
+ ghost_banned: BooleanLike;
+ may_respawn: BooleanLike;
+ special_role_respawn: string;
+ existing_ghost_spawnpoints: BooleanLike;
+ remaining_ghost_roles: number;
+};
+
+export type MouseData = {
+ disabled: BooleanLike;
+ bad_turf: BooleanLike;
+ respawn_time: string;
+ found_vents: BooleanLike;
+};
+
+export type DroneData = {
+ disabled: BooleanLike;
+ days_to_play: number;
+ respawn_time: string;
+ fabricators: Record;
+};
+
+export type VRData = {
+ record_found: BooleanLike;
+ vr_landmarks: Record;
+};
+
+export type describeReturnData = {
+ text: string;
+ state: boolean;
+ name?: string;
+ action?: string;
+};
diff --git a/tgui/packages/tgui/interfaces/PublicLibraryWiki/WikiCommon/WikiSearchList.tsx b/tgui/packages/tgui/interfaces/PublicLibraryWiki/WikiCommon/WikiSearchList.tsx
index 98c3973c1f..b6796dc73f 100644
--- a/tgui/packages/tgui/interfaces/PublicLibraryWiki/WikiCommon/WikiSearchList.tsx
+++ b/tgui/packages/tgui/interfaces/PublicLibraryWiki/WikiCommon/WikiSearchList.tsx
@@ -1,6 +1,6 @@
import type { Dispatch, SetStateAction } from 'react';
import { useBackend } from 'tgui/backend';
-import { Button, Divider, Input, Section, Stack } from 'tgui-core/components';
+import { Button, Input, Section, Stack } from 'tgui-core/components';
import { capitalize } from 'tgui-core/string';
export const WikiSearchList = (props: {
@@ -41,7 +41,7 @@ export const WikiSearchList = (props: {
onChange={(value: string) => onSearchText(value)}
/>
-
+
);
};
diff --git a/tgui/packages/tgui/interfaces/RobotChoose/index.tsx b/tgui/packages/tgui/interfaces/RobotChoose/index.tsx
index 2ed9c00dc9..9934ee835c 100644
--- a/tgui/packages/tgui/interfaces/RobotChoose/index.tsx
+++ b/tgui/packages/tgui/interfaces/RobotChoose/index.tsx
@@ -27,23 +27,29 @@ export const RobotChoose = (props) => {
-
-
-
+
+
+
+
+
+
+
+
+
diff --git a/vorestation.dme b/vorestation.dme
index db6b635325..00cc888f77 100644
--- a/vorestation.dme
+++ b/vorestation.dme
@@ -507,6 +507,8 @@
#include "code\datums\forensics_crime.dm"
#include "code\datums\ghost_query.dm"
#include "code\datums\ghost_query_vr.dm"
+#include "code\datums\ghost_spawn.dm"
+#include "code\datums\ghost_spawn_options.dm"
#include "code\datums\hierarchy.dm"
#include "code\datums\json_savefile.dm"
#include "code\datums\mind.dm"
@@ -2098,6 +2100,7 @@
#include "code\modules\admin\verbs\smite.dm"
#include "code\modules\admin\verbs\special_verbs.dm"
#include "code\modules\admin\verbs\striketeam.dm"
+#include "code\modules\admin\verbs\tgui_verbs.dm"
#include "code\modules\admin\verbs\tripAI.dm"
#include "code\modules\admin\verbs\SDQL2\SDQL_2.dm"
#include "code\modules\admin\verbs\SDQL2\SDQL_2_parser.dm"
@@ -4687,7 +4690,6 @@
#include "code\modules\vore\eating\soulcatcher.dm"
#include "code\modules\vore\eating\soulcatcher_import.dm"
#include "code\modules\vore\eating\soulcatcher_mob.dm"
-#include "code\modules\vore\eating\soulcatcher_observer.dm"
#include "code\modules\vore\eating\special_bellies.dm"
#include "code\modules\vore\eating\stumblevore_vr.dm"
#include "code\modules\vore\eating\transforming_vr.dm"
@@ -4956,7 +4958,6 @@
#include "modular_chomp\code\game\turfs\simulated\outdoors\valley.dm"
#include "modular_chomp\code\modules\admin\functions\modify_traits.dm"
#include "modular_chomp\code\modules\admin\verbs\debug.dm"
-#include "modular_chomp\code\modules\admin\verbs\tgui_verbs.dm"
#include "modular_chomp\code\modules\artifice\deadringer.dm"
#include "modular_chomp\code\modules\awaymissions\tank.dm"
#include "modular_chomp\code\modules\casino\casino_book.dm"