diff --git a/code/modules/mob/new_player/new_player.dm b/code/modules/mob/new_player/new_player.dm
index b26d0f46d60..5422d84e391 100644
--- a/code/modules/mob/new_player/new_player.dm
+++ b/code/modules/mob/new_player/new_player.dm
@@ -9,6 +9,8 @@
var/totalPlayers = 0
var/totalPlayersReady = 0
var/datum/browser/panel
+ /// Track if we've shown the Ctrl+Click tip
+ var/shown_ctrl_tip = FALSE
universal_speak = 1
invisibility = 101
@@ -40,7 +42,7 @@
else
output += "View the Crew Manifest
"
- output += "
Join Game! " output += "
" @@ -173,6 +175,9 @@ return 1 if(href_list["late_join"]) + // Ctrl+Click forces legacy UI + var/force_legacy = href_list["force_legacy"] ? TRUE : FALSE + if(!SSticker.IsRoundInProgress()) to_chat(usr, span_red("The round is either not ready, or has already finished...")) return @@ -227,7 +232,15 @@ tgui_alert(src, "The server is full!", "Oh No!") return TRUE - LateChoices() + // Choose UI based on button clicked or auto-detection + if(!force_legacy && use_tgui_latejoin()) + // Show tip once about Ctrl+Click + if(!shown_ctrl_tip) + shown_ctrl_tip = TRUE + to_chat(src, span_notice("Tip: You can use Ctrl+Click on 'Join Game!' to open the legacy interface, if TGUI menu does not show.")) + ui_interact(src) // Try TGUI first + else + LateChoices() // Fallback to legacy if(href_list["manifest"]) show_manifest(src, nano_state = GLOB.interactive_state) @@ -312,19 +325,26 @@ return FALSE if(jobban_isbanned(src.ckey,rank)) return FALSE + // Check setup restrictions (e.g., Church jobs require specific setup options) + if(client && client.prefs && job.is_restricted(client.prefs)) + return FALSE return TRUE /mob/new_player/proc/AttemptLateSpawn(rank, spawning_at) if(src != usr) return FALSE + return LateSpawn(rank) + +// Shared late spawn logic (used by both legacy Topic and TGUI) +/mob/new_player/proc/LateSpawn(rank) if(!SSticker.IsRoundInProgress()) - to_chat(usr, span_red("The round is either not ready, or has already finished...")) + to_chat(src, span_red("The round is either not ready, or has already finished...")) return FALSE if(!GLOB.enter_allowed) - to_chat(usr, span_notice("There is an administrative lock on entering the game!")) + to_chat(src, span_notice("There is an administrative lock on entering the game!")) return FALSE if(!IsJobAvailable(rank)) - src << alert("[rank] is not available. Please try another.") + to_chat(src, span_warning("[rank] is not available. Please try another.")) return FALSE spawning = 1 @@ -374,6 +394,118 @@ qdel(src) +// TGUI Detection - check if we should use TGUI or fallback to legacy browser +/mob/new_player/proc/use_tgui_latejoin() + // Check if client exists + if(!client) + return FALSE + + // Check if TGUI subsystem exists + if(!SStgui) + return FALSE + + // Try to get a window from the pool to verify availability + var/datum/tgui_window/window = SStgui.request_pooled_window(src) + if(!window) + // Pool exhausted or unavailable - use fallback + to_chat(src, span_warning("TGUI window pool exhausted, using legacy interface.")) + return FALSE + + // Return window to pool (we were just checking) + window.release_lock() + + return TRUE + +// TGUI Interface - modern UI for job selection +/mob/new_player/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "LateJoin") + ui.set_autoupdate(FALSE) // Disable autoupdate to prevent dexterity checks + ui.open() + +/mob/new_player/ui_state(mob/user) + return GLOB.always_state // No distance/access restrictions for new players + +/mob/new_player/ui_status(mob/user, datum/ui_state/state) + // Always allow interaction for new_player, skip all checks + return UI_INTERACTIVE + +/mob/new_player/ui_data(mob/user) + var/list/data = list() + + // Player info + data["playerName"] = client.prefs.be_random_name ? "friend" : client.prefs.real_name + + // Round info + data["roundDuration"] = DisplayTimeText(world.time - SSticker.round_start_time) + + // Evacuation status + data["isEvacuating"] = evacuation_controller.is_evacuating() + data["isEvacuated"] = evacuation_controller.has_evacuated() + + // Build job list grouped by department + var/list/departments = list() + + for(var/datum/department/dept in SSjob.departments) + var/list/dept_data = list( + "name" = dept.name, + "jobs" = list() + ) + + for(var/datum/job/job in dept.jobs) + // Count active players + var/active = 0 + for(var/mob/M in GLOB.player_list) + if(M.mind && M.client && M.mind.assigned_role == job.title) + if(M.client.inactivity <= 10 MINUTES) + active++ + + // Check availability (including experience requirements) + var/is_available = IsJobAvailable(job.title) + + var/list/job_data = list( + "title" = job.title, + "currentPositions" = job.current_positions, + "totalPositions" = job.total_positions, // -1 = unlimited + "activePlayers" = active, + "expRequired" = job.exp_requirements, + "expType" = job.exp_required_type, + "department" = job.department, + "available" = is_available, + "description" = job.description, + "supervisors" = job.supervisors, + ) + + dept_data["jobs"] += list(job_data) + + if(length(dept_data["jobs"])) + departments += list(dept_data) + + data["departments"] = departments + + return data + +/mob/new_player/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) + // Don't call parent - we don't need the default checks for new_player + // Parent would check UI_INTERACTIVE status which may fail for new_player + + switch(action) + if("select_job") + var/job_title = params["job"] + if(!job_title) + return FALSE + + // Use shared spawn logic + LateSpawn(job_title) + return TRUE + + if("close") + ui.close() + return TRUE + + return FALSE + /mob/new_player/proc/LateChoices() var/name = client.prefs.be_random_name ? "friend" : client.prefs.real_name diff --git a/tgui/packages/tgui/assets/50px-Command.png b/tgui/packages/tgui/assets/50px-Command.png new file mode 100644 index 00000000000..268ee9c3315 Binary files /dev/null and b/tgui/packages/tgui/assets/50px-Command.png differ diff --git a/tgui/packages/tgui/assets/54px-NeoTheology.png b/tgui/packages/tgui/assets/54px-NeoTheology.png new file mode 100644 index 00000000000..fbb25f56554 Binary files /dev/null and b/tgui/packages/tgui/assets/54px-NeoTheology.png differ diff --git a/tgui/packages/tgui/assets/54px-Ship.png b/tgui/packages/tgui/assets/54px-Ship.png new file mode 100644 index 00000000000..f4b8db0cb3f Binary files /dev/null and b/tgui/packages/tgui/assets/54px-Ship.png differ diff --git a/tgui/packages/tgui/assets/64px-Guild.png b/tgui/packages/tgui/assets/64px-Guild.png new file mode 100644 index 00000000000..a5ec5fe787d Binary files /dev/null and b/tgui/packages/tgui/assets/64px-Guild.png differ diff --git a/tgui/packages/tgui/assets/64px-Ironhammer.png b/tgui/packages/tgui/assets/64px-Ironhammer.png new file mode 100644 index 00000000000..5fc73da17a7 Binary files /dev/null and b/tgui/packages/tgui/assets/64px-Ironhammer.png differ diff --git a/tgui/packages/tgui/assets/64px-Moebius.png b/tgui/packages/tgui/assets/64px-Moebius.png new file mode 100644 index 00000000000..4e063c72e57 Binary files /dev/null and b/tgui/packages/tgui/assets/64px-Moebius.png differ diff --git a/tgui/packages/tgui/assets/64px-Technomancers.png b/tgui/packages/tgui/assets/64px-Technomancers.png new file mode 100644 index 00000000000..4fb77ced68b Binary files /dev/null and b/tgui/packages/tgui/assets/64px-Technomancers.png differ diff --git a/tgui/packages/tgui/interfaces/LateJoin.tsx b/tgui/packages/tgui/interfaces/LateJoin.tsx new file mode 100644 index 00000000000..40bbf46d8f0 --- /dev/null +++ b/tgui/packages/tgui/interfaces/LateJoin.tsx @@ -0,0 +1,275 @@ +import { useBackend } from 'tgui/backend'; +import { + Box, + Button, + Flex, + LabeledList, + Section, + Stack, +} from 'tgui-core/components'; + +import factionCEVCommand from '../assets/50px-Command.png'; +import factionNeotheology from '../assets/54px-NeoTheology.png'; +import factionCEVCivilians from '../assets/54px-Ship.png'; +import factionGuild from '../assets/64px-Guild.png'; +import factionIronhammer from '../assets/64px-Ironhammer.png'; +import factionMoebius from '../assets/64px-Moebius.png'; +import factionTechnomancers from '../assets/64px-Technomancers.png'; +import { Window } from '../layouts'; + +type LateJoinData = { + playerName: string; + roundDuration: string; + isEvacuating: boolean; + isEvacuated: boolean; + departments: Department[]; +}; + +type Department = { + name: string; + jobs: Job[]; +}; + +type Job = { + title: string; + currentPositions: number; + totalPositions: number; // -1 = unlimited + activePlayers: number; + expRequired: number; + expType: string; + department: string; + available: boolean; + description: string; + supervisors: string; +}; + +// Department logos +const DEPARTMENT_LOGOS: Record