/* tws_t5_hud.gsc - fixed upper-right core HUD, visible from connection. */ #include maps\_utility; #include common_scripts\utility; init() { if (GetDvar(#"zombiemode") != "1") return; scripts\sp\tws_t5_common::ensure_config(); level thread round_watcher(); level thread game_over_watcher(); scripts\sp\tws_t5_common::register_connect_handler(::hud_connect); scripts\sp\tws_t5_common::register_spawn_handler(::hud_start); } // Connection construction never seeds clocks or reads player gameplay state. // The first-spawn fallback also handles a spawn before the connect callback. hud_connect() { self endon("disconnect"); if (isDefined(self.tws_hud_elems)) return; self.tws_hud_elems = []; background = scripts\sp\tws_t5_common::make_panel_shader( self, 0, 0, level.hud_panel_width, level.hud_core_height, level.hud_c_panel, level.hud_panel_alpha, 0); self.tws_hud_elems[self.tws_hud_elems.size] = background; self hud_text(level.hud_pad, level.hud_row_header, "left", level.hud_font_small, level.hud_c_name, "TWS ZOMBIES"); self hud_text(level.hud_panel_width - level.hud_pad, level.hud_row_header, "right", level.hud_font_small, level.hud_c_dim, "discord.tws.lol"); self hud_text(level.hud_pad, level.hud_row_timers, "left", level.hud_font_small, level.hud_c_label, "MATCH"); self.tws_hud_match = self hud_text(104, level.hud_row_timers - 2, "right", level.hud_font_mid, level.hud_c_count, "--:--"); self hud_text(114, level.hud_row_timers, "left", level.hud_font_small, level.hud_c_label, "ROUND"); self.tws_hud_round = self hud_text(level.hud_panel_width - level.hud_pad, level.hud_row_timers - 2, "right", level.hud_font_mid, level.hud_c_count, "--:--"); self hud_text(level.hud_pad, level.hud_row_stats, "left", level.hud_font_small, level.hud_c_label, "ZOMBIES"); self.tws_hud_count = self hud_text(67, level.hud_row_stats, "left", level.hud_font_mid, level.hud_c_count, "--"); self hud_text(132, level.hud_row_stats, "left", level.hud_font_small, level.hud_c_label, "HP"); // Independent numeric elements avoid a new config string on every tick. self.tws_hud_health = self hud_text(174, level.hud_row_stats, "right", level.hud_font_mid, level.hud_c_count, "--"); self.tws_hud_slash = self hud_text(181, level.hud_row_stats, "center", level.hud_font_mid, level.hud_c_count, "/"); self.tws_hud_maxhealth = self hud_text(level.hud_panel_width - level.hud_pad, level.hud_row_stats, "right", level.hud_font_mid, level.hud_c_count, "--"); track = scripts\sp\tws_t5_common::make_panel_shader( self, level.hud_pad, level.hud_row_bar, level.hud_bar_width, level.hud_bar_height, (0.18, 0.22, 0.19), 0.8, 10); self.tws_hud_elems[self.tws_hud_elems.size] = track; self.tws_hud_bar = scripts\sp\tws_t5_common::make_panel_shader( self, level.hud_pad, level.hud_row_bar, level.hud_bar_width, level.hud_bar_height, level.hud_c_high, 0, 11); self.tws_hud_elems[self.tws_hud_elems.size] = self.tws_hud_bar; self thread scripts\sp\tws_t5_common::destroy_on_disconnect(self.tws_hud_elems); } hud_text(x, y, align, scale, colour, text) { elem = scripts\sp\tws_t5_common::make_panel_text(self, x, y, align, scale, colour); elem SetText(text); self.tws_hud_elems[self.tws_hud_elems.size] = elem; return elem; } hud_start() { self endon("disconnect"); self hud_connect(); if (isDefined(self.tws_hud_bound) || is_true(level.tws_ended)) return; self.tws_hud_bound = true; self thread hud_timer_row(); self thread hud_zombie_counter(); self thread hud_health_display(); } // Race the map-dependent game-over notifications once at level scope. game_over_watcher() { level thread game_over_via("end_game"); level thread game_over_via("game_ended"); level thread game_over_via("intermission"); } game_over_via(notify_name) { level endon("hud_game_over"); level waittill(notify_name); level.tws_ended = true; level notify("hud_game_over"); } // Pre-set round 1 is seeded on first spawn; only observed transitions seed here. round_watcher() { level endon("hud_game_over"); last = 0; if (isDefined(level.round_number)) last = level.round_number; for (;;) { if (isDefined(level.round_number) && level.round_number != last) { last = level.round_number; level.round_start_time = getTime(); if (!isDefined(level.match_start_time) && level.round_number >= 1) { level.match_start_time = getTime(); level notify("hud_match_started"); } level notify("hud_round_changed"); } wait (0.5); } } hud_timer_row() { self endon("disconnect"); // A visible panel for a waiting newcomer must not start a pre-set round 1. if (!isDefined(level.match_start_time) && isDefined(level.round_number) && level.round_number >= 1) { level.match_start_time = getTime(); level.round_start_time = getTime(); level notify("hud_match_started"); } round = self.tws_hud_round; round SetTimerUp(0); self thread reset_timer_each_round(round); self thread match_timer_bind(self.tws_hud_match); self thread freeze_timers_on_game_over(self.tws_hud_match, round); } reset_timer_each_round(timer) { level endon("hud_game_over"); self endon("disconnect"); for (;;) { level waittill("hud_round_changed"); if (isDefined(timer)) timer SetTimerUp(0); } } // T5 SP only ticks with a zero seed; late joiners retain that engine limitation. match_timer_bind(elem) { self endon("disconnect"); level endon("hud_game_over"); if (!isDefined(level.match_start_time)) level waittill("hud_match_started"); if (isDefined(elem)) elem SetTimerUp(0); } // The panel remains visible at game over, with authoritative frozen clocks. freeze_timers_on_game_over(match_elem, round_elem) { self endon("disconnect"); level waittill("hud_game_over"); if (isDefined(match_elem) && isDefined(level.match_start_time)) match_elem SetText(scripts\sp\tws_t5_common::format_mmss(getTime() - level.match_start_time)); if (isDefined(round_elem) && isDefined(level.round_start_time)) round_elem SetText(scripts\sp\tws_t5_common::format_mmss(getTime() - level.round_start_time)); } hud_zombie_counter() { self endon("disconnect"); level endon("hud_game_over"); count = self.tws_hud_count; last = -1; for (;;) { remaining = get_enemy_count(); if (isDefined(level.zombie_total)) remaining += max(0, level.zombie_total); if (remaining != last) { count SetValue(remaining); last = remaining; } wait (level.hud_rate); } } get_enemy_count() { valid = []; enemies = GetAiSpeciesArray("axis", "all"); for (i = 0; i < enemies.size; i++) { if (is_true(enemies[i].ignore_enemy_count)) continue; if (isDefined(enemies[i].animname)) valid[valid.size] = enemies[i]; } return valid.size; } hud_health_display() { self endon("disconnect"); level endon("hud_game_over"); bar_width = level.hud_bar_width; bar = self.tws_hud_bar; cur = self.tws_hud_health; slash = self.tws_hud_slash; max_hp = self.tws_hud_maxhealth; last_width = -1; last_health = -1; last_max = -1; for (;;) { // Display fallbacks must not alter gameplay health values. maximum = 100; if (isDefined(self.maxhealth) && self.maxhealth > 0) maximum = self.maxhealth; current = 0; if (isDefined(self.health)) current = max(0, self.health); frac = current / (maximum * 1.0); if (frac > 1) frac = 1; width = int(max(frac * bar_width, 1)); if (width != last_width) { bar SetShader("white", width, level.hud_bar_height); last_width = width; } bar.alpha = 1; if (frac == 0) bar.alpha = 0; colour = level.hud_c_high; if (frac <= 0.25) colour = level.hud_c_low; else if (frac <= 0.5) colour = level.hud_c_mid; bar.color = colour; cur.color = colour; slash.color = colour; max_hp.color = colour; // Health numbers remain visible at full health. if (current != last_health) { cur SetValue(current); last_health = current; } if (maximum != last_max) { max_hp SetValue(maximum); last_max = maximum; } wait (level.hud_rate); } }