Simplify T5 and T6 HUD overlays and service displays

This commit is contained in:
2026-10-15 21:07:10 -04:00
parent 929bf0354b
commit 2b5b1955bc
15 changed files with 1934 additions and 643 deletions
+64 -31
View File
@@ -9,7 +9,7 @@ telemetry, leaderboard and bank modules; neither includes direct-database module
| --- | --- | --- |
| Paths | `maps/mp/_utility`, `scripts/zm/tws_t6_common` | T6 namespaces and requested filenames |
| Loading | No T5 `zombiemode` dvar gate; each module has `main()` and `init()` | Zombies-only folder loading; avoid r5346 missing-main warnings |
| HUD positioning | Fullscreen upper-right anchors, fixed core and service-window geometry | Keep clear of top-edge overlays and stock scores/ammo without moving the core for services |
| HUD positioning | Fullscreen X/Y anchors, direct normalized `640 × 480` coordinates, unboxed core and service area | Bypass safe-area insets; validate Plutonium tag clearance and font fit at each aspect ratio |
| HUD rendering | Precached white shader, local max-health fallback, floating-point ratio, `getPlayers()` | T6 rendering/state handling without changing player max health |
| Enemy count | Stock `_zm_utility::get_current_zombie_count()` plus queued zombies | Uses the map's zombie team and enemy filters |
| Timers | Seed elapsed time for late joiners; stop round watcher/reset on game over | T6 timer behavior; keep frozen timers frozen |
@@ -26,30 +26,56 @@ Common also listens for `.help` through the native `say` notification, independe
of first-spawn initialization so newcomers can view commands while spectating.
Each loaded command module registers its command and description with
`register_help_command()` during `init()`. Lazy registry initialization preserves
registrations regardless of module initialization order. Common owns one reusable
service window beneath the fixed core. Help uses four commands per page in stable
order, with 12 seconds per page and automatic dismissal after the final page.
`.help` / `.commands` restart at page one. Help pauses record page time, and a
`.leaders` request during help replaces the pending replay from page one. Service
state prevents old requests from closing newer displays. Disconnect and game-end
cleanup are independent of page timing. The command handlers retain their
existing listeners, and help/records work without the HUD module.
registrations regardless of module initialization order. Common owns one
unboxed upper-right service area. Help shows all nine installed commands in
explicit command/description rows; extra registered commands use pages of nine
rows. Pages last 12 seconds, with automatic dismissal after the final page and
a counter only for multi-page lists. `.help` / `.commands` restart at page one.
Help pauses record page time, and a `.leaders` request during help replaces
the pending replay from page one. Service state prevents old requests from
closing newer displays. Disconnect and game-end cleanup are independent of
page timing. Command handlers retain their existing listeners, and help/records
work without the HUD module.
Help reuses two multiline text columns and a muted command-column backdrop.
The service has 19 nonarchived elements independent of command/page count;
core + AFK use 15 + 1 archived elements. Hidden elements still occupy slots.
Both engines use explicit help rows; T5 no longer relies on newline rendering,
and T6 has the same row geometry. Common allocates only the active service
view: 19 nonarchived elements for nine-command help (20 with a multi-page
counter), or four for records (five with a multi-page counter). Closing the
service or switching kinds destroys the view; idle holds no elements. Pages
reuse the active elements, separately from controller and pending-request state.
Core + AFK use 11 + 1 archived elements. Hidden elements still occupy slots.
The [T6 player HUD structure](https://github.com/skiff/t6-gsc-helper/blob/50ff6cb7397c9376894a195f48118d1743812d1d/src/game/structs.hpp#L184)
has 31 slots in each bucket; it does not establish current Plutonium stock HUD
usage. Keep the live overlap/element-budget checks alongside the portable checks.
The core is created on connection for waiting spectators with placeholders, while
timer updates retain the existing spawn/round lifecycle. Connection-time creation
must not seed the match clock. Current/maximum HP remains visible at full health;
the former co-op name row is removed and AFK uses a reserved footer. Opening a
service never changes core position or height. HUD `1.2.0`, leaderboard `1.1.0`
and AFK `1.0.1` require matching common `1.2.0+`; deploy the changed scripts
together and restart the map. This introduces no native-plugin/backend dependency
or full server-process restart requirement.
The core is created on connection for waiting spectators with placeholders,
while timer updates retain the existing spawn/round lifecycle. Connection-time
creation must not seed the match clock. The unboxed layout has timers at the
upper left, branding at top center and zombies/health at bottom center. HP
numbers appear to the right of the bar only while injured; AFK sits above the
zombie count. Services never reposition the core. Records are right aligned,
with `Map record · Round N` followed by up to three bounded holder lines, no
footer and no single-page counter. Their 12-second page timing is unchanged.
HUD `1.4.0` and AFK `1.2.0` require matching common `1.4.0+`; update all three
where installed and restart the map. Leaderboard `1.1.0` remains compatible.
Overlay helpers use direct normalized `640 × 480` coordinates. Dynamic health
fills use `make_overlay_width()` like their tracks. Apply `int()` after `max()`
in that helper: native `max()` returns a float, but `SetShader` requires an
integer width. Legacy panel helpers retain their `0.75` horizontal conversion
for existing consumers. Font scale is at least `1.0`. This introduces no
Plutonium/native-plugin/backend dependency or full server-process restart
requirement.
HUD owns the exact, case-insensitive `.hud` chat command and registers it in
help only while installed. Common's core wrappers preserve each element's
intended alpha behind a per-player visibility mask; health and AFK changes use
`set_core_alpha()`. Data/timers keep updating while hidden and freeze at game
over. Help, records, stock HUD and AFK protection are unaffected. Visibility
defaults on, survives death/respawn during the connection/map, and resets on
reconnect or map restart. All nine installed commands use one help page,
ordered `.help`, `.hud`, `.afk`, `.back`, `.join`, `.leaders`, `.b`, `.d`, `.w`.
Bank commands display the full `<amount|all>` syntax.
AFK again listens to the native `say` notification, so it no longer needs plugin
chat registration. The additional T6 controls are only held while AFK; `.back`
@@ -164,10 +190,11 @@ command regressions. Compiler success does not certify runtime linking.
Live checks after deploying:
1. Join, late-join, die/respawn, and reconnect: one core per player, fixed
upper-right position, spectator placeholders without an early clock start,
visible full/damaged HP, health/Juggernog changes, zombies remaining, and
timers frozen at game over. Test safe-area settings and different resolutions.
1. Join, late-join, die/respawn, and reconnect: one unboxed core per player,
upper-left timers, top-center branding, bottom-center zombies/health and
spectator placeholders without an early clock start. Check damage-only HP,
health/Juggernog changes, AFK placement and timers frozen at game over.
Test safe-area settings and different resolutions against stock HUD elements.
2. `.afk` blocks movement, shooting and grenades; `.back` restores them. Try rapid
toggles, a round transition, death/respawn and game over.
3. Lock fires at the threshold and clears on game over/restart. HUD must be installed.
@@ -176,20 +203,26 @@ Live checks after deploying:
endpoint. Check server ID/player count, same-round joins and an unreachable API.
5. Repeat on the actual server maps, especially afterlife and special-enemy rounds.
6. Type `.help`, `.HELP` and `.commands` before first spawn, during play and
while AFK: four commands per page, only on the requesting player's screen,
without moving or resizing the core. Verify 12 seconds per page and dismissal
after the final page; repeat on page two to restart page one. Remove optional
modules and restart the map to confirm their commands are absent. Restore
the modules and confirm their command behavior is unchanged.
while AFK: one page of nine commands with all modules installed, full bank
syntax and no page counter, only on the requesting player's screen. Verify
dismissal after 12 seconds and repeat-to-restart behavior. Add custom
registered commands to exercise the nine-row page limit and page counter.
Remove optional modules and restart the map to confirm their commands are
absent. Restore them and confirm command behavior is unchanged.
7. Interrupt a record with help and check that its remaining page time resumes.
Repeat `.leaders` during help; only the latest replay should start from page
one after help closes. Check long names across pages, no records and
unavailable records. Reopen after expiry, disconnect/reconnect and end the
game during a service; verify no duplicates or leftovers. Repeat without HUD.
8. Toggle `.hud` before spawning, while playing/AFK and after game over. Verify
the core and AFK indicator hide privately while help/records remain visible.
Show it after taking damage or changing AFK state while hidden: current
values and intended alpha should return. Confirm updates continue hidden,
death/respawn keeps the choice, and reconnect/map restart restores visibility.
The stock T6 [HUD initialization](https://raw.githubusercontent.com/plutoniummod/t6-scripts/main/ZM/Core/maps/mp/gametypes_zm/_hud.gsc)
sets a nominal font height of 12; the corresponding
[font helper](https://raw.githubusercontent.com/plutoniummod/t6-scripts/main/ZM/Core/maps/mp/gametypes_zm/_hud_util.gsc)
uses `int(level.fontheight * fontscale)` for its height. The portable HUD checks
use that nominal box to prevent holder-row and AFK-footer overlaps. Exact glyph
extents and multiline spacing still need live validation, especially on T5.
use that nominal box to prevent holder-row, help-row and AFK overlaps. Exact
glyph extents still need live validation, especially on T5.
+50 -22
View File
@@ -8,8 +8,8 @@ private server token.
| File | Role | Dependencies |
| --- | --- | --- |
| `tws_t6_common.gsc` | Shared HUD values, helpers, paginated `.help`, service window, connection and first-spawn dispatchers | Stock T6 |
| `tws_t6_hud.gsc` | Fixed upper-right core: branding, timers, zombie count, current/maximum HP and health bar | Common 1.2.0+ |
| `tws_t6_afk.gsc` | `.afk` / `.back`, protection, movement and weapon blocking | Common 1.2.0+ |
| `tws_t6_hud.gsc` | Unboxed timers, branding, zombies/health and per-player `.hud` visibility toggle | Common 1.4.0+ |
| `tws_t6_afk.gsc` | `.afk` / `.back`, protection, movement and weapon blocking | Common 1.4.0+ |
| `tws_t6_lock.gsc` | Lock at round 20; clear password on game over | Common, HUD |
| `tws_t6_telemetry.gsc` | Async queued game reports (game_start/heartbeat/round_change/game_end) | tws-t6-gsc-utils |
| `tws_t6_join.gsc` | `.join` chat command lets a spectator newcomer enter mid-match once | Common |
@@ -70,22 +70,43 @@ populates that field before the `connected` notification. Follow the shared
## Core HUD and service window
The fixed upper-right core shows `TWS ZOMBIES`, `discord.tws.lol`, side-by-side
match/round timers, zombie count, current/maximum HP and a full-width health bar.
HP stays visible at full health. A reserved footer holds the AFK indicator;
there is no co-op name row. The core is created once per connection, including
for spectator newcomers with placeholders until values are available. Creation
does not seed the match clock. It persists across death and respawn.
Clocks, health and zombie-count displays freeze at game over. Health fallbacks
only affect the display and never modify gameplay max health.
The unboxed core puts match time above round time at the upper left,
`TWS ZOMBIES · discord.tws.lol` at the top center, and zombies remaining above a
slim health bar at the bottom center. Current/maximum HP appears to the right
of the bar only while injured; it disappears at full health. The AFK indicator
sits above the zombie count. There are no panel backgrounds or timer labels.
Fullscreen X/Y anchors use the normalized `640 × 480` viewport directly and
bypass safe-area insets. Font scale is at least `1.0`; verify text fit, stock
HUD overlap and Plutonium tag clearance at each aspect ratio in-game.
Common provides one fixed-height service window beneath the core for help or
records. Services never move or resize the core. They remain private and work
without the HUD module installed. See the [shared layout and validation guide](../../README.md#core-hud-and-service-window-t5-and-t6).
The core is created once per connection, including for spectator newcomers
with placeholders until values are available. Creation does not seed the match
clock. It persists across death and respawn. Clocks, health and zombie-count
displays freeze at game over. Health fallbacks only affect the display and
never modify gameplay max health.
Install HUD `1.2.0`, leaderboard `1.1.0` and AFK `1.0.1` with common `1.2.0+`.
Update the installed scripts together and restart the map; this change requires
no new plugin/backend dependency or full server-process restart.
The exact, case-insensitive `.hud` command toggles your core and AFK indicator.
It defaults visible, keeps your choice across death/respawn on the current
connection/map, and resets visible on reconnect or map restart. Timers and data
continue updating while hidden; game-over values remain frozen. Help, records
and the stock HUD are unaffected. `.hud` requires the HUD module.
Common provides one unboxed upper-right service area below the Plutonium tag.
Records are right aligned, with `Map record · Round N` followed by up to three
holder lines; there is no replay footer or single-page counter. Help displays
the full nine-command list in aligned rows. Services remain private, leave the
core in place and work without the HUD module installed. See the
[shared layout and validation guide](../../README.md#core-hud-and-service-window-t5-and-t6).
Only the active service view holds HUD elements: 19 for nine-command help
(20 with a multi-page counter), or four for records (five with a multi-page
counter). Pages reuse elements; switching views or closing the service releases
them. Idle services hold no HUD elements, while the controller retains pending
requests and interruption/resume behavior. Core + AFK use 11 + 1 elements.
HUD `1.4.0` and AFK `1.2.0` require common `1.4.0+`. Update common, HUD and AFK
together where installed, then restart the map. Leaderboard `1.1.0` remains
compatible. This change requires no new Plutonium/plugin/backend dependency or
full server-process restart.
## Map record announcements
@@ -110,14 +131,17 @@ apply to both versions. All T6 script filenames use the `tws_t6_` prefix.
Type `.help` or `.commands` in chat to see the commands supplied by the loaded TWS modules and
a short description of each. The list is shown only to the requesting player,
including spectators who have not spawned. Commands are case-insensitive.
Help uses the service window below the core. It shows four commands per page,
advances automatically every 12 seconds and closes after the last page. The
panel retains its height on short pages. Typing either command again returns to
page one with a fresh timer.
Help uses the unboxed upper-right service area. All nine installed commands
fit in one list with command and description columns. Additional registered
commands use pages of nine rows, with a counter only for multi-page lists.
Pages advance every 12 seconds and close after the last page. Typing either
command again returns to page one with a fresh timer. Both engines use
explicit row positions so T5 does not concatenate newline-separated text.
| Command | Description |
| --- | --- |
| `.help`, `.commands` | List available commands |
| `.hud` | Hide or show your core HUD and AFK indicator (HUD module required) |
| `.afk` | Pause with protection |
| `.back` | Return from AFK |
| `.join` | Join mid-round (newcomers only) |
@@ -126,11 +150,14 @@ page one with a fresh timer.
| `.d <amount\|all>` | Deposit points (bank module required) |
| `.w <amount\|all>` | Withdraw points (bank module required) |
AFK and Join register their commands during `init()` through
Command modules register their commands during `init()` through
`tws_t6_common::register_help_command(command, description)`. If a module is
omitted, its commands are omitted from `.help`. Commands follow the order above,
independent of initialization order. Register new commands alongside their
module's initialization so the list stays current.
With all command modules installed, help shows one page of nine commands,
including the full `.d <amount|all>` / `.w <amount|all>` syntax. Removing HUD
removes `.hud` from the list.
The enabled leaderboard module registers `.leaders`, which replays the latest
cached map record privately. Repeating it restarts the record from the first page;
help pauses the record's remaining page time and resumes it after the final
@@ -140,7 +167,8 @@ help page. `.leaders` during help replaces the pending record replay from page o
- HUD values live in `tws_t6_common::common_config()`. That function and
`ensure_config()` came from T5; they initialize in-script values and do not
load a config file. Core and service panels use fullscreen upper-right anchors.
load a config file. Core and service overlays use fullscreen X/Y anchors in
normalized `640 × 480` coordinates to bypass safe-area insets.
- AFK uses T5's native `level waittill("say", message, player)` listener.
`.back` restores movement and weapons immediately, then ends protection after
two seconds. The T6 control/weapon fix remains in place.
+7 -7
View File
@@ -48,11 +48,11 @@ afk_spawn()
self.hud_afk_on = false;
self.tws_afk_protected = false;
self.tws_afk_controls_locked = false;
elem = scripts\zm\tws_t6_common::make_panel_text(self,
level.hud_pad, level.hud_row_afk, "left",
level.hud_font_afk, level.hud_c_afk);
elem = scripts\zm\tws_t6_common::make_core_overlay_text(self,
level.hud_overlay_afk_x, level.hud_overlay_afk_y, "center",
1.0, level.hud_c_afk);
elem SetText("[AFK]");
elem.alpha = 0;
scripts\zm\tws_t6_common::set_core_alpha(self, elem, 0);
self.hud_afk_elem = elem;
elems = [];
elems[0] = elem;
@@ -84,7 +84,7 @@ afk_enter()
self.ignoreme = true;
self enableInvulnerability();
self afk_lock_controls();
self.hud_afk_elem.alpha = 1;
scripts\zm\tws_t6_common::set_core_alpha(self, self.hud_afk_elem, 1);
self iPrintLnBold("AFK - type .back to return");
}
@@ -95,7 +95,7 @@ afk_exit()
return;
self.hud_afk_on = false;
self afk_restore_controls();
self.hud_afk_elem.alpha = 0;
scripts\zm\tws_t6_common::set_core_alpha(self, self.hud_afk_elem, 0);
self.tws_afk_generation++;
generation = self.tws_afk_generation;
self iPrintLnBold("Back in the fight");
@@ -110,7 +110,7 @@ afk_release()
self.hud_afk_on = false;
self.tws_afk_protected = false;
self afk_restore_controls();
self.hud_afk_elem.alpha = 0;
scripts\zm\tws_t6_common::set_core_alpha(self, self.hud_afk_elem, 0);
// Stock scripts own protection during laststand/intermission/death.
if (scripts\zm\tws_t6_common::player_active(self))
{
+288 -123
View File
@@ -25,8 +25,7 @@ common_config()
if (isDefined(level.hud_x))
return;
// Existing generic helpers retain their screen-anchor conventions. Panels
// use their own right-edge origin and never push stock HUD areas around.
// 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.
@@ -40,7 +39,10 @@ common_config()
level.hud_col_health = level.hud_x + 120 + 12;
level.hud_health_threshold = 1.0;
level.hud_panel_right = 10;
level.hud_panel_top = 42;
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;
@@ -70,6 +72,38 @@ common_config()
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 = [];
}
@@ -133,7 +167,7 @@ show_help()
state = self.tws_service;
service_advance(state, getTime());
state.generation++;
state.help = service_request("help", service_page_count(state.commands.size, 4), state.generation);
state.help = service_request("help", service_page_count(state.commands.size, level.hud_service_help_rows), state.generation);
service_render(state);
}
@@ -205,14 +239,15 @@ service_advance(state, now)
help_rank(command)
{
if (command == ".help") return 0;
if (command == ".afk") return 1;
if (command == ".back") return 2;
if (command == ".join") return 3;
if (command == ".leaders") return 4;
if (command == ".b") return 5;
if (command.size >= 3 && getSubStr(command, 0, 3) == ".d ") return 6;
if (command.size >= 3 && getSubStr(command, 0, 3) == ".w ") return 7;
return 8;
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()
@@ -220,7 +255,7 @@ help_entries()
entries = [];
if (!isDefined(level.tws_help_commands))
return entries;
for (rank = 0; rank <= 8; rank++)
for (rank = 0; rank <= 9; rank++)
{
for (i = 0; i < level.tws_help_commands.size; i++)
{
@@ -234,9 +269,6 @@ help_entries()
help_command_label(command)
{
rank = help_rank(command);
if (rank == 6) return ".d <n|all>";
if (rank == 7) return ".w <n|all>";
return command;
}
@@ -244,13 +276,14 @@ help_description(entry)
{
rank = help_rank(entry.command);
if (rank == 0) return "Show commands";
if (rank == 1) return "Enable protection";
if (rank == 2) return "Return to play";
if (rank == 3) return "Join as newcomer";
if (rank == 4) return "View records";
if (rank == 5) return "Check balance";
if (rank == 6) return "Deposit points";
if (rank == 7) return "Withdraw points";
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;
}
@@ -273,57 +306,82 @@ service_ensure()
state.record_text = [];
state.help_page = -1;
state.rows = [];
state.command_rows = [];
state.description_rows = [];
state.view_kind = undefined;
self.tws_service = state;
y = level.hud_service_y;
state.background = make_panel_shader(self, 0, y, level.hud_panel_width,
level.hud_service_height, level.hud_c_panel, level.hud_panel_alpha, 0);
state.elems[state.elems.size] = state.background;
state.help_title = service_text(self, state, 10, 10, "left", 1.1, "COMMANDS", level.hud_c_label);
state.record_title = service_text(self, state, 10, 10, "left", 1.1, "MAP RECORD", level.hud_c_label);
state.help_footer = service_text(self, state, 10, 108, "left", 0.9,
"Four commands per page", level.hud_c_dim);
state.record_footer = service_text(self, state, 10, 108, "left", 0.9,
"Briefly shown - .leaders to view again", level.hud_c_dim);
state.page_number = service_text(self, state, 178, 10, "right", 1.1, "", level.hud_c_dim);
state.page_slash = service_text(self, state, 184, 10, "center", 1.1, "/", level.hud_c_dim);
state.page_total = service_text(self, state, 210, 10, "right", 1.1, "", level.hud_c_dim);
// Two fixed text columns keep the HUD element budget independent of the
// command count. Strings change only at page boundaries, never per tick.
state.command_back = make_panel_shader(self, 10, y + 30, 65, 68,
(0.16, 0.19, 0.17), 0.9, 5);
state.elems[state.elems.size] = state.command_back;
state.command_column = service_text(self, state, 14, 31,
"left", 1.0, "", level.hud_c_name);
state.description_column = service_text(self, state, 81, 31,
"left", 1.0, "", level.hud_c_count);
state.round_label = service_text(self, state, 10, 32, "left", 1.1, "ROUND", level.hud_c_label);
state.round_number = service_text(self, state, 54, 27, "left", 1.9, "", level.hud_c_count);
state.holders_label = service_text(self, state, 10, 52, "left", 1.0, "RECORD HOLDERS", level.hud_c_label);
for (i = 0; i < 3; i++)
state.rows[i] = service_text(self, state, 10, 65 + i * 13,
"left", 1.1, "", level.hud_c_count);
state.empty = service_text(self, state, 10, 40, "left", 1.1,
"No recorded high round yet", level.hud_c_count);
state.unavailable = service_text(self, state, 10, 40, "left", 1.1,
"Map record unavailable", level.hud_c_count);
for (i = 0; i < state.elems.size; i++)
{
state.elems[i].alpha = 0;
// Keep temporary private services out of the archived HUD bucket.
// Hidden elements still occupy slots; do not allocate one per page.
state.elems[i].archived = false;
}
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_panel_text(player, x, level.hud_service_y + y, align, scale, 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;
@@ -347,74 +405,70 @@ service_render(state)
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 (!isDefined(request))
return;
state.background.alpha = level.hud_panel_alpha;
state.page_number SetValue(page + 1);
state.page_total SetValue(request.pages);
state.page_number.alpha = 1;
state.page_slash.alpha = 1;
state.page_total.alpha = 1;
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;
state.help_footer.alpha = 1;
if (state.help_page != page)
for (i = 0; i < state.command_rows.size; i++)
{
command_text = "";
description_text = "";
for (i = 0; i < 4; i++)
index = page * level.hud_service_help_rows + i;
if (index >= state.commands.size)
continue;
if (state.help_page != page)
{
index = page * 4 + i;
if (index < state.commands.size)
{
if (i > 0)
{
command_text += "\n";
description_text += "\n";
}
entry = state.commands[index];
command_text += help_command_label(entry.command);
description_text += help_description(entry);
}
entry = state.commands[index];
state.command_rows[i] SetText(help_command_label(entry.command));
state.description_rows[i] SetText(help_description(entry));
}
state.command_column SetText(command_text);
state.description_column SetText(description_text);
state.help_page = page;
state.command_rows[i].alpha = 1;
state.description_rows[i].alpha = 1;
}
state.command_back.alpha = 0.9;
state.command_column.alpha = 1;
state.description_column.alpha = 1;
state.help_page = page;
return;
}
state.record_title.alpha = 1;
state.record_footer.alpha = 1;
header = "Map record unavailable";
snapshot = request.snapshot;
if (!isDefined(snapshot))
if (isDefined(snapshot))
{
state.unavailable.alpha = 1;
return;
header = "No recorded high round yet";
if (snapshot.round > 0)
header = "Map record · Round " + snapshot.round;
}
if (snapshot.round == 0)
if (!isDefined(state.record_header_text) || state.record_header_text != header)
{
state.empty.alpha = 1;
return;
state.record_title SetText(header);
state.record_header_text = header;
}
state.round_label.alpha = 1;
state.round_number SetValue(snapshot.round);
state.round_number.alpha = 1;
state.holders_label.alpha = 1;
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];
// Skip identical text when replaying a page or resuming after help.
if (!isDefined(state.record_text[i]) || state.record_text[i] != text)
{
state.rows[i] SetText(text);
@@ -457,12 +511,7 @@ service_destroy(state)
state.closed = true;
state.help = undefined;
state.record = undefined;
for (i = 0; i < state.elems.size; i++)
{
if (isDefined(state.elems[i]))
state.elems[i] Destroy();
}
state.elems = [];
service_view_destroy(state);
}
service_game_over_via(event)
@@ -523,7 +572,8 @@ make_text_elem(player, x, y, align_x, scale, colour)
elem.x = x;
elem.y = y;
elem.foreground = 1;
elem.fontScale = scale;
// 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;
@@ -550,14 +600,59 @@ make_shader_elem(player, x, y)
return elem;
}
// Panel x/y are local to the fixed upper-right core origin. A right screen
// anchor keeps the panel on-screen across aspect ratios; negative offsets grow
// left from that edge. Keep each engine's existing vertical anchor convention.
// 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, x - level.hud_panel_width - level.hud_panel_right,
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 = "right";
elem.horzAlign = "fullscreen";
elem.vertAlign = "fullscreen";
elem.sort = 20;
elem.archived = true;
return elem;
@@ -565,17 +660,87 @@ make_panel_text(player, x, y, align_x, scale, colour)
make_panel_shader(player, x, y, width, height, colour, alpha, sort)
{
elem = make_shader_elem(player, x - level.hud_panel_width - level.hud_panel_right,
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 = "right";
elem.horzAlign = "fullscreen";
elem.vertAlign = "fullscreen";
elem.color = colour;
elem.alpha = alpha;
elem.sort = sort;
elem.archived = true;
elem SetShader("white", width, height);
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");
+72 -47
View File
@@ -1,4 +1,4 @@
/* tws_t6_hud.gsc - fixed upper-right core HUD, visible from connection. */
/* tws_t6_hud.gsc - minimal edge HUD, toggleable with .hud. */
#include maps\mp\_utility;
#include common_scripts\utility;
@@ -11,14 +11,34 @@ main()
init()
{
scripts\zm\tws_t6_common::ensure_config();
scripts\zm\tws_t6_common::register_help_command(".hud", "Toggle core HUD");
level thread round_watcher();
level thread game_over_watcher();
level thread hud_chat_listener();
scripts\zm\tws_t6_common::register_connect_handler(::hud_connect);
scripts\zm\tws_t6_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\zm\tws_t6_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()
@@ -29,48 +49,42 @@ hud_connect()
return;
self.tws_hud_elems = [];
background = scripts\zm\tws_t6_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.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 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");
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.
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, "--");
// 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\zm\tws_t6_common::set_core_alpha(self, self.tws_hud_health, 0);
scripts\zm\tws_t6_common::set_core_alpha(self, self.tws_hud_slash, 0);
scripts\zm\tws_t6_common::set_core_alpha(self, self.tws_hud_maxhealth, 0);
scripts\zm\tws_t6_common::set_core_alpha(self, self.tws_hud_hp, 0);
track = scripts\zm\tws_t6_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);
track = scripts\zm\tws_t6_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\zm\tws_t6_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_bar = scripts\zm\tws_t6_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\zm\tws_t6_common::destroy_on_disconnect(self.tws_hud_elems);
@@ -78,7 +92,7 @@ hud_connect()
hud_text(x, y, align, scale, colour, text)
{
elem = scripts\zm\tws_t6_common::make_panel_text(self, x, y, align, scale, colour);
elem = scripts\zm\tws_t6_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;
@@ -147,7 +161,7 @@ hud_timer_row()
{
self endon("disconnect");
// A visible panel for a waiting newcomer must not start a pre-set round 1.
// 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)
@@ -194,7 +208,7 @@ match_timer_bind(elem)
elem SetTimerUp(max(0, (getTime() - level.match_start_time) / 1000.0));
}
// The panel remains visible at game over, with authoritative frozen clocks.
// Preserve authoritative frozen clocks when the core is shown after game over.
freeze_timers_on_game_over(match_elem, round_elem)
{
self endon("disconnect");
@@ -238,11 +252,12 @@ hud_health_display()
self endon("disconnect");
level endon("hud_game_over");
bar_width = level.hud_bar_width;
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;
@@ -261,16 +276,17 @@ hud_health_display()
if (frac > 1)
frac = 1;
width = int(max(frac * bar_width, 1));
width = scripts\zm\tws_t6_common::make_overlay_width(frac * bar_width);
if (width != last_width)
{
bar SetShader("white", width, level.hud_bar_height);
bar SetShader("white", width, level.hud_overlay_bar_height);
last_width = width;
}
bar.alpha = 1;
bar_alpha = 1;
if (frac == 0)
bar.alpha = 0;
bar_alpha = 0;
scripts\zm\tws_t6_common::set_core_alpha(self, bar, bar_alpha);
colour = level.hud_c_high;
if (frac <= 0.25)
@@ -282,8 +298,17 @@ hud_health_display()
cur.color = colour;
slash.color = colour;
max_hp.color = colour;
hp.color = colour;
// Health numbers remain visible at full health.
readout_alpha = 0;
if (isDefined(self.health) && current < maximum)
readout_alpha = 1;
scripts\zm\tws_t6_common::set_core_alpha(self, cur, readout_alpha);
scripts\zm\tws_t6_common::set_core_alpha(self, slash, readout_alpha);
scripts\zm\tws_t6_common::set_core_alpha(self, max_hp, readout_alpha);
scripts\zm\tws_t6_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);