simple ghost pod find (#17909)

* simple ghost spawn rewrite

* neutral

* .

* don't close

* migrate vore spawn

* use a searchable list

* move some more verbs

* allow special role spawns

* add key

* .

* fix that part

* announce for logging

* filter those

* typ o

* .

* .

* Update SelectionList.tsx

* .

* that

* .

---------

Co-authored-by: Cameron Lennox <killer65311@gmail.com>
This commit is contained in:
Kashargul
2025-07-17 01:00:51 +02:00
committed by GitHub
parent f94a66cb97
commit 426b24d37e
34 changed files with 1543 additions and 430 deletions
+5
View File
@@ -92,3 +92,8 @@ _ADMIN_VERB(verb_path_name, verb_permissions, verb_name, verb_desc, verb_categor
// Visibility flags
#define ADMIN_VERB_VISIBLITY_FLAG_MAPPING_DEBUG "Map-Debug"
// Tgui input list options
#define TGUI_VIEW_ATTACK_LOGS "Check Player Attack Logs"
#define TGUI_VIEW_DIALOG_LOGS "Check Player Dialogue Logs"
#define TGUI_RESIZE "Resize player"
+2
View File
@@ -1 +1,3 @@
#define OBSERVER_EVENT_DESTROY "OnDestroy"
#define MOUSE_VENT_NETWORK_LENGTH 20
+1
View File
@@ -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)
+1 -1
View File
@@ -29,7 +29,7 @@
sorter.cmp = cmp; \
sorter.associative = associative;
//This is a global instance to allow much of this code to be reused. The interfaces are kept separately
//This is a global instance to allow much of this code to be reused. The interfaces are kept separately
GLOBAL_DATUM_INIT(sortInstance, /datum/sort_instance, new())
/datum/sort_instance
//The array being sorted.
+24 -1
View File
@@ -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)
..()
+224
View File
@@ -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
+329
View File
@@ -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
+2 -2
View File
@@ -102,12 +102,12 @@
var/list/cameras = list()
/mob/living/silicon/ai/proc/trackable_mobs()
if(usr.stat == 2)
if(src.stat == 2)
return list()
var/datum/trackable/TB = new()
for(var/mob/living/M in GLOB.mob_list)
if(M == usr)
if(M == src)
continue
if(M.tracking_status() != TRACKING_POSSIBLE)
continue
@@ -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
@@ -96,3 +96,6 @@
name = JOB_TALON_PILOT
/obj/effect/landmark/start/talonminer
name = JOB_TALON_MINER
/obj/effect/landmark/start/ghost_roles
name = JOB_GHOSTROLES
+5
View File
@@ -3,7 +3,10 @@
set name = "Check Player Attack Logs"
set desc = "Check a player's attack logs."
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:<HR>")
dat += span_bold("Viewing attack logs of [M]") + " - (Played by ([key_name(M)]).<br>"
if(M.mind)
@@ -33,8 +36,10 @@
set category = "Admin.Logs"
set name = "Check Player Dialogue Logs"
set desc = "Check a player's dialogue logs."
show_cmd_admin_check_dialogue_logs(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:<HR>")
dat += span_bold("Viewing say and emote logs of [M]") + " - (Played by ([key_name(M)]).<br>"
if(M.mind)
+6 -3
View File
@@ -1,4 +1,7 @@
ADMIN_VERB_AND_CONTEXT_MENU(resize, (R_ADMIN|R_FUN|R_VAREDIT), "Resize", "Resizes any living mob without any restrictions on size.", "Fun.Event Kit", mob/living/L in GLOB.mob_list)
user.do_resize(L)
/client/proc/do_resize(var/mob/living/L)
var/size_multiplier = tgui_input_number(usr, "Input size multiplier.", "Resize", 1, round_value=FALSE)
if(!size_multiplier)
return //cancelled
@@ -8,11 +11,11 @@ ADMIN_VERB_AND_CONTEXT_MENU(resize, (R_ADMIN|R_FUN|R_VAREDIT), "Resize", "Resize
var/very_big = is_extreme_size(size_multiplier)
if(very_big && can_be_big) // made an extreme size in an area that allows it, don't assume adminbuse
to_chat(user, span_warning("[L] will lose this size upon moving into an area where this size is not allowed."))
to_chat(src, span_warning("[L] will lose this size upon moving into an area where this size is not allowed."))
else if(very_big) // made an extreme size in an area that doesn't allow it, assume adminbuse
to_chat(user, span_warning("[L] will retain this normally unallowed size outside this area."))
to_chat(src, span_warning("[L] will retain this normally unallowed size outside this area."))
L.resize(size_multiplier, animate = TRUE, uncapped = TRUE, ignore_prefs = TRUE)
log_and_message_admins("has changed [key_name(L)]'s size multiplier to [size_multiplier].", user)
log_and_message_admins("has changed [key_name(L)]'s size multiplier to [size_multiplier].", src)
feedback_add_details("admin_verb","RESIZE")
+29
View File
@@ -0,0 +1,29 @@
//Allows some list search inputs
/client/proc/tgui_admin_lists()
set name = "TGUI Admin Lists"
set desc = "Allows to have some procs with searchable lists."
set category = "Admin.Game"
if(!check_rights(R_ADMIN|R_EVENT))
return
var/list/modification_options = list(TGUI_VIEW_ATTACK_LOGS, TGUI_VIEW_DIALOG_LOGS, TGUI_RESIZE)
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
log_and_message_admins("[key_name(src)] has used TGUI Admin Lists with ([tgui_list_choice])].")
feedback_add_details("admin_verb","TGUIADL") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
switch(tgui_list_choice)
if(TGUI_VIEW_ATTACK_LOGS)
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(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(src, "Resizes any living mob without any restrictions on size.", "Resize", GLOB.mob_list)
if(L)
do_resize(L)
+1 -1
View File
@@ -1,4 +1,4 @@
//GLOBAL_LIST_EMPTY(event_viruses) // so that event viruses are kept around for admin logs, rather than being GCed
GLOBAL_LIST_EMPTY(event_viruses) // so that event viruses are kept around for admin logs, rather than being GCed
/datum/event/viral_infection
var/list/viruses = list()
+2 -56
View File
@@ -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
@@ -648,61 +649,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?","Are you sure you want to squeek?",list("Squeek!","Nope!"))
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"
@@ -916,7 +862,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)
+5 -77
View File
@@ -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"
@@ -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)
-66
View File
@@ -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
@@ -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...
@@ -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 (
<Stack fill vertical>
<Stack.Item>
<Section fill title="Spawn options">
<Stack vertical>
{!!vr_data.record_found && (
<Stack.Item>
<NoticeBox danger>
You seem to have previously joined this round. If you are
currently dead, you should not enter VR as this character.
</NoticeBox>
</Stack.Item>
)}
<Stack.Item grow>
<LabeledList>
<SpawnElement
title="Mouse"
disabled={!describedMouse.state || !may_respawn}
tooltip={describedMouse.text}
action="mouse_spawn"
/>
<LabeledList.Item label="Drone Spawn">
<Stack>
<Stack.Item basis="200px">
<Button.Confirm
fluid
disabled={
!describeDrone.state || !may_respawn || !selectedFab
}
color={
describeDrone.state && may_respawn ? 'green' : 'red'
}
tooltip={describeDrone.text}
tooltipPosition="top"
onClick={() =>
act('drone_spawn', { fabricator: selectedFab })
}
>
Become Drone?
</Button.Confirm>
</Stack.Item>
<Stack.Item>
<Dropdown
onSelected={(value) => setSelectedFab(value)}
options={playerDropdown}
selected={drone_data.fabricators[selectedFab]}
/>
</Stack.Item>
</Stack>
</LabeledList.Item>
<LabeledList.Item label="Enter VR">
<Stack>
<Stack.Item basis="200px">
<Button.Confirm
fluid
disabled={!selectedLandmark}
color={selectedLandmark ? 'green' : 'red'}
tooltip="Log into NanoTrasen's local virtual reality server."
tooltipPosition="top"
onClick={() =>
act('vr_spawn', { landmark: selectedLandmark })
}
>
Enter VR?
</Button.Confirm>
</Stack.Item>
<Stack.Item>
<Dropdown
onSelected={(value) => setSelectedLandmark(value)}
options={landmarkDropdown}
selected={vr_data.vr_landmarks[selectedLandmark]}
/>
</Stack.Item>
</Stack>
</LabeledList.Item>
</LabeledList>
</Stack.Item>
</Stack>
</Section>
</Stack.Item>
<Stack.Item grow>
<Section fill title={'Special roles (' + remaining_ghost_roles + ')'}>
<LabeledList>
{specialRoles.map((role) => (
<SpawnElement
key={role.name}
title={role.name || ''}
disabled={!role.state || !may_respawn}
tooltip={role.text}
action={role.action || ''}
/>
))}
</LabeledList>
</Section>
</Stack.Item>
</Stack>
);
};
@@ -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 (
<LabeledList.Item label={title + ' Spawn'}>
<Stack>
<Stack.Item basis="200px">
<Button.Confirm
fluid
disabled={disabled}
color={!disabled ? 'green' : 'red'}
tooltip={tooltip}
tooltipPosition="top"
onClick={() => act(action)}
>
{'Become ' + title + '?'}
</Button.Confirm>
</Stack.Item>
</Stack>
</LabeledList.Item>
);
};
@@ -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<string>('');
const [selectedPod, setSelectedPod] = useState<string>('');
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 (
<Stack fill>
<Stack.Item basis="30%">
<PodSelectionList
podTypes={podTypes}
selectedPod={selectedType}
onSelectedPod={setSelectedType}
/>
</Stack.Item>
<Stack.Item grow>
<SelectionList
allPods={selectedGhostPods}
userZ={user_z}
selectedType={selectedType}
selectedPod={selectedPod}
onSelectedPod={setSelectedPod}
/>
</Stack.Item>
</Stack>
);
};
@@ -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<React.SetStateAction<string>>;
}) => {
const { podTypes, selectedPod, onSelectedPod } = props;
const [searchText, setSearchText] = useState<string>('');
const searcher = createSearch(searchText, (element: string) => {
return element;
});
const filtered = podTypes?.filter(searcher);
return (
<Section title="Ghost Pod Types" fill>
<Stack vertical fill>
<Stack.Item>
<Input
fluid
value={searchText}
placeholder="Search for types..."
onChange={(value: string) => setSearchText(value)}
/>
</Stack.Item>
<Stack.Divider />
<Stack.Item grow>
<Section fill scrollable>
<Stack vertical fill>
{!!filtered &&
filtered.map((pod_type) => (
<Stack.Item key={pod_type}>
<Button
fluid
selected={selectedPod === pod_type}
onClick={() => onSelectedPod(pod_type)}
>
{pod_type}
</Button>
</Stack.Item>
))}
</Stack>
</Section>
</Stack.Item>
</Stack>
</Section>
);
};
@@ -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<React.SetStateAction<string>>;
}) => {
const { act } = useBackend();
const { allPods, userZ, selectedType, selectedPod, onSelectedPod } = props;
const [searchText, setSearchText] = useState<string>('');
const [usePlayerZ, setUsePlayerZ] = useState<boolean>(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 (
<Section
title="Active Pods"
fill
buttons={
<Stack>
<Stack.Item>
<Button.Checkbox
checked={usePlayerZ}
onClick={() => setUsePlayerZ(!usePlayerZ)}
>
Player Z ({userZ})
</Button.Checkbox>
</Stack.Item>
<Stack.Item>
<Button
disabled={!selectedPod}
onClick={() => act('select_pod', { selected_pod: selectedPod })}
>
Jump to Pod
</Button>
</Stack.Item>
</Stack>
}
>
<Stack vertical fill>
<Stack.Item>
<Input
fluid
value={searchText}
placeholder="Search for pods..."
onChange={(value: string) => setSearchText(value)}
/>
</Stack.Item>
<Stack.Divider />
<Stack.Item grow>
<Section fill scrollable>
<Stack vertical fill>
{!!filtered &&
filtered.map((our_pod) => (
<Stack.Item key={our_pod.ref}>
<Button
fluid
disabled={!our_pod.remains_active}
color={our_pod.remains_active ? undefined : 'red'}
selected={selectedPod === our_pod.ref}
onClick={() => onSelectedPod(our_pod.ref)}
>
{our_pod.pod_name}
</Button>
</Stack.Item>
))}
</Stack>
</Section>
</Stack.Item>
</Stack>
</Section>
);
};
@@ -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<string, VoreSpawnData>;
}) => {
const { act } = useBackend();
const { all_vore_spawns } = props;
const [selectedPlayer, setSelectedPlayer] = useState<string>('');
const [searchText, setSearchText] = useState<string>('');
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 (
<Stack vertical fill>
<Stack.Item>
<Box color="red">
{
"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."
}
</Box>
</Stack.Item>
<Stack.Item grow>
<Stack fill>
<Stack.Item basis="30%">
<Section fill title="Player Selection">
<Stack vertical fill>
<Stack.Item>
<Input
fluid
value={searchText}
placeholder="Search for players..."
onChange={(value: string) => setSearchText(value)}
/>
</Stack.Item>
<Stack.Divider />
<Stack.Item grow>
<Section fill scrollable>
<Stack vertical fill>
{filtered.map((player) => (
<Stack.Item key={player.value}>
<Button
fluid
selected={player.value === selectedPlayer}
onClick={() => setSelectedPlayer(player.value)}
>
{player.displayText}
</Button>
</Stack.Item>
))}
</Stack>
</Section>
</Stack.Item>
</Stack>
</Section>
</Stack.Item>
<Stack.Item grow>
<Section fill title="Spawn Options">
<Stack vertical>
<Stack.Item>
<Stack>
<Stack.Item basis="200px">
<Button.Confirm
fluid
color={allowSoulcatcher ? 'green' : 'red'}
disabled={!allowSoulcatcher}
onClick={() =>
act('soulcatcher_spawn', {
selected_player: selectedPlayer,
})
}
tooltip="Select a player with a working NIF + Soulcatcher NIFSoft to join into it."
>
Soulcatcher
</Button.Confirm>
</Stack.Item>
</Stack>
</Stack.Item>
<Stack.Item>
<Stack>
<Stack.Item basis="200px">
<Button.Confirm
fluid
color={allowSoulcatcherVore ? 'green' : 'red'}
disabled={!allowSoulcatcherVore}
onClick={() =>
act('soulcatcher_vore_spawn', {
selected_player: selectedPlayer,
})
}
tooltip="Select a player with enabled Vore Soulcatcher to join."
>
Soulcatcher (Vore)
</Button.Confirm>
</Stack.Item>
</Stack>
</Stack.Item>
<Stack.Item>
<Stack>
<Stack.Item basis="200px">
<Button.Confirm
fluid
color={allowBellySpawn ? 'green' : 'red'}
disabled={!allowBellySpawn}
onClick={() =>
act('bellyspawn', { selected_player: selectedPlayer })
}
>
Spawn In Belly
</Button.Confirm>
</Stack.Item>
</Stack>
</Stack.Item>
</Stack>
</Section>
</Stack.Item>
</Stack>
</Stack.Item>
</Stack>
);
};
@@ -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;
}
@@ -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<Data>();
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 && (
<GhostPod all_ghost_pods={all_ghost_pods} user_z={user_z} />
);
tabs[1] = all_ghost_join_options && (
<GhostJoin all_ghost_join_options={all_ghost_join_options} />
);
tabs[2] = all_vore_spawns && <Vorespawn all_vore_spawns={all_vore_spawns} />;
return (
<Window width={650} height={510} theme="abstract">
<Window.Content>
<Stack vertical fill>
<Stack.Item>
<Tabs>
<Tabs.Tab
selected={active_tab === 0}
onClick={() => act('set_tab', { val: 0 })}
>
Ghost Pods
</Tabs.Tab>
<Tabs.Tab
selected={active_tab === 1}
onClick={() => act('set_tab', { val: 1 })}
>
Ghost Join
</Tabs.Tab>
<Tabs.Tab
selected={active_tab === 2}
onClick={() => act('set_tab', { val: 2 })}
>
Vorespawn
</Tabs.Tab>
</Tabs>
</Stack.Item>
<Stack.Item grow>{tabs[active_tab] || 'Invalid Tab Data'}</Stack.Item>
</Stack>
</Window.Content>
</Window>
);
};
@@ -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<string, VoreSpawnData>;
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<string, string>;
};
export type VRData = {
record_found: BooleanLike;
vr_landmarks: Record<string, string>;
};
export type describeReturnData = {
text: string;
state: boolean;
name?: string;
action?: string;
};
@@ -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)}
/>
</Stack.Item>
<Divider />
<Stack.Divider />
<Stack.Item grow>
<Section fill scrollable>
<Stack vertical fill>
@@ -16,8 +16,6 @@ export const IconSection = (props: {
<Section
title="Sprite"
fill
scrollable
width="40%"
buttons={
<Button disabled={!sprite} onClick={() => act('confirm')}>
Confirm
@@ -20,28 +20,33 @@ export const ModuleSection = (props: {
const filtered = sortable?.filter(searcher);
return (
<Section title={title} fill scrollable width="30%">
<Stack.Item mb={'10px'}>
<Input
fluid
value={searchText}
placeholder="Search for modules..."
onChange={(value: string) => setSearchText(value)}
/>
</Stack.Item>
<Stack.Item>
<Stack vertical fill>
{!!filtered &&
filtered.map((filter) => (
<SelectorElement
key={filter}
option={filter}
action="pick_module"
selected={selected}
/>
))}
</Stack>
</Stack.Item>
<Section title={title} fill>
<Stack vertical fill>
<Stack.Item>
<Input
fluid
value={searchText}
placeholder="Search for modules..."
onChange={(value: string) => setSearchText(value)}
/>
</Stack.Item>
<Stack.Divider />
<Stack.Item grow>
<Section fill scrollable>
<Stack vertical fill>
{!!filtered &&
filtered.map((filter) => (
<SelectorElement
key={filter}
option={filter}
action="pick_module"
selected={selected}
/>
))}
</Stack>
</Section>
</Stack.Item>
</Stack>
</Section>
);
};
@@ -28,8 +28,6 @@ export const SpriteSection = (props: {
<Section
title={title}
fill
scrollable
width="30%"
buttons={
<Stack g={0.2}>
<Stack.Item>
@@ -59,28 +57,33 @@ export const SpriteSection = (props: {
</Stack>
}
>
<Stack.Item mb={'10px'}>
<Input
fluid
value={searchText}
placeholder="Search for sprites..."
onChange={(value: string) => setSearchText(value)}
/>
</Stack.Item>
<Stack.Item>
<Stack vertical>
{!!filtered &&
filtered.map((filter) => (
<SelectorElement
key={filter.sprite}
option={filter.sprite}
action="pick_icon"
selected={selected}
belly={filter.belly}
/>
))}
</Stack>
</Stack.Item>
<Stack vertical fill>
<Stack.Item>
<Input
fluid
value={searchText}
placeholder="Search for sprites..."
onChange={(value: string) => setSearchText(value)}
/>
</Stack.Item>
<Stack.Divider />
<Stack.Item grow>
<Section fill scrollable>
<Stack vertical fill>
{!!filtered &&
filtered.map((filter) => (
<SelectorElement
key={filter.sprite}
option={filter.sprite}
action="pick_icon"
selected={selected}
belly={filter.belly}
/>
))}
</Stack>
</Section>
</Stack.Item>
</Stack>
</Section>
);
};
@@ -27,23 +27,29 @@ export const RobotChoose = (props) => {
<Window width={800} height={605} theme={theme || 'ntos'}>
<Window.Content>
<Stack fill>
<ModuleSection
title="Modules"
sortable={possible_modules}
selected={selected_module}
/>
<SpriteSection
title="Sprites"
sortable={possible_sprites}
selected={sprite_datum}
/>
<IconSection
currentName={currentName}
mindName={mind_name}
isDefaultName={isDefaultName}
sprite={sprite_datum_class}
size={sprite_datum_size}
/>
<Stack.Item basis="30%">
<ModuleSection
title="Modules"
sortable={possible_modules}
selected={selected_module}
/>
</Stack.Item>
<Stack.Item basis="30%">
<SpriteSection
title="Sprites"
sortable={possible_sprites}
selected={sprite_datum}
/>
</Stack.Item>
<Stack.Item grow>
<IconSection
currentName={currentName}
mindName={mind_name}
isDefaultName={isDefaultName}
sprite={sprite_datum_class}
size={sprite_datum_size}
/>
</Stack.Item>
</Stack>
</Window.Content>
</Window>
+3 -1
View File
@@ -480,6 +480,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"
@@ -2001,6 +2003,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"
@@ -4450,7 +4453,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"