/* tws_t5_common.gsc — shared config, HUD helpers, connect/spawn/help registries. */ #include maps\_utility; #include common_scripts\utility; main() { precacheShader("white"); } init() { if (GetDvar(#"zombiemode") != "1") return; common_config(); register_help_command(".help", "List available commands"); level thread service_game_over_via("end_game"); level thread service_game_over_via("game_ended"); level thread service_game_over_via("intermission"); level thread help_chat_listener(); level thread on_player_connect_dispatch(); } common_config() { if (isDefined(level.hud_x)) return; // Retain legacy panel coordinates for existing helper consumers. level.hud_x = 10; level.hud_y = 30; // Retain the legacy fields for consumers of the original common config. // The panel renderers below use dedicated row/column offsets instead. level.hud_row_match = level.hud_y; level.hud_row_round = level.hud_y + 15; level.hud_row_name = level.hud_y + 32; level.hud_row_count = level.hud_y + 54; level.hud_col_count = level.hud_x + 90; level.hud_col_name = level.hud_x + 60; level.hud_col_health = level.hud_x + 120 + 12; level.hud_health_threshold = 1.0; level.hud_panel_right = 10; level.hud_panel_top = 20; // Fullscreen uses a 640x480 frame. Convert the panel's 480-height units // to that frame at the 16:9 reference aspect (640 / (480 * 16 / 9)). level.hud_panel_x_scale = 0.75; level.hud_panel_width = 220; level.hud_core_height = 100; level.hud_service_y = 108; level.hud_service_height = 124; level.hud_pad = 10; level.hud_row_header = 10; level.hud_row_timers = 34; level.hud_row_stats = 59; level.hud_row_bar = 80; level.hud_row_afk = 86; level.hud_font_small = 1.1; level.hud_font_large = 1.6; level.hud_font_mid = 1.25; level.hud_font_afk = 1.0; level.hud_bar_width = 200; level.hud_bar_height = 4; level.hud_rate = 0.1; level.hud_c_panel = (0.07, 0.10, 0.08); level.hud_panel_alpha = 0.72; level.hud_c_label = (0.72, 0.77, 0.74); level.hud_c_dim = (0.65, 0.70, 0.67); level.hud_c_count = (0.93, 0.96, 0.94); level.hud_c_name = (0.65, 0.85, 0.70); level.hud_c_high = (0.65, 0.85, 0.70); level.hud_c_mid = (1, 0.8, 0); level.hud_c_low = (1, 0.2, 0.2); level.hud_c_afk = (1, 0.8, 0.2); // Minimal overlays use absolute coordinates in the fullscreen 640x480 frame. level.hud_overlay_timer_x = 6; level.hud_overlay_match_y = 8; level.hud_overlay_round_y = 30; level.hud_overlay_brand_x = 320; level.hud_overlay_brand_y = 8; level.hud_overlay_count_x = 300; level.hud_overlay_count_y = 440; level.hud_overlay_count_label_x = 306; level.hud_overlay_count_label_y = 446; level.hud_overlay_bar_x = 242; level.hud_overlay_bar_y = 466; level.hud_overlay_bar_width = 156; level.hud_overlay_bar_height = 4; level.hud_overlay_health_y = 460; level.hud_overlay_health_x = 414; level.hud_overlay_slash_x = 418; level.hud_overlay_maxhealth_x = 445; level.hud_overlay_hp_x = 449; level.hud_overlay_afk_x = 320; level.hud_overlay_afk_y = 421; level.hud_service_left = 432; level.hud_service_right = 633; level.hud_service_description_x = 538; level.hud_service_title_y = 54; level.hud_service_help_y = 80; level.hud_service_help_pitch = 24; level.hud_service_help_rows = 9; level.hud_service_record_y = 36; level.hud_service_holders_y = 60; level.hud_service_holders_pitch = 18; if (!isDefined(level.tws_spawn_handlers)) level.tws_spawn_handlers = []; } ensure_config() { if (!isDefined(level.hud_x)) common_config(); } register_spawn_handler(fn) { if (!isDefined(level.tws_spawn_handlers)) level.tws_spawn_handlers = []; level.tws_spawn_handlers[level.tws_spawn_handlers.size] = fn; } // Connection handlers also serve newcomers still waiting in spectator mode. register_connect_handler(fn) { if (!isDefined(level.tws_connect_handlers)) level.tws_connect_handlers = []; level.tws_connect_handlers[level.tws_connect_handlers.size] = fn; } // Modules register their own entries so help only lists installed commands. register_help_command(command, description) { if (!isDefined(level.tws_help_commands)) level.tws_help_commands = []; entry = spawnStruct(); entry.command = command; entry.description = description; level.tws_help_commands[level.tws_help_commands.size] = entry; } // Listen at level scope so newcomers can ask for help before their first spawn. help_chat_listener() { for (;;) { level waittill("say", message, player); if (!isDefined(player) || !isDefined(message)) continue; command = toLower(message); if (command == ".help" || command == ".commands") player show_help(); } } // A single per-player controller owns service priority, pages and expiry. show_help() { if (isDefined(level.tws_service_ended)) return; self service_ensure(); state = self.tws_service; service_advance(state, getTime()); state.generation++; state.help = service_request("help", service_page_count(state.commands.size, level.hud_service_help_rows), state.generation); service_render(state); } // An undefined snapshot is the explicit unavailable view for manual requests. show_record(snapshot) { if (isDefined(level.tws_service_ended)) return; self service_ensure(); state = self.tws_service; service_advance(state, getTime()); count = 0; if (isDefined(snapshot)) count = snapshot.lines.size; state.generation++; state.record = service_request("record", service_page_count(count, 3), state.generation); state.record.snapshot = snapshot; service_render(state); } service_page_count(count, rows) { return int((max(count, 1) + rows - 1) / rows); } service_request(kind, pages, generation) { request = spawnStruct(); request.kind = kind; request.generation = generation; request.page = 0; request.pages = pages; request.remaining = 12000; return request; } service_current(state) { if (isDefined(state.help)) return state.help; return state.record; } service_advance(state, now) { elapsed = max(0, now - state.updated_at); state.updated_at = now; request = service_current(state); if (!isDefined(request) || state.closed) return; // Only the visible request consumes time. Help leaves the record untouched. request.remaining -= elapsed; if (request.remaining > 0) return; request.page++; request.remaining = 12000; if (request.page >= request.pages) { if (request.kind == "help") state.help = undefined; else state.record = undefined; } } // Stable order is independent of module initialization order; absent commands // stay absent. Unknown future commands follow the built-in ones in registry order. help_rank(command) { if (command == ".help") return 0; if (command == ".hud") return 1; if (command == ".afk") return 2; if (command == ".back") return 3; if (command == ".join") return 4; if (command == ".leaders") return 5; if (command == ".b") return 6; if (command.size >= 3 && getSubStr(command, 0, 3) == ".d ") return 7; if (command.size >= 3 && getSubStr(command, 0, 3) == ".w ") return 8; return 9; } help_entries() { entries = []; if (!isDefined(level.tws_help_commands)) return entries; for (rank = 0; rank <= 9; rank++) { for (i = 0; i < level.tws_help_commands.size; i++) { entry = level.tws_help_commands[i]; if (help_rank(entry.command) == rank) entries[entries.size] = entry; } } return entries; } help_command_label(command) { return command; } help_description(entry) { rank = help_rank(entry.command); if (rank == 0) return "Show commands"; if (rank == 1) return "Toggle core HUD"; if (rank == 2) return "Protect & freeze"; if (rank == 3) return "Return to play"; if (rank == 4) return "Join as newcomer"; if (rank == 5) return "View records"; if (rank == 6) return "Bank balance"; if (rank == 7) return "Deposit"; if (rank == 8) return "Withdraw"; return entry.description; } service_ensure() { if (isDefined(self.tws_service)) return; ensure_config(); state = spawnStruct(); state.elems = []; state.commands = help_entries(); state.help = undefined; state.record = undefined; state.closed = false; state.updated_at = getTime(); state.rendered = false; state.generation = 0; state.last_generation = 0; state.last_page = -1; state.record_text = []; state.help_page = -1; state.rows = []; state.command_rows = []; state.description_rows = []; state.view_kind = undefined; self.tws_service = state; self thread service_loop(state); self thread service_disconnect(state); self thread service_end_game(state); } // Allocate only the active view. Alpha-zero elements still occupy HUD slots. service_view_create(state, request) { service_view_destroy(state); state.view_kind = request.kind; state.view_paged = request.pages > 1; if (state.view_paged) state.page_counter = service_text(self, state, level.hud_service_right, level.hud_service_title_y, "right", 1.0, "", level.hud_c_dim); if (request.kind == "help") { state.help_title = service_text(self, state, level.hud_service_left, level.hud_service_title_y, "left", 1.1, "COMMANDS", level.hud_c_label); rows = state.commands.size; if (rows > level.hud_service_help_rows) rows = level.hud_service_help_rows; // Explicit lines also work on T5, which can collapse multiline strings. for (i = 0; i < rows; i++) { y = level.hud_service_help_y + i * level.hud_service_help_pitch; state.command_rows[i] = service_text(self, state, level.hud_service_left, y, "left", 1.0, "", level.hud_c_count); state.description_rows[i] = service_text(self, state, level.hud_service_description_x, y, "left", 1.0, "", level.hud_c_label); } return; } state.record_title = service_text(self, state, level.hud_service_right, level.hud_service_record_y, "right", 1.1, "", level.hud_c_count); for (i = 0; i < 3; i++) state.rows[i] = service_text(self, state, level.hud_service_right, level.hud_service_holders_y + i * level.hud_service_holders_pitch, "right", 1.05, "", level.hud_c_label); if (isDefined(state.page_counter)) state.page_counter.y = level.hud_service_holders_y + 3 * level.hud_service_holders_pitch; } service_view_destroy(state) { for (i = 0; i < state.elems.size; i++) { if (isDefined(state.elems[i])) state.elems[i] Destroy(); } state.elems = []; state.view_kind = undefined; state.view_paged = undefined; state.page_counter = undefined; state.help_title = undefined; state.command_rows = []; state.description_rows = []; state.record_title = undefined; state.rows = []; state.page_text = undefined; state.record_header_text = undefined; state.record_text = []; state.help_page = -1; } service_text(player, state, x, y, align, scale, text, colour) { elem = make_overlay_text(player, x, y, align, scale, colour); elem.alpha = 0; elem.archived = false; if (text != "") elem SetText(text); state.elems[state.elems.size] = elem; return elem; } service_render(state) { if (state.closed) return; request = service_current(state); page = -1; generation = 0; if (isDefined(request)) { page = request.page; generation = request.generation; } if (state.rendered && state.last_generation == generation && state.last_page == page) return; state.rendered = true; state.last_generation = generation; state.last_page = page; if (!isDefined(request)) { service_view_destroy(state); return; } paged = request.pages > 1; if (!isDefined(state.view_kind) || state.view_kind != request.kind || state.view_paged != paged) service_view_create(state, request); for (i = 0; i < state.elems.size; i++) state.elems[i].alpha = 0; if (paged) { page_text = "" + (page + 1) + " / " + request.pages; if (!isDefined(state.page_text) || state.page_text != page_text) { state.page_counter SetText(page_text); state.page_text = page_text; } state.page_counter.alpha = 1; } if (request.kind == "help") { state.help_title.alpha = 1; for (i = 0; i < state.command_rows.size; i++) { index = page * level.hud_service_help_rows + i; if (index >= state.commands.size) continue; if (state.help_page != page) { entry = state.commands[index]; state.command_rows[i] SetText(help_command_label(entry.command)); state.description_rows[i] SetText(help_description(entry)); } state.command_rows[i].alpha = 1; state.description_rows[i].alpha = 1; } state.help_page = page; return; } header = "Map record unavailable"; snapshot = request.snapshot; if (isDefined(snapshot)) { header = "No recorded high round yet"; if (snapshot.round > 0) header = "Map record | Round " + snapshot.round; } if (!isDefined(state.record_header_text) || state.record_header_text != header) { state.record_title SetText(header); state.record_header_text = header; } state.record_title.alpha = 1; if (!isDefined(snapshot) || snapshot.round == 0) return; for (i = 0; i < 3; i++) { index = page * 3 + i; if (index < snapshot.lines.size) { text = snapshot.lines[index]; if (!isDefined(state.record_text[i]) || state.record_text[i] != text) { state.rows[i] SetText(text); state.record_text[i] = text; } state.rows[i].alpha = 1; } } } service_loop(state) { self endon("disconnect"); level endon("tws_service_game_over"); for (;;) { service_advance(state, getTime()); service_render(state); wait 0.1; } } service_disconnect(state) { self waittill("disconnect"); service_destroy(state); } service_end_game(state) { self endon("disconnect"); level waittill("tws_service_game_over"); service_destroy(state); } service_destroy(state) { if (state.closed) return; state.closed = true; state.help = undefined; state.record = undefined; service_view_destroy(state); } service_game_over_via(event) { level endon("tws_service_game_over"); level waittill(event); level.tws_service_ended = true; level notify("tws_service_game_over"); } // The only connected listener; modules use the shared registries. on_player_connect_dispatch() { for (;;) { level waittill("connected", player); player thread on_player_spawned_dispatch(); player thread on_player_connected_dispatch(); } } on_player_connected_dispatch() { self endon("disconnect"); // Leave the connection handshake before invoking optional modules. wait 0.05; if (!isDefined(level.tws_connect_handlers)) return; for (i = 0; i < level.tws_connect_handlers.size; i++) self thread [[ level.tws_connect_handlers[i] ]](); } // handlers own their own self endon("disconnect") on_player_spawned_dispatch() { self endon("disconnect"); self waittill("spawned_player"); if (!isDefined(level.tws_spawn_handlers)) return; for (i = 0; i < level.tws_spawn_handlers.size; i++) self thread [[ level.tws_spawn_handlers[i] ]](); } make_text_elem(player, x, y, align_x, scale, colour) { elem = NewClientHudElem(player); elem.horzAlign = "left"; elem.vertAlign = "top"; elem.alignX = align_x; elem.alignY = "top"; elem.x = x; elem.y = y; elem.foreground = 1; // Sub-1.0 text can render oversized on clients; keep a safe minimum. elem.fontScale = max(1.0, scale); elem.alpha = 1; elem.color = colour; // black glow reads as an outline against bright geometry elem.glowColor = (0, 0, 0); elem.glowAlpha = 0.6; return elem; } make_shader_elem(player, x, y) { elem = NewClientHudElem(player); elem.horzAlign = "left"; elem.vertAlign = "top"; elem.alignX = "left"; elem.alignY = "top"; elem.x = x; elem.y = y; elem.foreground = 1; elem.alpha = 1; return elem; } // Absolute fullscreen overlays leave stock HUD areas and the center clear. make_overlay_text(player, x, y, align_x, scale, colour) { elem = make_text_elem(player, x, y, align_x, scale, colour); elem.horzAlign = "fullscreen"; elem.vertAlign = "fullscreen"; elem.sort = 20; elem.archived = true; return elem; } make_overlay_shader(player, x, y, width, height, colour, alpha, sort) { elem = make_shader_elem(player, x, y); elem.horzAlign = "fullscreen"; elem.vertAlign = "fullscreen"; elem.color = colour; elem.alpha = alpha; elem.sort = sort; elem.archived = true; elem SetShader("white", make_overlay_width(width), int(max(1, height))); return elem; } make_overlay_width(width) { // Native max() returns a float; shader dimensions must be integers. return int(max(1, width)); } make_core_overlay_text(player, x, y, align_x, scale, colour) { elem = make_overlay_text(player, x, y, align_x, scale, colour); register_core_elem(player, elem); return elem; } make_core_overlay_shader(player, x, y, width, height, colour, alpha, sort) { elem = make_overlay_shader(player, x, y, width, height, colour, alpha, sort); register_core_elem(player, elem); return elem; } // Panel x/y are local to the upper-right core origin. Fullscreen bypasses the // engine safe area. Horizontal conversion preserves the original size at 16:9; // other aspects retain the right-edge anchor with proportional horizontal size. make_panel_text(player, x, y, align_x, scale, colour) { elem = make_text_elem(player, 640 + (x - level.hud_panel_width - level.hud_panel_right) * level.hud_panel_x_scale, level.hud_panel_top + y, align_x, scale, colour); elem.horzAlign = "fullscreen"; elem.vertAlign = "fullscreen"; elem.sort = 20; elem.archived = true; return elem; } make_panel_shader(player, x, y, width, height, colour, alpha, sort) { elem = make_shader_elem(player, 640 + (x - level.hud_panel_width - level.hud_panel_right) * level.hud_panel_x_scale, level.hud_panel_top + y); elem.horzAlign = "fullscreen"; elem.vertAlign = "fullscreen"; elem.color = colour; elem.alpha = alpha; elem.sort = sort; elem.archived = true; elem SetShader("white", make_panel_width(width), height); return elem; } // Native shader widths are integers; dynamic fills use the same conversion. make_panel_width(width) { // max() returns a float even for integer inputs; cast its final result. return int(max(1, width * level.hud_panel_x_scale)); } // Core elements share a visibility mask; services keep their independent alpha. make_core_text(player, x, y, align_x, scale, colour) { elem = make_panel_text(player, x, y, align_x, scale, colour); register_core_elem(player, elem); return elem; } make_core_shader(player, x, y, width, height, colour, alpha, sort) { elem = make_panel_shader(player, x, y, width, height, colour, alpha, sort); register_core_elem(player, elem); return elem; } register_core_elem(player, elem) { live = []; if (isDefined(player.tws_core_hud_elems)) { for (i = 0; i < player.tws_core_hud_elems.size; i++) { old = player.tws_core_hud_elems[i]; if (isDefined(old)) live[live.size] = old; } } live[live.size] = elem; player.tws_core_hud_elems = live; set_core_alpha(player, elem, elem.alpha); } set_core_alpha(player, elem, alpha) { if (!isDefined(elem)) return; elem.tws_core_alpha = alpha; elem.alpha = alpha; if (isDefined(player.tws_hud_hidden) && player.tws_hud_hidden) elem.alpha = 0; } toggle_core_hud() { hidden = false; if (isDefined(self.tws_hud_hidden)) hidden = self.tws_hud_hidden; self.tws_hud_hidden = !hidden; if (!isDefined(self.tws_core_hud_elems)) return; live = []; for (i = 0; i < self.tws_core_hud_elems.size; i++) { elem = self.tws_core_hud_elems[i]; if (!isDefined(elem)) continue; set_core_alpha(self, elem, elem.tws_core_alpha); live[live.size] = elem; } self.tws_core_hud_elems = live; } destroy_on_disconnect(elems) { self waittill("disconnect"); for (i = 0; i < elems.size; i++) { if (isDefined(elems[i])) elems[i] Destroy(); } } int_dvar_or(name, fallback) { raw = GetDvar(name); if (raw == "") return fallback; return int(raw); } format_mmss(elapsed_ms) { total_secs = int(elapsed_ms / 1000); if (total_secs < 0) total_secs = 0; mins = int(total_secs / 60); secs = total_secs - (mins * 60); sec_str = "" + secs; if (secs < 10) sec_str = "0" + secs; return "" + mins + ":" + sec_str; }