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 ( + + ); +}; 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')}> +