diff --git a/code/_onclick/hud/screen_objects.dm b/code/_onclick/hud/screen_objects.dm
index a517b78e54..de8604e5bc 100644
--- a/code/_onclick/hud/screen_objects.dm
+++ b/code/_onclick/hud/screen_objects.dm
@@ -706,51 +706,6 @@
/obj/screen/setup_preview/bg/Click(params)
pref?.bgstate = next_in_list(pref.bgstate, pref.bgstate_options)
pref?.update_preview_icon()
-
-/obj/screen/splash
- screen_loc = "1,1"
- layer = LAYER_HUD_ABOVE
- plane = PLANE_PLAYER_HUD_ABOVE
- var/client/holder
-
-INITIALIZE_IMMEDIATE(/obj/screen/splash)
-/obj/screen/splash/Initialize(mapload, visible)
- . = ..()
-
- if(!isclient(loc))
- return INITIALIZE_HINT_QDEL
-
- holder = loc
-
- if(!visible)
- alpha = 0
-
- if(!lobby_image)
- return INITIALIZE_HINT_QDEL
-
- icon = lobby_image.icon
- icon_state = lobby_image.icon_state
-
- holder.screen += src
-
-/obj/screen/splash/proc/Fade(out, qdel_after = TRUE)
- if(QDELETED(src))
- return
- if(out)
- animate(src, alpha = 0, time = 30)
- else
- alpha = 0
- animate(src, alpha = 255, time = 30)
- if(qdel_after)
- QDEL_IN(src, 30)
-
-/obj/screen/splash/Destroy()
- if(holder)
- holder.screen -= src
- holder = null
- return ..()
-
-
/**
* This object holds all the on-screen elements of the mapping unit.
* It has a decorative frame and onscreen buttons. The map itself is drawn
diff --git a/code/controllers/subsystems/tgui.dm b/code/controllers/subsystems/tgui.dm
index a03e3452c3..1563be683d 100644
--- a/code/controllers/subsystems/tgui.dm
+++ b/code/controllers/subsystems/tgui.dm
@@ -279,7 +279,7 @@ SUBSYSTEM_DEF(tgui)
if(length(user?.tgui_open_uis) == 0)
return count
for(var/datum/tgui/ui in user.tgui_open_uis)
- if(isnull(src_object) || ui.src_object == src_object)
+ if((isnull(src_object) || ui.src_object == src_object) && ui.closeable)
ui.close(logout = logout)
count++
return count
diff --git a/code/controllers/subsystems/ticker.dm b/code/controllers/subsystems/ticker.dm
index a3a93d8fff..f065fdf800 100644
--- a/code/controllers/subsystems/ticker.dm
+++ b/code/controllers/subsystems/ticker.dm
@@ -421,8 +421,6 @@ var/global/datum/controller/subsystem/ticker/ticker
if(new_char)
qdel(player)
if(new_char.client)
- var/obj/screen/splash/S = new(new_char.client, TRUE)
- S.Fade(TRUE)
new_char.client.init_verbs()
// If they're a carbon, they can get manifested
diff --git a/code/datums/managed_browsers/feedback_form.dm b/code/datums/managed_browsers/feedback_form.dm
index 3717e3e16a..f04d162b0a 100644
--- a/code/datums/managed_browsers/feedback_form.dm
+++ b/code/datums/managed_browsers/feedback_form.dm
@@ -141,7 +141,4 @@ GENERAL_PROTECT_DATUM(/datum/managed_browser/feedback_form)
return
my_client.mob << browse(null, "window=[browser_id]") // Closes the window.
- if(isnewplayer(my_client.mob))
- var/mob/new_player/NP = my_client.mob
- NP.new_player_panel_proc() // So the feedback button goes away, if the user gets put on cooldown.
qdel(src)
diff --git a/code/game/jobs/job_controller.dm b/code/game/jobs/job_controller.dm
index 1b3fecf79b..b11cf840f3 100644
--- a/code/game/jobs/job_controller.dm
+++ b/code/game/jobs/job_controller.dm
@@ -353,7 +353,6 @@ var/global/datum/controller/occupations/job_master
for(var/mob/new_player/player in unassigned)
if(player.client.prefs.alternate_option == RETURN_TO_LOBBY)
player.ready = 0
- player.new_player_panel_proc()
unassigned -= player
return 1
diff --git a/code/modules/asset_cache/assets/lobby.dm b/code/modules/asset_cache/assets/lobby.dm
new file mode 100644
index 0000000000..46d61ef305
--- /dev/null
+++ b/code/modules/asset_cache/assets/lobby.dm
@@ -0,0 +1,10 @@
+/datum/asset/simple/lobby_files
+ keep_local_name = TRUE
+ assets = list(
+ "lobby_loading.gif" = 'html/lobby/loading.gif',
+ "load.ogg" = 'sound/lobby/lobby_load.ogg',
+ )
+
+/datum/asset/simple/lobby_files/register()
+ assets["lobby_bg.gif"] = icon(using_map.lobby_icon, pick(using_map.lobby_screens))
+ . = ..()
diff --git a/code/modules/clothing/gloves/miscellaneous_vr.dm b/code/modules/clothing/gloves/miscellaneous_vr.dm
index de68a0acc9..f774cd0cac 100644
--- a/code/modules/clothing/gloves/miscellaneous_vr.dm
+++ b/code/modules/clothing/gloves/miscellaneous_vr.dm
@@ -1,4 +1,5 @@
/obj/item/clothing/gloves/color
+ name = "gloves"
desc = "A pair of gloves, they don't look special in any way."
item_state_slots = list(slot_r_hand_str = "white", slot_l_hand_str = "white")
icon_state = "latex"
diff --git a/code/modules/mob/login.dm b/code/modules/mob/login.dm
index 2b8f81d7d9..70131546e0 100644
--- a/code/modules/mob/login.dm
+++ b/code/modules/mob/login.dm
@@ -25,6 +25,7 @@
log_adminwarn("Notice: [key_name(src)] has the same [matches] as [key_name(M)] (no longer logged in).")
/mob/Login()
+ persistent_ckey = client.ckey
player_list |= src
update_Login_details()
diff --git a/code/modules/mob/mob_defines.dm b/code/modules/mob/mob_defines.dm
index e520bacdfe..13913d4977 100644
--- a/code/modules/mob/mob_defines.dm
+++ b/code/modules/mob/mob_defines.dm
@@ -245,3 +245,6 @@
var/custom_footstep = FOOTSTEP_MOB_SHOE
VAR_PRIVATE/is_motion_tracking = FALSE // Prevent multiple unsubs and resubs, also used to check if the vis layer is enabled, use has_motiontracking() to get externally.
VAR_PRIVATE/wants_to_see_motion_echos = TRUE
+
+ /// a ckey that persists client logout / ghosting, replaced when a client inhabits the mob
+ var/persistent_ckey
diff --git a/code/modules/mob/new_player/lobby_browser.dm b/code/modules/mob/new_player/lobby_browser.dm
new file mode 100644
index 0000000000..b6222cc2a1
--- /dev/null
+++ b/code/modules/mob/new_player/lobby_browser.dm
@@ -0,0 +1,157 @@
+/mob/new_player/proc/initialize_lobby_screen()
+ if(!client)
+ return
+
+ var/datum/tgui/ui = SStgui.get_open_ui(src, src)
+ if(ui)
+ ui.close()
+
+ winset(src, "lobby_browser", "is-disabled=false;is-visible=true")
+ // winset(src, "mapwindow.status_bar", "is-visible=false")
+ lobby_window = new(client, "lobby_browser")
+ lobby_window.initialize(
+ assets = list(
+ get_asset_datum(/datum/asset/simple/tgui)
+ )
+ )
+
+ tgui_interact(src)
+
+/mob/new_player/tgui_interact(mob/user, datum/tgui/ui, datum/tgui/parent_ui, custom_state)
+ . = ..()
+
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "LobbyMenu", window = lobby_window)
+ ui.closeable = FALSE
+ ui.open(preinitialized = TRUE)
+
+/mob/new_player/tgui_state(mob/user)
+ return GLOB.tgui_always_state
+
+/mob/new_player/ui_assets(mob/user)
+ . = ..()
+ . += get_asset_datum(/datum/asset/simple/lobby_files)
+
+/mob/new_player/tgui_data(mob/user, datum/tgui/ui, datum/tgui_state/state)
+ var/list/data = ..()
+
+ data["map"] = using_map.full_name
+ data["station_time"] = stationtime2text()
+ data["display_loading"] = SSticker.current_state == GAME_STATE_INIT
+ data["round_start"] = !SSticker.mode || SSticker.current_state <= GAME_STATE_PREGAME
+ data["round_time"] = roundduration2text()
+ data["ready"] = ready
+ data["new_news"] = client?.check_for_new_server_news()
+ data["can_submit_feedback"] = SSsqlite.can_submit_feedback(client)
+ data["show_station_news"] = GLOB.news_data.station_newspaper
+ data["new_station_news"] = client.prefs.lastlorenews != GLOB.news_data.newsindex
+ data["new_changelog"] = read_preference(/datum/preference/text/lastchangelog) == GLOB.changelog_hash
+
+ return data
+
+/mob/new_player/tgui_static_data(mob/user)
+ var/list/data = ..()
+
+ data["bg"] = 'icons/misc/loading.dmi'
+ data["bg_state"] = "loading"
+
+ return data
+
+/mob/new_player/tgui_act(action, list/params, datum/tgui/ui, datum/tgui_state/state)
+ . = ..()
+ if(.)
+ return
+
+ switch(action)
+ if("character_setup")
+ client.prefs.ShowChoices(src)
+ return TRUE
+ if("ready")
+ if(!SSticker || SSticker.current_state <= GAME_STATE_PREGAME)
+ ready = !ready
+ else
+ ready = 0
+ return TRUE
+ if("manifest")
+ ViewManifest()
+ return TRUE
+ if("late_join")
+ if(!ticker || ticker.current_state != GAME_STATE_PLAYING)
+ to_chat(usr, span_red("The round is either not ready, or has already finished..."))
+ return TRUE
+
+ 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!"))
+ else if(time_till_respawn) // Nonzero time to respawn
+ to_chat(usr, span_warning("You can't respawn yet! You need to wait another [round(time_till_respawn/10/60, 0.1)] minutes."))
+ return TRUE
+ LateChoices()
+ return TRUE
+ if("observe")
+ if(!SSticker || SSticker.current_state == GAME_STATE_INIT)
+ to_chat(src, span_warning("The game is still setting up, please try again later."))
+ return TRUE
+ if(tgui_alert(src,"Are you sure you wish to observe? If you do, make sure to not use any knowledge gained from observing if you decide to join later.","Observe Round?",list("Yes","No")) == "Yes")
+ if(!client)
+ return TRUE
+
+ //Make a new mannequin quickly, and allow the observer to take the appearance
+ var/mob/living/carbon/human/dummy/mannequin = get_mannequin(client.ckey)
+ client.prefs.dress_preview_mob(mannequin)
+ var/mob/observer/dead/observer = new(mannequin)
+ observer.moveToNullspace() //Let's not stay in our doomed mannequin
+
+ spawning = 1
+ if(client.media)
+ client.media.stop_music() // MAD JAMS cant last forever yo
+
+ observer.started_as_observer = 1
+ close_spawn_windows()
+ var/obj/O = locate("landmark*Observer-Start")
+ if(istype(O))
+ to_chat(src, span_notice("Now teleporting."))
+ observer.forceMove(O.loc)
+ else
+ to_chat(src, span_danger("Could not locate an observer spawn point. Use the Teleport verb to jump to the station map."))
+
+ announce_ghost_joinleave(src)
+
+ if(client.prefs.read_preference(/datum/preference/toggle/human/name_is_always_random))
+ client.prefs.real_name = random_name(client.prefs.identifying_gender)
+ observer.real_name = client.prefs.real_name
+ observer.name = observer.real_name
+ if(!client.holder && !CONFIG_GET(flag/antag_hud_allowed)) // For new ghosts we remove the verb from even showing up if it's not allowed.
+ remove_verb(observer, /mob/observer/dead/verb/toggle_antagHUD) // Poor guys, don't know what they are missing!
+ observer.key = key
+ observer.set_respawn_timer(time_till_respawn()) // Will keep their existing time if any, or return 0 and pass 0 into set_respawn_timer which will use the defaults
+ observer.client.init_verbs()
+ qdel(src)
+
+ return TRUE
+ if("shownews")
+ handle_server_news()
+ return TRUE
+ if("give_feedback")
+ if(!SSsqlite.can_submit_feedback(GLOB.directory[persistent_ckey]))
+ return
+
+ if(client.feedback_form)
+ client.feedback_form.display() // In case they closed the form early.
+ else
+ client.feedback_form = new(client)
+ return TRUE
+ if("open_station_news")
+ show_latest_news(GLOB.news_data.station_newspaper)
+ return TRUE
+ if("open_changelog")
+ write_preference_directly(/datum/preference/text/lastchangelog, GLOB.changelog_hash)
+ client.changes()
+ return TRUE
+ if("keyboard")
+ if(!SSsounds.subsystem_initialized)
+ return
+
+ playsound_local(ui.user, get_sfx("keyboard"), vol = 20)
+ return TRUE
diff --git a/code/modules/mob/new_player/login.dm b/code/modules/mob/new_player/login.dm
index eb7b78d616..c4befc15e8 100644
--- a/code/modules/mob/new_player/login.dm
+++ b/code/modules/mob/new_player/login.dm
@@ -1,31 +1,3 @@
-///var/atom/movable/lobby_image = new /atom/movable{icon = 'icons/misc/title.dmi'; icon_state = lobby_image_state; screen_loc = "1,1"; name = "Polaris"}
-
-var/obj/effect/lobby_image = new /obj/effect/lobby_image
-
-/obj/effect/lobby_image
- name = "VORE Station"
- desc = "How are you reading this?"
- screen_loc = "1,1"
- icon = 'icons/misc/loading.dmi'
- icon_state = "loading"
-
-/obj/effect/lobby_image/Initialize(mapload)
- icon = using_map.lobby_icon
- var/known_icon_states = cached_icon_states(icon)
- for(var/lobby_screen in using_map.lobby_screens)
- if(!(lobby_screen in known_icon_states))
- error("Lobby screen '[lobby_screen]' did not exist in the icon set [icon].")
- using_map.lobby_screens -= lobby_screen
-
- if(using_map.lobby_screens.len)
- icon_state = pick(using_map.lobby_screens)
- else
- icon_state = known_icon_states[1]
- . = ..()
-
-/mob/new_player
- var/client/my_client // Need to keep track of this ourselves, since by the time Logout() is called the client has already been nulled
-
/mob/new_player/Login()
update_Login_details() //handles setting lastKnownIP and computer_id for use by the ban systems as well as checking for multikeying
if(GLOB.join_motd)
@@ -41,16 +13,19 @@ var/obj/effect/lobby_image = new /obj/effect/lobby_image
mind.active = 1
mind.current = src
+ if(client)
+ persistent_ckey = client.ckey
+
loc = null
- client.screen += lobby_image
- my_client = client
sight |= SEE_TURFS
+
+ initialize_lobby_screen()
+
player_list |= src
created_for = ckey
if(!QDELETED(src))
- new_player_panel()
addtimer(CALLBACK(src, PROC_REF(do_after_login)), 4 SECONDS, TIMER_DELETE_ME)
/mob/new_player/proc/do_after_login()
diff --git a/code/modules/mob/new_player/logout.dm b/code/modules/mob/new_player/logout.dm
index bbffe5e9ba..29a3abddad 100644
--- a/code/modules/mob/new_player/logout.dm
+++ b/code/modules/mob/new_player/logout.dm
@@ -1,10 +1,11 @@
/mob/new_player/Logout()
ready = 0
- // see login.dm
- if(my_client)
- my_client.screen -= lobby_image
- my_client = null
+ QDEL_NULL(lobby_window)
+
+ var/client/exiting_client = GLOB.directory[persistent_ckey]
+ if(exiting_client)
+ winset(exiting_client, "lobby_browser", "is-disabled=true;is-visible=false")
..()
diff --git a/code/modules/mob/new_player/new_player.dm b/code/modules/mob/new_player/new_player.dm
index e44813c728..b35e83a00d 100644
--- a/code/modules/mob/new_player/new_player.dm
+++ b/code/modules/mob/new_player/new_player.dm
@@ -5,9 +5,8 @@
var/spawning = 0 //Referenced when you want to delete the new_player later on in the code.
var/totalPlayers = 0 //Player counts for the Lobby tab
var/totalPlayersReady = 0
- var/show_hidden_jobs = 0 //Show jobs that are set to "Never" in preferences
var/has_respawned = FALSE //Determines if we're using RESPAWN_MESSAGE
- var/datum/browser/panel
+ var/datum/tgui_window/lobby_window = null
var/datum/tgui_module/crew_manifest/new_player/manifest_dialog = null
var/datum/tgui_module/late_choices/late_choices_dialog = null
universal_speak = 1
@@ -27,104 +26,12 @@
add_verb(src, /mob/proc/insidePanel)
/mob/new_player/Destroy()
- if(panel)
- QDEL_NULL(panel)
if(manifest_dialog)
QDEL_NULL(manifest_dialog)
if(late_choices_dialog)
QDEL_NULL(late_choices_dialog)
. = ..()
-/mob/new_player/verb/new_player_panel()
- set src = usr
- new_player_panel_proc()
-
-
-/mob/new_player/proc/new_player_panel_proc()
- var/output = "
"
-
- output += span_bold("Map:") + " [using_map.full_name]
"
- output += span_bold("Station Time:") + " [stationtime2text()]
"
-
- if(!ticker || ticker.current_state <= GAME_STATE_PREGAME)
- output += span_bold("Server Initializing!")
- else
- output += span_bold("Round Duration:") + " [roundduration2text()]
"
- output += "
"
-
- output += "
Character Setup
"
-
- if(!ticker || ticker.current_state <= GAME_STATE_PREGAME)
- if(ready)
- output += "
\[ " + span_linkOn(span_bold("Ready")) + " | Not Ready \]
"
- else
- output += "
\[ Ready | " + span_linkOn(span_bold("Not Ready")) + " \]
"
- output += "
Join Game!
"
-
- else
- output += "
View the Crew Manifest
"
- output += "
Join Game!
"
-
- output += "
Observe
"
-
- /*
- //nobody uses this feature
- if(!IsGuestKey(src.key))
- establish_db_connection()
-
- if(SSdbcore.IsConnected())
- var/isadmin = 0
- if(src.client && src.client.holder)
- isadmin = 1
- var/datum/db_query/query = SSdbcore.NewQuery("SELECT id FROM erro_poll_question WHERE [(isadmin ? "" : "adminonly = false AND")] Now() BETWEEN starttime AND endtime AND id NOT IN (SELECT pollid FROM erro_poll_vote WHERE ckey = \"[ckey]\") AND id NOT IN (SELECT pollid FROM erro_poll_textreply WHERE ckey = \"[ckey]\")")
- query.Execute()
- var/newpoll = 0
- while(query.NextRow())
- newpoll = 1
- break
- qdel(query)
-
- if(newpoll)
- output += "
Show Player Polls
(NEW!)
"
- else
- output += "
Show Player Polls
No Changes
"
- */
-
- if(client?.check_for_new_server_news())
- output += "
Show Server News
(NEW!)
"
- else
- output += "
Show Server News
No Changes
"
-
- if(SSsqlite.can_submit_feedback(client))
- output += "
[href(src, list("give_feedback" = 1), "Give Feedback")]
"
-
- if(GLOB.news_data.station_newspaper)
- if(client.prefs.lastlorenews == GLOB.news_data.newsindex)
- output += "
Show [using_map.station_name] News
No Changes
"
- else
- output += "
Show [using_map.station_name] News
(NEW!)
"
-
- if(read_preference(/datum/preference/text/lastchangelog) == GLOB.changelog_hash)
- output += "
Show Changelog
No Changes
"
- else
- output += "
Show Changelog
(NEW!)
"
-
- output += "
"
-
- if (client.prefs?.lastlorenews == GLOB.news_data.newsindex)
- client.seen_news = 1
-
- if(GLOB.news_data.station_newspaper && !client.seen_news && client.prefs?.read_preference(/datum/preference/toggle/show_lore_news))
- show_latest_news(GLOB.news_data.station_newspaper)
- client.prefs.lastlorenews = GLOB.news_data.newsindex
- SScharacter_setup.queue_preferences_save(client.prefs)
-
- panel = new(src, "Welcome","Welcome", 210, 500, src)
- panel.set_window_options("can_close=0")
- panel.set_content(output)
- panel.open()
- return
-
/mob/new_player/get_status_tab_items()
. = ..()
. += ""
@@ -162,78 +69,6 @@
/mob/new_player/Topic(href, href_list[])
if(!client) return 0
- if(href_list["show_preferences"])
- client.prefs.ShowChoices(src)
- return 1
-
- if(href_list["ready"])
- if(!ticker || ticker.current_state <= GAME_STATE_PREGAME) // Make sure we don't ready up after the round has started
- ready = text2num(href_list["ready"])
- else
- ready = 0
-
- if(href_list["refresh"])
- panel.close()
- new_player_panel_proc()
-
- if(href_list["observe"])
- if(!SSticker || SSticker.current_state == GAME_STATE_INIT)
- to_chat(src, span_warning("The game is still setting up, please try again later."))
- return 0
- if(tgui_alert(src,"Are you sure you wish to observe? If you do, make sure to not use any knowledge gained from observing if you decide to join later.","Observe Round?",list("Yes","No")) == "Yes")
- if(!client) return 1
-
- //Make a new mannequin quickly, and allow the observer to take the appearance
- var/mob/living/carbon/human/dummy/mannequin = get_mannequin(client.ckey)
- client.prefs.dress_preview_mob(mannequin)
- var/mob/observer/dead/observer = new(mannequin)
- observer.moveToNullspace() //Let's not stay in our doomed mannequin
-
- spawning = 1
- if(client.media)
- client.media.stop_music() // MAD JAMS cant last forever yo
-
- observer.started_as_observer = 1
- close_spawn_windows()
- var/obj/O = locate("landmark*Observer-Start")
- if(istype(O))
- to_chat(src, span_notice("Now teleporting."))
- observer.forceMove(O.loc)
- else
- to_chat(src, span_danger("Could not locate an observer spawn point. Use the Teleport verb to jump to the station map."))
-
- announce_ghost_joinleave(src)
-
- if(client.prefs.read_preference(/datum/preference/toggle/human/name_is_always_random))
- client.prefs.real_name = random_name(client.prefs.identifying_gender)
- observer.real_name = client.prefs.real_name
- observer.name = observer.real_name
- if(!client.holder && !CONFIG_GET(flag/antag_hud_allowed)) // For new ghosts we remove the verb from even showing up if it's not allowed.
- remove_verb(observer, /mob/observer/dead/verb/toggle_antagHUD) // Poor guys, don't know what they are missing!
- observer.key = key
- observer.set_respawn_timer(time_till_respawn()) // Will keep their existing time if any, or return 0 and pass 0 into set_respawn_timer which will use the defaults
- observer.client.init_verbs()
- qdel(src)
-
- return TRUE
-
- if(href_list["late_join"])
-
- if(!ticker || ticker.current_state != GAME_STATE_PLAYING)
- to_chat(usr, span_red("The round is either not ready, or has already finished..."))
- return
-
- 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!"))
- else if(time_till_respawn) // Nonzero time to respawn
- to_chat(usr, span_warning("You can't respawn yet! You need to wait another [round(time_till_respawn/10/60, 0.1)] minutes."))
- return
- LateChoices()
-
- if(href_list["manifest"])
- ViewManifest()
-
if(href_list["privacy_poll"])
establish_db_connection()
if(!SSdbcore.IsConnected())
@@ -276,13 +111,6 @@
if(!ready && href_list["preference"])
if(client)
client.prefs.process_link(src, href_list)
- else if(!href_list["late_join"])
- new_player_panel()
-
- if(href_list["showpoll"])
-
- handle_player_polling()
- return
if(href_list["pollid"])
@@ -334,27 +162,6 @@
if(!isnull(href_list["option_[optionid]"])) //Test if this optionid was selected
vote_on_poll(pollid, optionid, 1)
- if(href_list["shownews"])
- handle_server_news()
- return
-
- if(href_list["hidden_jobs"])
- show_hidden_jobs = !show_hidden_jobs
- LateChoices()
-
- if(href_list["give_feedback"])
- if(!SSsqlite.can_submit_feedback(my_client))
- return
-
- if(client.feedback_form)
- client.feedback_form.display() // In case they closed the form early.
- else
- client.feedback_form = new(client)
-
- if(href_list["open_changelog"])
- write_preference_directly(/datum/preference/text/lastchangelog, GLOB.changelog_hash)
- client.changes()
- return
/mob/new_player/proc/handle_server_news()
if(!client)
@@ -473,9 +280,6 @@
var/mob/living/character = create_character(T) //creates the human and transfers vars and mind
character = job_master.EquipRank(character, rank, 1) //equips the human
UpdateFactionList(character)
- if(character && character.client)
- var/obj/screen/splash/Spl = new(character.client, TRUE)
- Spl.Fade(TRUE)
var/datum/job/J = SSjob.get_job(rank)
@@ -665,7 +469,6 @@
src << browse(null, "window=latechoices") //closes late choices window
src << browse(null, "window=preferences_window") //VOREStation Edit?
src << browse(null, "window=News") //closes news window
- panel.close()
/mob/new_player/get_species()
var/datum/species/chosen_species
diff --git a/code/modules/tgui/tgui.dm b/code/modules/tgui/tgui.dm
index 7d1c8ecd7f..5fa72caa71 100644
--- a/code/modules/tgui/tgui.dm
+++ b/code/modules/tgui/tgui.dm
@@ -47,6 +47,8 @@
var/list/children = list()
/// Any partial packets that we have received from TGUI, waiting to be sent
var/partial_packets
+ /// If the window should be closed with other windows when requested
+ var/closeable = TRUE
/**
* public
@@ -60,13 +62,13 @@
* optional parent_ui datum/tgui The parent of this UI.
* optional ui_x int Deprecated: Window width.
* optional ui_y int Deprecated: Window height.
+ * optional window datum/tgui_window: The window to display this TGUI within
*
* return datum/tgui The requested UI.
*/
-/datum/tgui/New(mob/user, datum/src_object, interface, title, datum/tgui/parent_ui, ui_x, ui_y)
+/datum/tgui/New(mob/user, datum/src_object, interface, title, datum/tgui/parent_ui, ui_x, ui_y, datum/tgui_window/window)
src.user = user
src.src_object = src_object
- src.window_key = "[REF(src_object)]-main"
src.interface = interface
if(title)
src.title = title
@@ -78,6 +80,12 @@
if(ui_x && ui_y)
src.window_size = list(ui_x, ui_y)
+ if(window)
+ src.window = window
+ src.window_key = window.id
+ else
+ src.window_key = "[REF(src_object)]-main"
+
/datum/tgui/Destroy()
user = null
src_object = null
@@ -88,22 +96,26 @@
*
* Open this UI (and initialize it with data).
*
+ * Args:
+ * preinitialized: bool - if TRUE, we will not attempt to force strict mode on the tgui's window datum
+ *
* return bool - TRUE if a new pooled window is opened, FALSE in all other situations including if a new pooled window didn't open because one already exists.
*/
-/datum/tgui/proc/open()
+/datum/tgui/proc/open(preinitialized = FALSE)
if(!user?.client)
return FALSE
- if(window)
+ if(window && window.status > TGUI_WINDOW_LOADING)
return FALSE
process_status()
if(status < STATUS_UPDATE)
return FALSE
- window = SStgui.request_pooled_window(user)
+ if(!window)
+ window = SStgui.request_pooled_window(user)
if(!window)
return FALSE
opened_at = world.time
window.acquire_lock(src)
- if(!window.is_ready())
+ if(!window.is_ready() && !preinitialized)
window.initialize(
strict_mode = TRUE,
fancy = user.read_preference(/datum/preference/toggle/tgui_fancy),
diff --git a/html/lobby/loading.gif b/html/lobby/loading.gif
new file mode 100644
index 0000000000..4d2f0623d3
Binary files /dev/null and b/html/lobby/loading.gif differ
diff --git a/interface/skin.dmf b/interface/skin.dmf
index b225b5be93..2c9c24b7be 100644
--- a/interface/skin.dmf
+++ b/interface/skin.dmf
@@ -1284,6 +1284,15 @@ window "mapwindow"
on-show = ".winset\"mainwindow.mainvsplit.left=mapwindow\""
on-hide = ".winset\"mainwindow.mainvsplit.left=\""
style = ".center { text-align: center; } .runechatdiv {background-color: #20202070} .black_outline { -dm-text-outline: 1px black } .boldtext { font-weight: bold; } .maptext { font-family: 'Grand9K Pixel'; font-size: 6pt; -dm-text-outline: 1px black; color: white; line-height: 1.0; } .command_headset { font-weight: bold;\tfont-size: 8px; } .small { font-size: 6px; } .very_small { font-size: 5px;} .big { font-size: 8px; } .reallybig { font-size: 8px; } .extremelybig { font-size: 8px; } .greentext { color: #00FF00; font-size: 7px; } .redtext { color: #FF0000; font-size: 7px; } .clown { color: #FF69Bf; font-size: 7px; font-weight: bold; } .his_grace { color: #15D512; } .hypnophrase { color: #0d0d0d; font-weight: bold; } .yell { font-weight: bold; } .italics { font-size: 7px; font-style: italic; }"
+ elem "lobby_browser"
+ type = BROWSER
+ pos = 0,0
+ size = 640,480
+ anchor1 = 0,0
+ anchor2 = 100,100
+ is-visible = false
+ is-disabled = true
+ background-color = #222222
window "outputwindow"
elem "outputwindow"
diff --git a/sound/lobby/lobby_load.ogg b/sound/lobby/lobby_load.ogg
new file mode 100644
index 0000000000..71e30bf2a6
Binary files /dev/null and b/sound/lobby/lobby_load.ogg differ
diff --git a/tgui/packages/tgui/interfaces/AlertModal.tsx b/tgui/packages/tgui/interfaces/AlertModal.tsx
index 791bc38105..dc7f549a7c 100644
--- a/tgui/packages/tgui/interfaces/AlertModal.tsx
+++ b/tgui/packages/tgui/interfaces/AlertModal.tsx
@@ -1,26 +1,29 @@
-import { useState } from 'react';
+import { type KeyboardEvent, useState } from 'react';
import { useBackend } from 'tgui/backend';
import { Window } from 'tgui/layouts';
import { Autofocus, Box, Button, Section, Stack } from 'tgui-core/components';
import { isEscape, KEY } from 'tgui-core/keys';
+import { type BooleanLike } from 'tgui-core/react';
import { Loader } from './common/Loader';
-type AlertModalData = {
- autofocus: boolean;
+type Data = {
+ autofocus: BooleanLike;
buttons: string[];
- large_buttons: boolean;
+ large_buttons: BooleanLike;
message: string;
- swapped_buttons: boolean;
+ swapped_buttons: BooleanLike;
timeout: number;
title: string;
};
-const KEY_DECREMENT = -1;
-const KEY_INCREMENT = 1;
+enum DIRECTION {
+ Increment = 1,
+ Decrement = -1,
+}
-export const AlertModal = (props) => {
- const { act, data } = useBackend();
+export function AlertModal(props) {
+ const { act, data } = useBackend();
const {
autofocus,
buttons = [],
@@ -29,128 +32,168 @@ export const AlertModal = (props) => {
timeout,
title,
} = data;
+
+ // Stolen wholesale from fontcode
+ function textWidth(text: string, font: string, fontsize: number) {
+ // default font height is 12 in tgui
+ font = fontsize + 'x ' + font;
+ const c = document.createElement('canvas');
+ const ctx = c.getContext('2d') as CanvasRenderingContext2D;
+ ctx.font = font;
+ return ctx.measureText(text).width;
+ }
+
const [selected, setSelected] = useState(0);
+
+ const windowWidth = 345 + (buttons.length > 2 ? 55 : 0);
+
+ // very accurate estimate of padding for each num of buttons
+ const paddingMagicNumber = 67 / buttons.length + 23;
+
+ // At least one of the buttons has a long text message
+ const isVerbose = buttons.some(
+ (button) =>
+ textWidth(button, '', large_buttons ? 14 : 12) > // 14 is the larger font size for large buttons
+ windowWidth / buttons.length - paddingMagicNumber,
+ );
+ const largeSpacing = isVerbose && large_buttons ? 20 : 15;
+
// Dynamically sets window dimensions
const windowHeight =
- 115 +
+ 120 +
+ (isVerbose ? largeSpacing * buttons.length : 0) +
(message.length > 30 ? Math.ceil(message.length / 4) : 0) +
(message.length && large_buttons ? 5 : 0);
- const windowWidth = 325 + (buttons.length > 2 ? 55 : 0);
- const onKey = (direction: number) => {
- if (selected === 0 && direction === KEY_DECREMENT) {
- setSelected(buttons.length - 1);
- } else if (selected === buttons.length - 1 && direction === KEY_INCREMENT) {
- setSelected(0);
- } else {
- setSelected(selected + direction);
- }
- };
- function handleKeyDown(event: React.KeyboardEvent) {
- const key = event.key;
- /**
- * Simulate a click when pressing space or enter,
- * allow keyboard navigation, override tab behavior
- */
- if (key === KEY.Space || key === KEY.Enter) {
- act('choose', { choice: buttons[selected] });
- } else if (isEscape(key)) {
- act('cancel');
- } else if (key === KEY.Left) {
- event.preventDefault();
- onKey(KEY_DECREMENT);
- } else if (key === KEY.Tab || key === KEY.Right) {
- event.preventDefault();
- onKey(KEY_INCREMENT);
+ /** Changes button selection, etc */
+ function keyDownHandler(event: KeyboardEvent) {
+ switch (event.key) {
+ case KEY.Space:
+ case KEY.Enter:
+ act('choose', { choice: buttons[selected] });
+ return;
+ case KEY.Left:
+ event.preventDefault();
+ onKey(DIRECTION.Decrement);
+ return;
+ case KEY.Tab:
+ case KEY.Right:
+ event.preventDefault();
+ onKey(DIRECTION.Increment);
+ return;
+
+ default:
+ if (isEscape(event.key)) {
+ act('cancel');
+ return;
+ }
}
}
+ /** Manages iterating through the buttons */
+ function onKey(direction: DIRECTION) {
+ const newIndex = (selected + direction + buttons.length) % buttons.length;
+ setSelected(newIndex);
+ }
+
return (
{!!timeout && }
- handleKeyDown(e)}>
+
-
+
{message}
-
+
{!!autofocus && }
-
+ {isVerbose ? (
+
+ ) : (
+
+ )}
);
+}
+
+type ButtonDisplayProps = {
+ selected: number;
};
/**
* Displays a list of buttons ordered by user prefs.
- * Technically this handles more than 2 buttons, but you
+ */
+function HorizontalButtons(props: ButtonDisplayProps) {
+ const { act, data } = useBackend();
+ const { buttons = [], large_buttons, swapped_buttons } = data;
+ const { selected } = props;
+
+ return (
+
+ {buttons.map((button, index) => (
+
+
+
+ ))}
+
+ );
+}
+
+/**
+ * Technically the parent handles more than 2 buttons, but you
* should just be using a list input in that case.
*/
-const ButtonDisplay = (props) => {
- const { data } = useBackend();
+function VerticalButtons(props: ButtonDisplayProps) {
+ const { act, data } = useBackend();
const { buttons = [], large_buttons, swapped_buttons } = data;
const { selected } = props;
return (
- {buttons?.map((button, index) =>
- !!large_buttons && buttons.length < 3 ? (
-
-
-
- ) : (
-
-
-
- ),
- )}
+ {buttons.map((button, index) => (
+
+
+
+ ))}
);
-};
-
-/**
- * Displays a button with variable sizing.
- */
-const AlertButton = (props) => {
- const { act, data } = useBackend();
- const { large_buttons } = data;
- const { button, selected } = props;
- const buttonWidth = button.length > 7 ? button.length : 7;
-
- return (
-
- );
-};
+}
diff --git a/tgui/packages/tgui/interfaces/LobbyMenu/LobbyButtons.tsx b/tgui/packages/tgui/interfaces/LobbyMenu/LobbyButtons.tsx
new file mode 100644
index 0000000000..41d6c3b2fa
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/LobbyMenu/LobbyButtons.tsx
@@ -0,0 +1,136 @@
+import { useBackend } from 'tgui/backend';
+import { Box, Section, Stack } from 'tgui-core/components';
+
+import { LobbyButton, TimedDivider } from './LobbyElements';
+import type { LobbyData } from './types';
+
+export const LobbyButtons = (props: {
+ readonly setModal: (_) => void;
+ readonly hidden: boolean;
+ readonly setHidden: (_: boolean) => void;
+}) => {
+ const { act, data } = useBackend();
+ const { setModal, hidden, setHidden } = props;
+
+ const {
+ map,
+ station_time,
+ round_start,
+ round_time,
+ ready,
+ new_news,
+ can_submit_feedback,
+ show_station_news,
+ new_station_news,
+ new_changelog,
+ } = data;
+
+ return (
+
+
+
+
+
+
+ setHidden(true)}
+ />
+
+
+
+
+
+ Welcome to VOREStation.
+
+
+ Map: {map}
+
+
+ Station Time: {station_time}
+
+
+
+ Round Duration:{' '}
+ {round_start ? 'Initializing...' : round_time}{' '}
+
+
+
+
+
+
+
+
+ act('character_setup')}
+ icon="file-lines"
+ >
+ Setup Character
+
+
+ act('manifest')}
+ icon="circle-user"
+ >
+ View Crew Manifest
+
+
+ act('shownews')} icon="newspaper">
+ Show Server News {new_news ? '(NEW!)' : null}
+
+
+ {can_submit_feedback ? (
+ act('give_feedback')}
+ icon="clipboard-question"
+ >
+ Give Feedback
+
+ ) : null}
+
+ act('open_changelog')}
+ icon="pencil"
+ >
+ Show Changelog {new_changelog ? '(NEW!)' : null}
+
+
+ {show_station_news ? (
+ act('')} icon="newspaper">
+ Show Station News {new_station_news ? '(NEW!)' : null}
+
+ ) : null}
+
+
+ act('observe')}>
+ Observe
+
+
+ {round_start ? (
+ act('ready')}
+ icon={ready ? 'check' : 'xmark'}
+ >
+ {ready ? 'Unready' : 'Ready'}
+
+ ) : (
+ act('late_join')} icon="users">
+ Join Game
+
+ )}
+
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/LobbyMenu/LobbyElements.tsx b/tgui/packages/tgui/interfaces/LobbyMenu/LobbyElements.tsx
new file mode 100644
index 0000000000..19467b82ad
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/LobbyMenu/LobbyElements.tsx
@@ -0,0 +1,68 @@
+import { useContext, useEffect, useRef } from 'react';
+import { useBackend } from 'tgui/backend';
+import { Box, Button, Stack } from 'tgui-core/components';
+
+import { LobbyContext } from './constants';
+import type { LobbyButtonProps, LobbyContextType } from './types';
+
+export const TimedDivider = () => {
+ const ref = useRef(null);
+
+ const context = useContext(LobbyContext);
+ const { animationsDisabled, animationsFinished } = context;
+
+ useEffect(() => {
+ if (!animationsFinished && !animationsDisabled) {
+ setTimeout(() => {
+ ref.current!.style.display = 'block';
+ }, 1500);
+ }
+ }, [animationsFinished, animationsDisabled]);
+
+ return (
+
+
+
+ );
+};
+
+export const LobbyButton = (props: LobbyButtonProps) => {
+ const { children, index, className, ...rest } = props;
+
+ const context = useContext(LobbyContext);
+
+ return (
+
+
+ {children}
+
+
+ );
+};
+
+export const CustomButton = (props) => {
+ const { act } = useBackend();
+
+ return (
+ // this works because of event propagation
+ act('keyboard')}>
+
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/LobbyMenu/constants.ts b/tgui/packages/tgui/interfaces/LobbyMenu/constants.ts
new file mode 100644
index 0000000000..018f6a02a6
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/LobbyMenu/constants.ts
@@ -0,0 +1,8 @@
+import { createContext } from 'react';
+
+import type { LobbyContextType } from './types';
+
+export const LobbyContext = createContext({
+ animationsDisabled: false,
+ animationsFinished: false,
+});
diff --git a/tgui/packages/tgui/interfaces/LobbyMenu/index.tsx b/tgui/packages/tgui/interfaces/LobbyMenu/index.tsx
new file mode 100644
index 0000000000..3e1858c6c3
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/LobbyMenu/index.tsx
@@ -0,0 +1,165 @@
+import { storage } from 'common/storage';
+import { type ReactNode, useEffect, useRef, useState } from 'react';
+import { resolveAsset } from 'tgui/assets';
+import { useBackend } from 'tgui/backend';
+import { Window } from 'tgui/layouts';
+import { Box, Modal, Section, Stack } from 'tgui-core/components';
+import { classes } from 'tgui-core/react';
+
+import { LobbyContext } from './constants';
+import { LobbyButtons } from './LobbyButtons';
+import { CustomButton } from './LobbyElements';
+import type { LobbyData } from './types';
+
+export const LobbyMenu = (props) => {
+ const { act, data } = useBackend();
+
+ const onLoadPlayer = useRef(null);
+
+ const [modal, setModal] = useState(false);
+
+ const [filterDisabled, setFilterDisabled] = useState(false);
+ const [animationsDisabled, setAnimationsDisabled] = useState(false);
+ const [audioDisabled, setAudioDisabled] = useState(false);
+ const [animationsFinished, setAnimationsFinished] = useState(false);
+
+ useEffect(() => {
+ storage
+ .get('lobby-filter-disabled')
+ .then((val) => setFilterDisabled(!!val));
+ storage
+ .get('lobby-animations-disabled')
+ .then((val) => setAnimationsDisabled(!!val));
+ storage.get('lobby-audio-disabled').then((val) => setAudioDisabled(!!val));
+
+ setTimeout(() => {
+ if (onLoadPlayer.current) {
+ onLoadPlayer.current!.play();
+ }
+ }, 250);
+
+ setTimeout(() => {
+ setAnimationsFinished(true);
+ }, 10000);
+ }, []);
+
+ const [hidden, setHidden] = useState(false);
+
+ return (
+
+ {!audioDisabled && (
+
+ )}
+
+
+ {!!modal && {modal}}
+
+
+
+ {
+ setModal(
+ setModal(false)}
+ />
+ }
+ >
+
+
+ {
+ storage.set(
+ 'lobby-filter-disabled',
+ !filterDisabled,
+ );
+ setFilterDisabled(!filterDisabled);
+ setModal(false);
+ }}
+ tooltip="Removes the CRT filter background"
+ >
+ {`${filterDisabled ? 'Enable' : 'Disable'} Scanlines`}
+
+
+
+ {
+ storage.set('lobby-audio-disabled', !audioDisabled);
+ setAudioDisabled(!audioDisabled);
+ setModal(false);
+ }}
+ tooltip="Removes the loading audio"
+ >
+ {`${audioDisabled ? 'Enable' : 'Disable'} Audio`}
+
+
+
+ {
+ storage.set(
+ 'lobby-animations-disabled',
+ !animationsDisabled,
+ );
+ setAnimationsDisabled(!animationsDisabled);
+ setModal(false);
+ }}
+ tooltip="Disables animations."
+ >
+ {`${animationsDisabled ? 'Enable' : 'Disable'} Animations`}
+
+
+
+ ,
+ );
+ }}
+ />
+
+ {hidden && (
+
+ setHidden(false)} />
+
+ )}
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/LobbyMenu/types.ts b/tgui/packages/tgui/interfaces/LobbyMenu/types.ts
new file mode 100644
index 0000000000..c15ddc6644
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/LobbyMenu/types.ts
@@ -0,0 +1,31 @@
+import type { ReactNode } from 'react';
+import type { Box } from 'tgui-core/components';
+import type { BooleanLike } from 'tgui-core/react';
+
+export type LobbyData = {
+ display_loading: BooleanLike;
+ map: string;
+ station_time: string;
+ round_start: BooleanLike;
+ round_time: string;
+ ready: BooleanLike;
+ new_news: BooleanLike;
+ can_submit_feedback: BooleanLike;
+ show_station_news: BooleanLike;
+ new_station_news: BooleanLike;
+ new_changelog: BooleanLike;
+};
+
+export type LobbyContextType = {
+ animationsDisabled: boolean;
+ animationsFinished: boolean;
+ setModal?: (_: ReactNode | false) => void;
+};
+
+export type LobbyButtonProps = React.ComponentProps & {
+ readonly index: number;
+ readonly selected?: boolean;
+ readonly disabled?: boolean;
+ readonly icon?: string;
+ readonly tooltip?: string;
+};
diff --git a/tgui/packages/tgui/interfaces/MobSpawner.tsx b/tgui/packages/tgui/interfaces/MobSpawner.tsx
index 42355086da..7fb5317032 100644
--- a/tgui/packages/tgui/interfaces/MobSpawner.tsx
+++ b/tgui/packages/tgui/interfaces/MobSpawner.tsx
@@ -363,6 +363,7 @@ const GeneralMobSettings = (props: {
Description: