/* tws_t5_hud.gsc - minimal edge HUD, toggleable with .hud. */ #include maps\_utility; #include common_scripts\utility; init() { if (GetDvar(#"zombiemode") != "1") return; scripts\sp\tws_t5_common::ensure_config(); scripts\sp\tws_t5_common::register_help_command(".hud", "Toggle core HUD"); level thread round_watcher(); level thread game_over_watcher(); level thread hud_chat_listener(); scripts\sp\tws_t5_common::register_connect_handler(::hud_connect); scripts\sp\tws_t5_common::register_spawn_handler(::hud_start); } hud_chat_listener() { for (;;) { level waittill("say", message, player); if (!isDefined(player) || !isDefined(message)) continue; if (toLower(message) == ".hud") player thread hud_toggle(); } } hud_toggle() { self hud_connect(); self scripts\sp\tws_t5_common::toggle_core_hud(); } // 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 = []; self.tws_hud_match = self hud_text(level.hud_overlay_timer_x, level.hud_overlay_match_y, "left", 1.4, level.hud_c_count, "--:--"); self.tws_hud_round = self hud_text(level.hud_overlay_timer_x, level.hud_overlay_round_y, "left", 1.25, level.hud_c_dim, "--:--"); self hud_text(level.hud_overlay_brand_x, level.hud_overlay_brand_y, "center", 1.1, level.hud_c_dim, "TWS ZOMBIES ยท discord.tws.lol"); self.tws_hud_count = self hud_text(level.hud_overlay_count_x, level.hud_overlay_count_y, "right", 1.5, level.hud_c_count, "--"); self hud_text(level.hud_overlay_count_label_x, level.hud_overlay_count_label_y, "left", 1.05, level.hud_c_dim, "ZOMBIES"); // Independent numeric elements avoid a new config string on every tick. // The readout appears only after a valid damaged-health sample. self.tws_hud_health = self hud_text(level.hud_overlay_health_x, level.hud_overlay_health_y, "right", 1.0, level.hud_c_count, "--"); self.tws_hud_slash = self hud_text(level.hud_overlay_slash_x, level.hud_overlay_health_y, "left", 1.0, level.hud_c_count, "/"); self.tws_hud_maxhealth = self hud_text(level.hud_overlay_maxhealth_x, level.hud_overlay_health_y, "right", 1.0, level.hud_c_count, "--"); self.tws_hud_hp = self hud_text(level.hud_overlay_hp_x, level.hud_overlay_health_y, "left", 1.0, level.hud_c_count, "HP"); scripts\sp\tws_t5_common::set_core_alpha(self, self.tws_hud_health, 0); scripts\sp\tws_t5_common::set_core_alpha(self, self.tws_hud_slash, 0); scripts\sp\tws_t5_common::set_core_alpha(self, self.tws_hud_maxhealth, 0); scripts\sp\tws_t5_common::set_core_alpha(self, self.tws_hud_hp, 0); track = scripts\sp\tws_t5_common::make_core_overlay_shader( self, level.hud_overlay_bar_x, level.hud_overlay_bar_y, level.hud_overlay_bar_width, level.hud_overlay_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_core_overlay_shader( self, level.hud_overlay_bar_x, level.hud_overlay_bar_y, level.hud_overlay_bar_width, level.hud_overlay_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_core_overlay_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 HUD 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); } // Preserve authoritative frozen clocks when the core is shown after game over. 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_overlay_bar_width; bar = self.tws_hud_bar; cur = self.tws_hud_health; slash = self.tws_hud_slash; max_hp = self.tws_hud_maxhealth; hp = self.tws_hud_hp; 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 = scripts\sp\tws_t5_common::make_overlay_width(frac * bar_width); if (width != last_width) { bar SetShader("white", width, level.hud_overlay_bar_height); last_width = width; } bar_alpha = 1; if (frac == 0) bar_alpha = 0; scripts\sp\tws_t5_common::set_core_alpha(self, bar, bar_alpha); 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; hp.color = colour; readout_alpha = 0; if (isDefined(self.health) && current < maximum) readout_alpha = 1; scripts\sp\tws_t5_common::set_core_alpha(self, cur, readout_alpha); scripts\sp\tws_t5_common::set_core_alpha(self, slash, readout_alpha); scripts\sp\tws_t5_common::set_core_alpha(self, max_hp, readout_alpha); scripts\sp\tws_t5_common::set_core_alpha(self, hp, readout_alpha); // Keep health numbers current even at full health or while hidden. 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); } }