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 += "

Join Game! " output += "

Observe

" @@ -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 = { + 'Ironhammer Mercenary Company': factionIronhammer, + 'Moebius Labs: Medical Division': factionMoebius, + 'Moebius Labs: Research Division': factionMoebius, + 'Asters Merchant Guild': factionGuild, + 'Church of NeoTheology': factionNeotheology, + 'CEV Eris Command': factionCEVCommand, + 'Technomancer League': factionTechnomancers, + 'CEV Eris Civilian': factionCEVCivilians, +}; + +// Department color scheme - backgrounds and accents +const DEPARTMENT_COLORS: Record< + string, + { bg: string; accent: string; button: string } +> = { + 'CEV Eris Command': { + bg: 'rgba(74, 144, 226, 0.25)', + accent: 'rgba(74, 144, 226, 0.8)', + button: 'rgba(74, 144, 226, 0.15)', + }, + 'Ironhammer Mercenary Company': { + bg: 'rgba(231, 76, 60, 0.25)', + accent: 'rgba(231, 76, 60, 0.8)', + button: 'rgba(231, 76, 60, 0.15)', + }, + 'Technomancer League': { + bg: 'rgba(243, 156, 18, 0.25)', + accent: 'rgba(243, 156, 18, 0.8)', + button: 'rgba(243, 156, 18, 0.15)', + }, + 'Moebius Labs: Medical Division': { + bg: 'rgba(100, 200, 255, 0.25)', + accent: 'rgba(100, 200, 255, 0.8)', + button: 'rgba(100, 200, 255, 0.15)', + }, + 'Moebius Labs: Research Division': { + bg: 'rgba(155, 89, 182, 0.25)', + accent: 'rgba(155, 89, 182, 0.8)', + button: 'rgba(155, 89, 182, 0.15)', + }, + 'Church of NeoTheology': { + bg: 'rgba(241, 196, 15, 0.25)', + accent: 'rgba(241, 196, 15, 0.8)', + button: 'rgba(241, 196, 15, 0.15)', + }, + 'Asters Merchant Guild': { + bg: 'rgba(39, 174, 96, 0.25)', + accent: 'rgba(39, 174, 96, 0.8)', + button: 'rgba(39, 174, 96, 0.15)', + }, + 'CEV Eris Civilian': { + bg: 'rgba(149, 165, 166, 0.25)', + accent: 'rgba(149, 165, 166, 0.8)', + button: 'rgba(149, 165, 166, 0.15)', + }, + Offship: { + bg: 'rgba(139, 69, 19, 0.25)', + accent: 'rgba(139, 69, 19, 0.8)', + button: 'rgba(139, 69, 19, 0.15)', + }, + Silicon: { + bg: 'rgba(52, 152, 219, 0.25)', + accent: 'rgba(52, 152, 219, 0.8)', + button: 'rgba(52, 152, 219, 0.15)', + }, +}; + +// Split departments into columns for better layout +const splitIntoColumns = (items: Department[], columns: number) => { + const result: Department[][] = Array.from({ length: columns }, () => []); + items.forEach((item, index) => { + result[index % columns].push(item); + }); + return result; +}; + +export const LateJoin = (props: any) => { + const { act, data } = useBackend(); + const { + playerName, + roundDuration, + isEvacuating: evacuating, + isEvacuated: evacuated, + departments, + } = data; + + // Convert DM numbers to proper booleans + const isEvacuating = !!evacuating; + const isEvacuated = !!evacuated; + + const columns = splitIntoColumns(departments || [], 3); + + return ( + + + + +
+ + + {roundDuration || 'Unknown'} + + {isEvacuating && !isEvacuated && ( + + The station is evacuating! + + )} + {isEvacuated && ( + + The station has been evacuated! + + )} + +
+
+ + + + {columns.map((column, columnIndex) => ( + + + {column.map((dept) => { + const deptColors = DEPARTMENT_COLORS[dept.name] || { + bg: 'rgba(149, 165, 166, 0.25)', + accent: 'rgba(149, 165, 166, 0.8)', + button: 'rgba(149, 165, 166, 0.15)', + }; + return ( + + + + + {dept.name} + + {DEPARTMENT_LOGOS[dept.name] ? ( + + {dept.name} + + ) : ( + + )} + + + {dept.jobs.map((job) => { + const isAvailable = job.available; + const tooltipText = !isAvailable + ? 'Not available (check requirements, positions, or bans)' + : `You answer to ${job.supervisors}`; + + return ( + +