|
9 | 9 | var/totalPlayers = 0 |
10 | 10 | var/totalPlayersReady = 0 |
11 | 11 | var/datum/browser/panel |
| 12 | + /// Track if we've shown the Ctrl+Click tip |
| 13 | + var/shown_ctrl_tip = FALSE |
12 | 14 | universal_speak = 1 |
13 | 15 |
|
14 | 16 | invisibility = 101 |
|
40 | 42 |
|
41 | 43 | else |
42 | 44 | output += "<a href='byond://?src=[REF(src)];manifest=1'>View the Crew Manifest</A><br><br>" |
43 | | - output += "<p><a href='byond://?src=[REF(src)];late_join=1'>Join Game!</A></p>" |
| 45 | + output += "<p><a href='byond://?src=[REF(src)];late_join=1' onclick='var e = window.event || event; if(e && e.ctrlKey) { window.location=\"byond://?src=[REF(src)];late_join=1;force_legacy=1\"; return false; } return true;'>Join Game!</A> " |
44 | 46 |
|
45 | 47 | output += "<p><a href='byond://?src=[REF(src)];observe=1'>Observe</A></p>" |
46 | 48 |
|
|
173 | 175 | return 1 |
174 | 176 |
|
175 | 177 | if(href_list["late_join"]) |
| 178 | + // Ctrl+Click forces legacy UI |
| 179 | + var/force_legacy = href_list["force_legacy"] ? TRUE : FALSE |
| 180 | + |
176 | 181 | if(!SSticker.IsRoundInProgress()) |
177 | 182 | to_chat(usr, span_red("The round is either not ready, or has already finished...")) |
178 | 183 | return |
|
227 | 232 | tgui_alert(src, "The server is full!", "Oh No!") |
228 | 233 | return TRUE |
229 | 234 |
|
230 | | - LateChoices() |
| 235 | + // Choose UI based on button clicked or auto-detection |
| 236 | + if(!force_legacy && use_tgui_latejoin()) |
| 237 | + // Show tip once about Ctrl+Click |
| 238 | + if(!shown_ctrl_tip) |
| 239 | + shown_ctrl_tip = TRUE |
| 240 | + 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.")) |
| 241 | + ui_interact(src) // Try TGUI first |
| 242 | + else |
| 243 | + LateChoices() // Fallback to legacy |
231 | 244 |
|
232 | 245 | if(href_list["manifest"]) |
233 | 246 | show_manifest(src, nano_state = GLOB.interactive_state) |
|
312 | 325 | return FALSE |
313 | 326 | if(jobban_isbanned(src.ckey,rank)) |
314 | 327 | return FALSE |
| 328 | + // Check setup restrictions (e.g., Church jobs require specific setup options) |
| 329 | + if(client && client.prefs && job.is_restricted(client.prefs)) |
| 330 | + return FALSE |
315 | 331 | return TRUE |
316 | 332 |
|
317 | 333 | /mob/new_player/proc/AttemptLateSpawn(rank, spawning_at) |
318 | 334 | if(src != usr) |
319 | 335 | return FALSE |
| 336 | + return LateSpawn(rank) |
| 337 | + |
| 338 | +// Shared late spawn logic (used by both legacy Topic and TGUI) |
| 339 | +/mob/new_player/proc/LateSpawn(rank) |
320 | 340 | if(!SSticker.IsRoundInProgress()) |
321 | | - to_chat(usr, span_red("The round is either not ready, or has already finished...")) |
| 341 | + to_chat(src, span_red("The round is either not ready, or has already finished...")) |
322 | 342 | return FALSE |
323 | 343 | if(!GLOB.enter_allowed) |
324 | | - to_chat(usr, span_notice("There is an administrative lock on entering the game!")) |
| 344 | + to_chat(src, span_notice("There is an administrative lock on entering the game!")) |
325 | 345 | return FALSE |
326 | 346 | if(!IsJobAvailable(rank)) |
327 | | - src << alert("[rank] is not available. Please try another.") |
| 347 | + to_chat(src, span_warning("[rank] is not available. Please try another.")) |
328 | 348 | return FALSE |
329 | 349 |
|
330 | 350 | spawning = 1 |
|
374 | 394 |
|
375 | 395 | qdel(src) |
376 | 396 |
|
| 397 | +// TGUI Detection - check if we should use TGUI or fallback to legacy browser |
| 398 | +/mob/new_player/proc/use_tgui_latejoin() |
| 399 | + // Check if client exists |
| 400 | + if(!client) |
| 401 | + return FALSE |
| 402 | + |
| 403 | + // Check if TGUI subsystem exists |
| 404 | + if(!SStgui) |
| 405 | + return FALSE |
| 406 | + |
| 407 | + // Try to get a window from the pool to verify availability |
| 408 | + var/datum/tgui_window/window = SStgui.request_pooled_window(src) |
| 409 | + if(!window) |
| 410 | + // Pool exhausted or unavailable - use fallback |
| 411 | + to_chat(src, span_warning("TGUI window pool exhausted, using legacy interface.")) |
| 412 | + return FALSE |
| 413 | + |
| 414 | + // Return window to pool (we were just checking) |
| 415 | + window.release_lock() |
| 416 | + |
| 417 | + return TRUE |
| 418 | + |
| 419 | +// TGUI Interface - modern UI for job selection |
| 420 | +/mob/new_player/ui_interact(mob/user, datum/tgui/ui) |
| 421 | + ui = SStgui.try_update_ui(user, src, ui) |
| 422 | + if(!ui) |
| 423 | + ui = new(user, src, "LateJoin") |
| 424 | + ui.set_autoupdate(FALSE) // Disable autoupdate to prevent dexterity checks |
| 425 | + ui.open() |
| 426 | + |
| 427 | +/mob/new_player/ui_state(mob/user) |
| 428 | + return GLOB.always_state // No distance/access restrictions for new players |
| 429 | + |
| 430 | +/mob/new_player/ui_status(mob/user, datum/ui_state/state) |
| 431 | + // Always allow interaction for new_player, skip all checks |
| 432 | + return UI_INTERACTIVE |
| 433 | + |
| 434 | +/mob/new_player/ui_data(mob/user) |
| 435 | + var/list/data = list() |
| 436 | + |
| 437 | + // Player info |
| 438 | + data["playerName"] = client.prefs.be_random_name ? "friend" : client.prefs.real_name |
| 439 | + |
| 440 | + // Round info |
| 441 | + data["roundDuration"] = DisplayTimeText(world.time - SSticker.round_start_time) |
| 442 | + |
| 443 | + // Evacuation status |
| 444 | + data["isEvacuating"] = evacuation_controller.is_evacuating() |
| 445 | + data["isEvacuated"] = evacuation_controller.has_evacuated() |
| 446 | + |
| 447 | + // Build job list grouped by department |
| 448 | + var/list/departments = list() |
| 449 | + |
| 450 | + for(var/datum/department/dept in SSjob.departments) |
| 451 | + var/list/dept_data = list( |
| 452 | + "name" = dept.name, |
| 453 | + "jobs" = list() |
| 454 | + ) |
| 455 | + |
| 456 | + for(var/datum/job/job in dept.jobs) |
| 457 | + // Count active players |
| 458 | + var/active = 0 |
| 459 | + for(var/mob/M in GLOB.player_list) |
| 460 | + if(M.mind && M.client && M.mind.assigned_role == job.title) |
| 461 | + if(M.client.inactivity <= 10 MINUTES) |
| 462 | + active++ |
| 463 | + |
| 464 | + // Check availability (including experience requirements) |
| 465 | + var/is_available = IsJobAvailable(job.title) |
| 466 | + |
| 467 | + var/list/job_data = list( |
| 468 | + "title" = job.title, |
| 469 | + "currentPositions" = job.current_positions, |
| 470 | + "totalPositions" = job.total_positions, // -1 = unlimited |
| 471 | + "activePlayers" = active, |
| 472 | + "expRequired" = job.exp_requirements, |
| 473 | + "expType" = job.exp_required_type, |
| 474 | + "department" = job.department, |
| 475 | + "available" = is_available, |
| 476 | + "description" = job.description, |
| 477 | + "supervisors" = job.supervisors, |
| 478 | + ) |
| 479 | + |
| 480 | + dept_data["jobs"] += list(job_data) |
| 481 | + |
| 482 | + if(length(dept_data["jobs"])) |
| 483 | + departments += list(dept_data) |
| 484 | + |
| 485 | + data["departments"] = departments |
| 486 | + |
| 487 | + return data |
| 488 | + |
| 489 | +/mob/new_player/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) |
| 490 | + // Don't call parent - we don't need the default checks for new_player |
| 491 | + // Parent would check UI_INTERACTIVE status which may fail for new_player |
| 492 | + |
| 493 | + switch(action) |
| 494 | + if("select_job") |
| 495 | + var/job_title = params["job"] |
| 496 | + if(!job_title) |
| 497 | + return FALSE |
| 498 | + |
| 499 | + // Use shared spawn logic |
| 500 | + LateSpawn(job_title) |
| 501 | + return TRUE |
| 502 | + |
| 503 | + if("close") |
| 504 | + ui.close() |
| 505 | + return TRUE |
| 506 | + |
| 507 | + return FALSE |
| 508 | + |
377 | 509 | /mob/new_player/proc/LateChoices() |
378 | 510 | var/name = client.prefs.be_random_name ? "friend" : client.prefs.real_name |
379 | 511 |
|
|
0 commit comments