diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a64e81..7371aab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,60 @@ Changes are listed newest first, with an independent version for every individua T5/T6 script and each native plugin. Current versions are recorded in [versions.json](versions.json); see the [version policy](README.md#versioning). +## 2026-10-17 + +### Center the complete zombie count above the health bar + +| Component | Version | Affected source | +| --- | --- | --- | +| `tws_t5_hud.gsc` | `1.4.2` | `t5/scripts/tws_t5_hud.gsc` | +| `tws_t6_hud.gsc` | `1.4.2` | `t6/scripts/tws_t6_hud.gsc` | + +- Replace the separately positioned number and label with one center-aligned + `N ZOMBIES` readout at the health track's midpoint in both engines. Use a + uniform font scale of `1.25` and the existing count color; digit changes and + the initial `-- ZOMBIES` placeholder share the same center. +- Update text only when the count changes using `SetTextUnlimited`, avoiding + a new config string for each distinct count. Keep counting, `.hud` visibility, + game-over termination and disconnect cleanup behavior. Core HUD allocation + decreases from 11 to 10 archived elements; AFK still adds one. +- Compatibility: HUD `1.4.2` requires matching common `1.4.0+` and + [Plutonium r5334+](https://plutonium.pw/docs/changelog/#r5334), which provides + `SetTextUnlimited` for T5 and T6. No new plugin or backend requirement. + Common and AFK need no source change for this fix; both HUD scripts change. +- Validation: 73 HUD checks passed, including digit transitions, a non-default + track midpoint, unchanged-count polling, hidden updates and service cleanup. + Leaderboard (143), bank (42), join (17) and telemetry (23) portable checks + passed. These use engine shims; actual EC integration and live Plutonium + validation were not run. GSC compilation was unavailable because gsc-tool + and PowerShell are not installed. Verify rendering on Windows/Plutonium. +- Deployment: copy each updated HUD script and restart the map on r5334+. + Older installations must first update Plutonium on clients and servers and + fully restart those processes. No full server-process restart is required + for the script update alone. No deployment performed. + +### Use ASCII separators in HUD branding and map records + +| Component | Version | Affected source | +| --- | --- | --- | +| `tws_t5_common.gsc` | `1.4.1` | `t5/scripts/tws_t5_common.gsc` | +| `tws_t6_common.gsc` | `1.4.1` | `t6/scripts/tws_t6_common.gsc` | +| `tws_t5_hud.gsc` | `1.4.1` | `t5/scripts/tws_t5_hud.gsc` | +| `tws_t6_hud.gsc` | `1.4.1` | `t6/scripts/tws_t6_hud.gsc` | + +- Replace the middle dot with an ASCII vertical bar in the top-center branding + and map-record heading after the separator displayed as two dots on T5. + Apply the same text change to T6 for a consistent appearance. Keep existing + spacing, positioning and HUD/service behavior. +- Compatibility: no new dependency requirement. No native-plugin, backend or + other script changes are required. +- Validation: 71 HUD, 143 leaderboard, 42 bank, 17 join and 23 telemetry portable + checks passed. These use engine shims; actual EC bank integration, GSC + compilation and live Windows/Plutonium validation were not performed. +- Deployment: copy the updated common and HUD scripts for each installed game, + then restart the map. No full server-process restart is required. No deployment + performed; live Windows/Plutonium rendering validation remains outstanding. + ## 2026-10-15 — Player moderation runtime - **tws_t5_moderation.gsc 1.0.0 / tws_t6_moderation.gsc 1.0.0**: new optional diff --git a/README.md b/README.md index 537813a..2e516a2 100644 --- a/README.md +++ b/README.md @@ -362,11 +362,14 @@ settles legacy and public journals without duplicate ledger entries or lost poin ## Core HUD and service window (T5 and T6) The core uses a light, unboxed layout: match time above round time at the upper -left, `TWS ZOMBIES · discord.tws.lol` centered along the top, and the zombie -count centered near the bottom above a slim health bar. Current/maximum HP -appears to the right of the bar only while injured, then disappears at full -health. The AFK indicator sits above the zombie count. Text uses an outline for -contrast; there are no panel backgrounds or timer labels. +left, `TWS ZOMBIES | discord.tws.lol` centered along the top, and the zombie +count centered near the bottom above a slim health bar. The complete +`N ZOMBIES` text uses one centered element at the health track's midpoint, +keeping the group centered as the count gains or loses digits. It uses a +uniform font scale of `1.25` and shows `-- ZOMBIES` before data is available. +Current/maximum HP appears to the right of the bar only while injured, then +disappears at full health. The AFK indicator sits above the zombie count. Text +uses an outline for contrast; there are no panel backgrounds or timer labels. Fullscreen X/Y anchors bypass safe-area insets. Overlay positions and shader widths use the normalized `640 × 480` viewport directly. Text uses a minimum @@ -401,7 +404,7 @@ count appears only when more than one page is needed. Each page lasts command restarts page one. Both engines position rows explicitly, avoiding T5's newline-rendering problem. -Records use the same service area, right aligned: `Map record · Round N` +Records use the same service area, right aligned: `Map record | Round N` followed by up to three bounded holder lines. Single-page records have no counter or footer; ties spanning multiple pages retain a page count and 12 seconds per page. Help pauses a visible record and resumes its remaining @@ -411,19 +414,24 @@ remain private and work when the HUD module is omitted. Both games allocate only the active service view and release it when idle or before switching between help and records. Pages reuse the active view's -elements. The core uses 11 archived elements, plus one for AFK. The standard +elements. The core uses 10 archived elements, plus one for AFK. The standard nine-command help view uses 19 nonarchived elements, or 20 with a multi-page counter; records use four, or five with a multi-page counter. Hidden elements still occupy slots. The controller and pending requests are separate from display lifetime, so an idle service holds no HUD elements. -HUD `1.4.0` and AFK `1.2.0` require matching common `1.4.0+` on both games. -Update common, HUD and AFK together where installed, then restart the map. -Leaderboard `1.1.0` remains compatible. There is no new Plutonium, native-plugin -or backend requirement, and this change does not require a full server-process -restart. Existing leaderboard plugin requirements still apply. Common retains -its older panel helpers for unchanged consumers; the new layout uses fullscreen -overlay helpers with the same integer shader-width handling. +HUD `1.4.2` requires matching common `1.4.0+` and +[Plutonium r5334+](https://plutonium.pw/docs/changelog/#r5334) on both games. +The centered counter uses `SetTextUnlimited` only when the count changes, +avoiding dynamic counter strings in the text config-string pool. Numeric HP +still uses `SetValue`. On r5334+, deploy the updated HUD script and restart +the map; no full server-process restart is required. On older Plutonium +revisions, update clients and server and fully restart them before deploying. +AFK `1.2.0` and leaderboard `1.1.0` remain compatible; AFK still requires +common `1.4.0+`. There is no new native-plugin or backend requirement. Existing +leaderboard plugin requirements still apply. Common retains its older panel +helpers for unchanged consumers; the new layout uses fullscreen overlay helpers +with the same integer shader-width handling. ### HUD validation @@ -445,7 +453,9 @@ Before rollout, validate both engines in Plutonium: legible without panel backgrounds; services must clear the Plutonium tag. Verify the health-bar fill stays within its track and damage-only HP clears stock scores/ammo. Check text fit at other aspect ratios because horizontal - geometry scales with viewport width. + geometry scales with viewport width. Check `0`, `6`, `9`, `10`, `99` and + `100 ZOMBIES`: the entire text must stay centered over the health track, + including after `.hud` hides/shows it and after game over freezes the count. 2. Join before spawning, join mid-match, die/respawn and reconnect. Verify one core per player, spectator placeholders, no early match-clock start, correct health/Juggernog changes, and frozen timers at game over. HP numbers must @@ -509,7 +519,7 @@ when changing variants; restart the map after changing these settings. The announcement starts five seconds after player readiness and waits for the initial record fetch if necessary. It appears as right-aligned text in the -common-owned upper-right service area: `Map record · Round N`, followed by up +common-owned upper-right service area: `Map record | Round N`, followed by up to three holder lines. There is no background or replay footer, and a page count appears only for multi-page records. Each page lasts 12 seconds. Opening `.help` pauses the record's remaining page time until help closes. A `.leaders` request during help replaces any diff --git a/t5/scripts/NOTES.md b/t5/scripts/NOTES.md index 0b72f39..9d1ae98 100644 --- a/t5/scripts/NOTES.md +++ b/t5/scripts/NOTES.md @@ -32,12 +32,15 @@ before copying the renamed scripts. - **`SetTimerUp(N)` only ticks when `N == 0`** on T5 SP. Non-zero seeds render "0:00" but don't advance. Late joiners see 0, not real elapsed; authoritative elapsed lives in `level.match_start_time`. -- **Dynamic numbers use `SetValue`.** Repeatedly generating unique strings - risks exhausting the text config-string pool; identical strings can be reused - by the engine's [string lookup](https://github.com/MisaDev4/bo1-zombies-decompiled/blob/102f6992be332610aff587e681e3180b15f395bb/src/game_mp/g_utils_mp.cpp#L46). - Composite HP uses numeric elements around a static slash and `HP` suffix; - the group appears only while injured. Help reuses up to nine explicitly - positioned command/description pairs, and record rows skip unchanged text. +- **Dynamic counter text uses `SetTextUnlimited`; HP uses `SetValue`.** + Repeatedly generating unique strings through `SetText` risks exhausting the + text config-string pool; identical strings can be reused by the engine's + [string lookup](https://github.com/MisaDev4/bo1-zombies-decompiled/blob/102f6992be332610aff587e681e3180b15f395bb/src/game_mp/g_utils_mp.cpp#L46). + The centered `N ZOMBIES` element uses Plutonium r5334+'s `SetTextUnlimited` + only when the count changes. Composite HP retains numeric elements around a + static slash and `HP` suffix; the group appears only while injured. Help + reuses up to nine explicitly positioned command/description pairs, and + record rows skip unchanged text. - **`NewClientHudElem(player)`, not `NewHudElem()`.** `NewHudElem` is globally owned; in co-op every player's HUD draws on top of every other player's. Fall back to `NewHudElem` only if elements don't render at all @@ -77,7 +80,7 @@ before copying the renamed scripts. from page one. Keep both services independent of the HUD module. Services never reposition the core, and AFK remains centered above the zombie count. - **Bound HUD element counts, including hidden elements.** Core + AFK use - 11 + 1 archived elements. Common allocates only the active service view: + 10 + 1 archived elements. 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), and none while idle. Switching help/record views destroys the previous view first; pages reuse @@ -99,12 +102,16 @@ before copying the renamed scripts. polling `self.health` misses the zero-crossing (health is rewritten during the game-over sequence). -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. +HUD `1.4.2` requires matching common `1.4.0+` and +[Plutonium r5334+](https://plutonium.pw/docs/changelog/#r5334) for +`SetTextUnlimited`. On r5334+, update the HUD script and restart the map; +older Plutonium clients and servers need updates and full restarts first. +AFK `1.2.0` and leaderboard `1.1.0` remain compatible; AFK still requires +common `1.4.0+`. The full help order is `.help`, `.hud`, `.afk`, `.back`, `.join`, `.leaders`, `.b`, `.d`, `.w`: one page of nine commands, with full `` bank syntax. `.hud` is absent when HUD is omitted. Records use right-aligned -`Map record · Round N` and up to three bounded holder lines, with no footer +`Map record | Round N` and up to three bounded holder lines, with no footer and no single-page counter. Both views retain 12-second page timing. Fullscreen X/Y overlay anchors use direct normalized `640 × 480` coordinates. Dynamic health fills use `make_overlay_width()` like their tracks. Apply @@ -112,10 +119,14 @@ Dynamic health fills use `make_overlay_width()` like their tracks. Apply `SetShader` requires an integer width. Legacy panel helpers and their `0.75` horizontal conversion remain available for existing consumers. Font scale is at least `1.0`. Numeric HP appears only while injured; the core and services -have no panel backgrounds. There is no new Plutonium/native-plugin/backend -dependency or full server-process restart requirement. +have no panel backgrounds. The counter is one `N ZOMBIES` element centered +at the health track's midpoint with uniform scale `1.25`, starting as +`-- ZOMBIES`. There is no new native-plugin/backend dependency or full +server-process restart requirement when already using Plutonium r5334+. Run the [shared HUD checks](../../README.md#hud-validation); portable fixtures -do not replace T5 live loading, layout and lifecycle validation. +do not replace T5 live loading, layout and lifecycle validation. Check the +counter's centering at `0`, `6`, `9`, `10`, `99` and `100`, after `.hud` +hides/shows it, and after game over freezes the count. ## Module map diff --git a/t5/scripts/README.md b/t5/scripts/README.md index bcee2c2..927520d 100644 --- a/t5/scripts/README.md +++ b/t5/scripts/README.md @@ -60,10 +60,13 @@ invalid identities fail privately without changing points. Follow the shared ## Core HUD and service window 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. +`TWS ZOMBIES | discord.tws.lol` at the top center, and zombies remaining above a +slim health bar at the bottom center. `N ZOMBIES` is one centered element at +the health track's midpoint, using uniform font scale `1.25` so the whole group +stays centered as the digit count changes. Its initial placeholder is +`-- ZOMBIES`. 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. @@ -81,7 +84,7 @@ 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 +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 @@ -90,12 +93,15 @@ 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. +requests and interruption/resume behavior. Core + AFK use 10 + 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. +HUD `1.4.2` requires common `1.4.0+` and +[Plutonium r5334+](https://plutonium.pw/docs/changelog/#r5334) for its +`SetTextUnlimited` counter, updated only when the count changes. Deploy the +updated HUD script and restart the map on r5334+; no full server-process +restart is required. Older Plutonium revisions need client/server updates and +full restarts first. AFK `1.2.0` and leaderboard `1.1.0` remain compatible; +AFK still requires common `1.4.0+`. No new native-plugin or backend requirement. ## Map record announcements @@ -263,6 +269,9 @@ connect listener anywhere else crashes the T5 SP handshake. Check that branding, the health bar, damage-only HP, zombie count and timers remain correct during each view and after it closes. Include other installed HUD mods; the live T5 SP element limit is unverified. +6. Check `0`, `6`, `9`, `10`, `99` and `100 ZOMBIES` at different resolutions: + the complete text must stay centered above the health track. Repeat after + toggling `.hud` and after game over freezes the count. --- @@ -274,8 +283,8 @@ gameplay updates start through the existing spawn lifecycle. **Features** - Upper-left match timer above round timer (round resets each round, both freeze on game over) - `.hud` toggles this player's core and AFK indicator; data updates continue while hidden -- Top-center `TWS ZOMBIES · discord.tws.lol` branding -- Bottom-center zombies-remaining counter, with the AFK indicator above it +- Top-center `TWS ZOMBIES | discord.tws.lol` branding +- One `N ZOMBIES` element centered above the health track, with the AFK indicator above it - Slim health bar with green / amber / red ramp; current/maximum HP appears beside it only while injured - Spectator placeholders without starting the match clock on connection @@ -301,6 +310,7 @@ without seeding clocks; `hud_start` calls it as a fallback if needed. `destroy_on_disconnect`, `format_mmss`, `register_help_command`, `register_spawn_handler`, `register_connect_handler` - Every `level.hud_*` config value from `tws_t5_common` +- Plutonium r5334+ for the count's `SetTextUnlimited` updates - Stock BO1 state: `level.round_number`, `level.zombie_total`, `level.players`, `self.health`, `self.maxhealth`, AI entities' `.animname` and `.ignore_enemy_count` diff --git a/t5/scripts/tws_t5_common.gsc b/t5/scripts/tws_t5_common.gsc index 179f323..c1a5bea 100644 --- a/t5/scripts/tws_t5_common.gsc +++ b/t5/scripts/tws_t5_common.gsc @@ -457,7 +457,7 @@ service_render(state) { header = "No recorded high round yet"; if (snapshot.round > 0) - header = "Map record · Round " + snapshot.round; + header = "Map record | Round " + snapshot.round; } if (!isDefined(state.record_header_text) || state.record_header_text != header) { diff --git a/t5/scripts/tws_t5_hud.gsc b/t5/scripts/tws_t5_hud.gsc index 90104f5..9fbb15d 100644 --- a/t5/scripts/tws_t5_hud.gsc +++ b/t5/scripts/tws_t5_hud.gsc @@ -52,12 +52,12 @@ hud_connect() 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"); + "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"); + // Center the entire readout on the track, regardless of the digit count. + count_x = level.hud_overlay_bar_x + level.hud_overlay_bar_width / 2.0; + self.tws_hud_count = self hud_text(count_x, + level.hud_overlay_count_y, "center", 1.25, level.hud_c_count, "-- ZOMBIES"); // Independent numeric elements avoid a new config string on every tick. // The readout appears only after a valid damaged-health sample. @@ -233,7 +233,8 @@ hud_zombie_counter() if (remaining != last) { - count SetValue(remaining); + // Plutonium r5334+: reuse text storage as the count changes. + count SetTextUnlimited(int(remaining) + " ZOMBIES"); last = remaining; } diff --git a/t6/scripts/NOTES.md b/t6/scripts/NOTES.md index 1fa1176..65d3974 100644 --- a/t6/scripts/NOTES.md +++ b/t6/scripts/NOTES.md @@ -43,7 +43,7 @@ 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. +Core + AFK use 10 + 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. @@ -54,18 +54,24 @@ 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 +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. +HUD `1.4.2` requires matching common `1.4.0+` and +[Plutonium r5334+](https://plutonium.pw/docs/changelog/#r5334) for +`SetTextUnlimited`. The `N ZOMBIES` counter is one element centered at the +health track's midpoint with uniform scale `1.25`, starting as `-- ZOMBIES`. +Only changed counts update its text; numeric HP retains `SetValue`. On r5334+, +update the HUD script and restart the map. Older Plutonium clients and servers +need updates and full restarts first. AFK `1.2.0` and leaderboard `1.1.0` remain +compatible; AFK still requires common `1.4.0+`. 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. +for existing consumers. Font scale is at least `1.0`. There is no new +native-plugin/backend dependency or full server-process restart requirement +when already using Plutonium r5334+. 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 @@ -195,6 +201,8 @@ Live checks after deploying: 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. + Check `0`, `6`, `9`, `10`, `99` and `100 ZOMBIES` remain centered above the + health track, including after `.hud` toggles and game over freezes the count. 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. diff --git a/t6/scripts/README.md b/t6/scripts/README.md index 34e727d..d3da8f6 100644 --- a/t6/scripts/README.md +++ b/t6/scripts/README.md @@ -8,7 +8,7 @@ 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` | Unboxed timers, branding, zombies/health and per-player `.hud` visibility toggle | Common 1.4.0+ | +| `tws_t6_hud.gsc` | Unboxed timers, branding, zombies/health and per-player `.hud` visibility toggle | Common 1.4.0+, Plutonium r5334+ | | `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 | @@ -73,10 +73,13 @@ populates that field before the `connected` notification. Follow the shared ## Core HUD and service window 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. +`TWS ZOMBIES | discord.tws.lol` at the top center, and zombies remaining above a +slim health bar at the bottom center. `N ZOMBIES` is one centered element at +the health track's midpoint, using uniform font scale `1.25` so the whole group +stays centered as the digit count changes. Its initial placeholder is +`-- ZOMBIES`. 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. @@ -94,7 +97,7 @@ 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 +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 @@ -103,12 +106,15 @@ 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. +requests and interruption/resume behavior. Core + AFK use 10 + 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. +HUD `1.4.2` requires common `1.4.0+` and +[Plutonium r5334+](https://plutonium.pw/docs/changelog/#r5334) for its +`SetTextUnlimited` counter, updated only when the count changes. Deploy the +updated HUD script and restart the map on r5334+; no full server-process +restart is required. Older Plutonium revisions need client/server updates and +full restarts first. AFK `1.2.0` and leaderboard `1.1.0` remain compatible; +AFK still requires common `1.4.0+`. No new native-plugin or backend requirement. ## Map record announcements diff --git a/t6/scripts/tws_t6_common.gsc b/t6/scripts/tws_t6_common.gsc index 25b0ab3..5bf1f20 100644 --- a/t6/scripts/tws_t6_common.gsc +++ b/t6/scripts/tws_t6_common.gsc @@ -453,7 +453,7 @@ service_render(state) { header = "No recorded high round yet"; if (snapshot.round > 0) - header = "Map record · Round " + snapshot.round; + header = "Map record | Round " + snapshot.round; } if (!isDefined(state.record_header_text) || state.record_header_text != header) { diff --git a/t6/scripts/tws_t6_hud.gsc b/t6/scripts/tws_t6_hud.gsc index 41823f9..b623bdc 100644 --- a/t6/scripts/tws_t6_hud.gsc +++ b/t6/scripts/tws_t6_hud.gsc @@ -54,12 +54,12 @@ hud_connect() 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"); + "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"); + // Center the entire readout on the track, regardless of the digit count. + count_x = level.hud_overlay_bar_x + level.hud_overlay_bar_width / 2.0; + self.tws_hud_count = self hud_text(count_x, + level.hud_overlay_count_y, "center", 1.25, level.hud_c_count, "-- ZOMBIES"); // Independent numeric elements avoid a new config string on every tick. // The readout appears only after a valid damaged-health sample. @@ -239,7 +239,8 @@ hud_zombie_counter() if (remaining != last) { - count SetValue(remaining); + // Plutonium r5334+: reuse text storage as the count changes. + count SetTextUnlimited(int(remaining) + " ZOMBIES"); last = remaining; } diff --git a/tools/check_core_hud.mjs b/tools/check_core_hud.mjs index 81e2f7d..7bfab8a 100644 --- a/tools/check_core_hud.mjs +++ b/tools/check_core_hud.mjs @@ -26,11 +26,12 @@ function install(context, source, names) { const locals = [...new Set([...code.matchAll(/(? match[1]))]; code = code.replace(/(?:self|level) endon\([^;]+;/g, '') .replace(/scripts\\(?:sp|zm)\\tws_t[56]_common::/g, 'common.') + .replace(/maps\\mp\\zombies\\_zm_utility::get_current_zombie_count/g, 'get_enemy_count') .replace(/self thread ([\w.]+)\(([^;]*)\);/g, (_, fn, values) => `queued.push(['${fn}', ${values}]);`) .replace(/self common\.toggle_core_hud\(\)/g, 'common.toggle_core_hud()') .replace(/self ((?:hud_|afk_)\w+)\(/g, '$1(') .replace(/self (\w+)\(/g, 'self.$1(') - .replace(/(\w+) (SetText|SetValue|SetTimerUp|SetShader)\(([^;]*)\);/g, '$1.$2($3);') + .replace(/(\w+) (SetText|SetTextUnlimited|SetValue|SetTimerUp|SetShader)\(([^;]*)\);/g, '$1.$2($3);') .replace(/level notify\(([^;]*)\);/g, 'level.notify($1);') .replace(/level waittill\(([^;]*)\);/g, 'yield { event: $1 };') .replace(/\.size\b/g, '.length') @@ -51,6 +52,7 @@ function engine(game, source, afk, hidden = false) { function element(args, alpha = 1) { const result = { args, alpha, writes: [], SetText(value) { this.text = value; this.writes.push(['text', value]); }, + SetTextUnlimited(value) { this.text = value; this.writes.push(['unlimitedText', value]); }, SetValue(value) { this.value = value; this.writes.push(['value', value]); }, SetShader(...args) { this.shader = args; }, SetTimerUp(value) { this.timer = value; this.writes.push(['timer', value]); }, @@ -79,7 +81,7 @@ function engine(game, source, afk, hidden = false) { player_active: player => player.active, format_mmss: ms => `${Math.floor(ms / 60000)}:${String(Math.floor(ms / 1000) % 60).padStart(2, '0')}`, }; - const level = { round_number: 1, hud_rate: .1, afk_grace_seconds: 2, + const level = { round_number: 1, hud_rate: .1, afk_grace_seconds: 2, zombie_team: 'axis', hud_overlay_timer_x: 6, hud_overlay_match_y: 8, hud_overlay_round_y: 30, hud_overlay_brand_x: 320, hud_overlay_brand_y: 8, hud_overlay_count_x: 300, hud_overlay_count_y: 440, @@ -92,10 +94,11 @@ function engine(game, source, afk, hidden = false) { hud_c_count: 'white', hud_c_dim: 'dim', hud_c_afk: 'yellow', hud_c_high: 'green', hud_c_mid: 'yellow', hud_c_low: 'red', notify() {} }; const context = vm.createContext({ self, level, common, queued, getTime: () => now, + get_enemy_count: () => level.fixture_enemies ?? 0, isDefined: value => value !== undefined && value !== null, is_true: value => !!value, int: Math.trunc, max: Math.max }); install(context, source, ['hud_connect', 'hud_text', 'hud_start', 'hud_toggle', 'hud_timer_row', - 'hud_health_display', 'freeze_timers_on_game_over']); + 'hud_health_display', 'hud_zombie_counter', 'freeze_timers_on_game_over']); install(context, afk, ['afk_spawn', 'afk_enter', 'afk_exit', ...(game === 't6' ? ['afk_release', 'afk_lock_controls', 'afk_restore_controls'] : [])]); return { context, self, level, queued, elems, actions, common, @@ -118,7 +121,7 @@ for (const game of ['t5', 't6']) { assert(chat.includes('player thread hud_toggle()'), 'disconnect scope stays in the player worker'); assert(!body(source, 'hud_connect')[1].includes('getTime()')); assert(!source.includes('tws_hud_hidden ='), 'spawn/construction must not reset the preference'); - for (const name of ['round_watcher', 'reset_timer_each_round', 'match_timer_bind']) + for (const name of ['round_watcher', 'reset_timer_each_round', 'match_timer_bind', 'hud_zombie_counter']) assert(body(source, name)[1].includes('level endon("hud_game_over")')); const bind = body(source, 'match_timer_bind')[1]; if (game === 't5') assert(bind.includes('elem SetTimerUp(0)')); @@ -127,13 +130,13 @@ for (const game of ['t5', 't6']) { assert(afk.includes('level.hud_overlay_afk_y')); assert(!/\.alpha\s*=/.test(afk), 'AFK visibility must use the common mask'); }); - check(`${game}: connection places 11 box-free core elements at the screen edges without seeding clocks`, () => { + check(`${game}: connection places 10 box-free core elements at the screen edges without seeding clocks`, () => { const e = make(); e.context.hud_connect(); assert.equal(e.level.match_start_time, undefined); assert.equal(e.level.round_start_time, undefined); assert.equal(e.self.tws_hud_match.text, '--:--'); assert.equal(e.self.tws_hud_health.text, '--'); - assert.equal(e.elems.length, 11); - assert.equal(e.self.tws_core_hud_elems.length, 11); - e.context.hud_connect(); assert.equal(e.elems.length, 11); + assert.equal(e.elems.length, 10); + assert.equal(e.self.tws_core_hud_elems.length, 10); + e.context.hud_connect(); assert.equal(e.elems.length, 10); assert.equal(e.queued.length, 1); assert.equal(e.queued[0][0], 'common.destroy_on_disconnect'); assert.equal(e.queued[0][1].length, e.elems.length); const textAt = (text, x, y, align, scale, colour) => { @@ -142,9 +145,9 @@ for (const game of ['t5', 't6']) { }; assert.deepEqual(e.self.tws_hud_match.args.slice(1), [6, 8, 'left', 1.4, 'white']); assert.deepEqual(e.self.tws_hud_round.args.slice(1), [6, 30, 'left', 1.25, 'dim']); - textAt('TWS ZOMBIES · discord.tws.lol', 320, 8, 'center', 1.1, 'dim'); - assert.deepEqual(e.self.tws_hud_count.args.slice(1), [300, 440, 'right', 1.5, 'white']); - textAt('ZOMBIES', 306, 446, 'left', 1.05, 'dim'); + textAt('TWS ZOMBIES | discord.tws.lol', 320, 8, 'center', 1.1, 'dim'); + textAt('-- ZOMBIES', 320, 440, 'center', 1.25, 'white'); + assert(!e.elems.some(elem => elem.text === 'ZOMBIES'), 'count and label share one alignment'); textAt('HP', 449, 460, 'left', 1, 'white'); for (const [key, x, align] of [['health', 414, 'right'], ['slash', 418, 'left'], ['maxhealth', 445, 'right']]) { const elem = e.self[`tws_hud_${key}`]; @@ -165,13 +168,42 @@ for (const game of ['t5', 't6']) { assert.equal(e.level.match_start_time, undefined); assert.equal(e.level.round_start_time, undefined); assert.equal(e.queued.length, 1, 'toggle only queues disconnect cleanup'); e.context.hud_start(); e.context.hud_start(); - assert.equal(e.elems.length, 11); assert.equal(e.self.tws_hud_hidden, true); + assert.equal(e.elems.length, 10); assert.equal(e.self.tws_hud_hidden, true); assert.deepEqual(e.queued.slice(1).map(item => item[0]), ['hud_timer_row', 'hud_zombie_counter', 'hud_health_display']); e.context.hud_timer_row(); assert.equal(e.level.match_start_time, 123456); assert.equal(e.level.round_start_time, 123456); const threads = e.queued.length; e.context.hud_toggle(); e.context.hud_toggle(); e.context.hud_start(); - assert.equal(e.queued.length, threads); assert.equal(e.elems.length, 11); + assert.equal(e.queued.length, threads); assert.equal(e.elems.length, 10); + }); + check(`${game}: complete zombie text stays centered through digit changes and hidden updates without config-string churn`, () => { + const e = make(); + // Center on the configured track, including non-default and fractional midpoints. + e.level.hud_overlay_bar_x = 210; + e.level.hud_overlay_bar_width = 155; + e.context.hud_connect(); + const count = e.self.tws_hud_count; + assert.deepEqual(count.args.slice(1, 4), [287.5, 440, 'center']); + const run = e.context.hud_zombie_counter(); + for (const value of [0, 6, 9, 10, 99, 100, 999, 1000, 6]) { + e.level.fixture_enemies = value; + run.next(); + assert.equal(count.text, `${value} ZOMBIES`); + assert.deepEqual(count.args.slice(1, 4), [287.5, 440, 'center']); + const writes = count.writes.length; + run.next(); assert.equal(count.writes.length, writes, 'unchanged counts do not rewrite text'); + } + e.level.zombie_total = 94; run.next(); + assert.equal(count.text, '100 ZOMBIES', 'remaining spawn count remains included'); + e.context.hud_toggle(); + e.level.zombie_total = -4; run.next(); + assert.equal(count.text, '6 ZOMBIES'); assert.equal(count.alpha, 0); + e.context.hud_toggle(); + assert.equal(count.text, '6 ZOMBIES'); assert.equal(count.alpha, 1); + assert.equal(count.writes.filter(([kind]) => kind === 'text').length, 1, + 'only the fixed placeholder uses a normal config string'); + assert(count.writes.slice(1).every(([kind]) => kind === 'unlimitedText')); + assert.equal(e.elems.length, 10, 'updates and toggles never allocate replacement elements'); }); check(`${game}: health numbers appear only while damaged, follow the bar colour, and avoid config-string churn`, () => { const e = make(); e.context.hud_connect(); const { self } = e; @@ -216,7 +248,7 @@ for (const game of ['t5', 't6']) { e.context.hud_toggle(); assert.equal(e.self.tws_hud_bar.alpha, 1); assert.equal(e.self.tws_hud_health.value, 50); assert.equal(other.self.tws_hud_hidden, undefined); assert.equal(other.self.tws_hud_health.alpha, 0); - assert.equal(e.elems.length, 11); assert.equal(e.queued.length, 1); + assert.equal(e.elems.length, 10); assert.equal(e.queued.length, 1); e.context.hud_toggle(); e.self.health = 100; run.next(); e.context.hud_toggle(); assert.equal(e.self.tws_hud_health.alpha, 0, 'healing while hidden conceals the readout when shown'); assert.equal(e.self.tws_hud_hp.alpha, 0); assert.equal(e.self.tws_hud_bar.alpha, 1); @@ -254,7 +286,7 @@ for (const game of ['t5', 't6']) { e.context.hud_toggle(); assert.equal(indicator.alpha, 0); exit.next(); assert.equal(e.self.ignoreme, false); assert(e.actions.some(([name]) => name === 'disableInvulnerability')); - assert.equal(e.elems.length, 12); + assert.equal(e.elems.length, 11); }); if (game === 't6') check('t6: AFK release while hidden respects stock protection ownership', () => { const e = make(); e.context.hud_connect(); const watcher = e.spawnAfk(); diff --git a/tools/check_hud.mjs b/tools/check_hud.mjs index 0227709..0a54683 100644 --- a/tools/check_hud.mjs +++ b/tools/check_hud.mjs @@ -45,7 +45,7 @@ function translate(source) { .replace(/(self|level) notify\(([^;]+)\);/g, 'emit($1, $2);') .replace(/self (\w+)\(/g, '$1(') .replace(/\bwait ([\d.]+);/g, 'yield { milliseconds: $1 * 1000 };') - .replace(/([\w.]+(?:\[[^\]\n]+\])?)\s+(SetText|SetValue|SetShader|Destroy)\(/g, '$1.$2(') + .replace(/([\w.]+(?:\[[^\]\n]+\])?)\s+(SetText|SetTextUnlimited|SetValue|SetShader|Destroy)\(/g, '$1.$2(') .replace(/__literal_(\d+)__/g, (_, i) => literals[Number(i)]); result.push(`function${coroutineNames.has(name) ? '*' : ''} ${name}(${match[2]}) { var ${[...locals].join(', ') || 'unused'}; ${body} }`); } @@ -99,6 +99,7 @@ function engine(game, count = 8, withHud = false) { owner, alpha: 1, textCalls: 0, valueCalls: 0, destroyCalls: 0, SetText(text) { assert(!this.destroyCalls); this.text = text; this.textCalls++; }, + SetTextUnlimited(text) { assert(!this.destroyCalls); this.text = text; this.textCalls++; }, SetValue(value) { assert(!this.destroyCalls); this.value = value; this.valueCalls++; }, SetShader(shader, width, height) { assert(!this.destroyCalls); @@ -335,7 +336,7 @@ for (const game of ['t5', 't6']) { assertCounter(e, 1, 1); e.record(snapshot(['A', 'B', 'C', 'D'])); assert(first.every(elem => elem.destroyCalls === 1), 'pagedness changes release the old view first'); - assert.equal(e.state.record_title.text, 'Map record · Round 82'); + assert.equal(e.state.record_title.text, 'Map record | Round 82'); assert.equal(e.live.length, 5); const paged = [...e.live]; assertCounter(e, 1, 2); @@ -380,7 +381,7 @@ for (const game of ['t5', 't6']) { e.advance(11000); assert.equal(e.textCalls, calls); assert.equal(e.elements.length, count); - assert.equal(e.state.record_title.text, 'Map record · Round 82'); + assert.equal(e.state.record_title.text, 'Map record | Round 82'); assert.equal(e.state.commands[3].command, '.join'); assert.match(e.c.help_description(e.state.commands[3]), /newcomer/); assert.equal(e.c.help_command_label(e.state.commands[6].command), '.d '); @@ -522,9 +523,9 @@ check('T5 service transitions preserve actual core element identities, styles, d const e = engine('t5', 8, true); e.createCore(); const core = Array.from(e.self.tws_core_hud_elems); - assert.equal(core.length, 12, 'eleven core elements plus one optional AFK indicator'); - assert.equal(e.live.length, 12); - e.self.tws_hud_count.SetValue(6); + assert.equal(core.length, 11, 'ten core elements plus one optional AFK indicator'); + assert.equal(e.live.length, 11); + e.self.tws_hud_count.SetTextUnlimited('6 ZOMBIES'); e.self.tws_hud_health.SetValue(100); e.self.tws_hud_maxhealth.SetValue(100); e.c.set_core_alpha(e.self, e.self.tws_hud_bar, 1); @@ -536,17 +537,17 @@ check('T5 service transitions preserve actual core element identities, styles, d e.advance(3000); e.record(snapshot(['Jeff', 'Lokester', 'Moniialx'])); const controller = e.state, tasks = e.tasks.length; - assert.equal(e.live.length, 16); + assert.equal(e.live.length, 15); assert.deepEqual(state(), initial); e.advance(12000); assert.equal(e.state, controller); assert.equal(e.state.closed, false, 'idle releases the view, not its controller'); assert.equal(e.state.elems.length, 0); - assert.equal(e.live.length, 12); + assert.equal(e.live.length, 11); assert.deepEqual(state(), initial); for (let i = 0; i < 4; i++) { e.record(snapshot(['One', 'Two', 'Three', 'Four'])); e.advance(15000); e.help(); - assert.equal(e.live.length, 31, '12 core/AFK plus 19 help elements; this is not an engine pool-limit assertion'); + assert.equal(e.live.length, 30, '11 core/AFK plus 19 help elements; this is not an engine pool-limit assertion'); assert.equal(e.live.filter(elem => elem.archived === false).length, 19); e.c.toggle_core_hud(); assert(core.every(elem => elem.alpha === 0)); @@ -556,17 +557,17 @@ check('T5 service transitions preserve actual core element identities, styles, d e.advance(12000); assert.equal(e.state.rows[0].text, 'Latest'); assert.equal(e.state.record.remaining, 12000); - assert.equal(e.live.length, 16); + assert.equal(e.live.length, 15); assert(core.every(elem => elem.alpha === 0 && elem.destroyCalls === 0)); e.c.toggle_core_hud(); assert.deepEqual(state(), initial); e.advance(12000); - assert.equal(e.live.length, 12); + assert.equal(e.live.length, 11); assert.equal(e.state.elems.length, 0); assert.equal(e.tasks.length, tasks, 'reopening reuses the controller and cleanup watchers'); assert.deepEqual(state(), initial); } - assert.equal(e.peakLive, 31); + assert.equal(e.peakLive, 30); assert.deepEqual(Array.from(e.self.tws_core_hud_elems), core); e.event('disconnect'); assert.equal(e.live.length, 0); diff --git a/versions.json b/versions.json index 3536cb9..ecfe7e1 100644 --- a/versions.json +++ b/versions.json @@ -1,16 +1,16 @@ { "tws_t5_afk.gsc": "1.2.0", "tws_t5_bank.gsc": "2.0.3", - "tws_t5_common.gsc": "1.4.0", - "tws_t5_hud.gsc": "1.4.0", + "tws_t5_common.gsc": "1.4.1", + "tws_t5_hud.gsc": "1.4.2", "tws_t5_join.gsc": "1.0.0", "tws_t5_leaderboard.gsc": "1.1.0", "tws_t5_lock.gsc": "1.0.0", "tws_t5_telemetry.gsc": "1.0.0", "tws_t6_afk.gsc": "1.2.0", "tws_t6_bank.gsc": "2.0.3", - "tws_t6_common.gsc": "1.4.0", - "tws_t6_hud.gsc": "1.4.0", + "tws_t6_common.gsc": "1.4.1", + "tws_t6_hud.gsc": "1.4.2", "tws_t6_join.gsc": "1.0.1", "tws_t6_leaderboard.gsc": "1.1.0", "tws_t6_lock.gsc": "1.0.0",