Center zombie counters and use ASCII HUD separators

This commit is contained in:
2026-10-17 16:12:39 -04:00
parent d7bf16e9c2
commit 7b95f604b4
13 changed files with 238 additions and 104 deletions
+54
View File
@@ -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 T5/T6 script and each native plugin. Current versions are recorded in
[versions.json](versions.json); see the [version policy](README.md#versioning). [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 ## 2026-10-15 — Player moderation runtime
- **tws_t5_moderation.gsc 1.0.0 / tws_t6_moderation.gsc 1.0.0**: new optional - **tws_t5_moderation.gsc 1.0.0 / tws_t6_moderation.gsc 1.0.0**: new optional
+26 -16
View File
@@ -362,11 +362,14 @@ settles legacy and public journals without duplicate ledger entries or lost poin
## Core HUD and service window (T5 and T6) ## Core HUD and service window (T5 and T6)
The core uses a light, unboxed layout: match time above round time at the upper 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 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 count centered near the bottom above a slim health bar. The complete
appears to the right of the bar only while injured, then disappears at full `N ZOMBIES` text uses one centered element at the health track's midpoint,
health. The AFK indicator sits above the zombie count. Text uses an outline for keeping the group centered as the count gains or loses digits. It uses a
contrast; there are no panel backgrounds or timer labels. 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 Fullscreen X/Y anchors bypass safe-area insets. Overlay positions and shader
widths use the normalized `640 × 480` viewport directly. Text uses a minimum 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 command restarts page one. Both engines position rows explicitly, avoiding
T5's newline-rendering problem. 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 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 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 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 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 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 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 counter; records use four, or five with a multi-page counter. Hidden elements
still occupy slots. The controller and pending requests are separate from still occupy slots. The controller and pending requests are separate from
display lifetime, so an idle service holds no HUD elements. 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. HUD `1.4.2` requires matching common `1.4.0+` and
Update common, HUD and AFK together where installed, then restart the map. [Plutonium r5334+](https://plutonium.pw/docs/changelog/#r5334) on both games.
Leaderboard `1.1.0` remains compatible. There is no new Plutonium, native-plugin The centered counter uses `SetTextUnlimited` only when the count changes,
or backend requirement, and this change does not require a full server-process avoiding dynamic counter strings in the text config-string pool. Numeric HP
restart. Existing leaderboard plugin requirements still apply. Common retains still uses `SetValue`. On r5334+, deploy the updated HUD script and restart
its older panel helpers for unchanged consumers; the new layout uses fullscreen the map; no full server-process restart is required. On older Plutonium
overlay helpers with the same integer shader-width handling. 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 ### HUD validation
@@ -445,7 +453,9 @@ Before rollout, validate both engines in Plutonium:
legible without panel backgrounds; services must clear the Plutonium tag. legible without panel backgrounds; services must clear the Plutonium tag.
Verify the health-bar fill stays within its track and damage-only HP clears 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 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 2. Join before spawning, join mid-match, die/respawn and reconnect. Verify one
core per player, spectator placeholders, no early match-clock start, correct core per player, spectator placeholders, no early match-clock start, correct
health/Juggernog changes, and frozen timers at game over. HP numbers must 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 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 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 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 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 page time until help closes. A `.leaders` request during help replaces any
+24 -13
View File
@@ -32,12 +32,15 @@ before copying the renamed scripts.
- **`SetTimerUp(N)` only ticks when `N == 0`** on T5 SP. Non-zero seeds - **`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; render "0:00" but don't advance. Late joiners see 0, not real elapsed;
authoritative elapsed lives in `level.match_start_time`. authoritative elapsed lives in `level.match_start_time`.
- **Dynamic numbers use `SetValue`.** Repeatedly generating unique strings - **Dynamic counter text uses `SetTextUnlimited`; HP uses `SetValue`.**
risks exhausting the text config-string pool; identical strings can be reused Repeatedly generating unique strings through `SetText` risks exhausting the
by the engine's [string lookup](https://github.com/MisaDev4/bo1-zombies-decompiled/blob/102f6992be332610aff587e681e3180b15f395bb/src/game_mp/g_utils_mp.cpp#L46). text config-string pool; identical strings can be reused by the engine's
Composite HP uses numeric elements around a static slash and `HP` suffix; [string lookup](https://github.com/MisaDev4/bo1-zombies-decompiled/blob/102f6992be332610aff587e681e3180b15f395bb/src/game_mp/g_utils_mp.cpp#L46).
the group appears only while injured. Help reuses up to nine explicitly The centered `N ZOMBIES` element uses Plutonium r5334+'s `SetTextUnlimited`
positioned command/description pairs, and record rows skip unchanged text. 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 - **`NewClientHudElem(player)`, not `NewHudElem()`.** `NewHudElem` is
globally owned; in co-op every player's HUD draws on top of every other 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 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 from page one. Keep both services independent of the HUD module. Services
never reposition the core, and AFK remains centered above the zombie count. never reposition the core, and AFK remains centered above the zombie count.
- **Bound HUD element counts, including hidden elements.** Core + AFK use - **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), 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. 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 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 polling `self.health` misses the zero-crossing (health is rewritten during
the game-over sequence). the game-over sequence).
HUD `1.4.0` and AFK `1.2.0` require matching common `1.4.0+`. Update all three HUD `1.4.2` requires matching common `1.4.0+` and
where installed and restart the map. Leaderboard `1.1.0` remains compatible. [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`, The full help order is `.help`, `.hud`, `.afk`, `.back`, `.join`, `.leaders`,
`.b`, `.d`, `.w`: one page of nine commands, with full `<amount|all>` bank `.b`, `.d`, `.w`: one page of nine commands, with full `<amount|all>` bank
syntax. `.hud` is absent when HUD is omitted. Records use right-aligned 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. and no single-page counter. Both views retain 12-second page timing.
Fullscreen X/Y overlay anchors use direct normalized `640 × 480` coordinates. Fullscreen X/Y overlay anchors use direct normalized `640 × 480` coordinates.
Dynamic health fills use `make_overlay_width()` like their tracks. Apply 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` `SetShader` requires an integer width. Legacy panel helpers and their `0.75`
horizontal conversion remain available for existing consumers. Font scale is horizontal conversion remain available for existing consumers. Font scale is
at least `1.0`. Numeric HP appears only while injured; the core and services 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 have no panel backgrounds. The counter is one `N ZOMBIES` element centered
dependency or full server-process restart requirement. 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 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 ## Module map
+22 -12
View File
@@ -60,10 +60,13 @@ invalid identities fail privately without changing points. Follow the shared
## Core HUD and service window ## Core HUD and service window
The unboxed core puts match time above round time at the upper left, 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 `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 slim health bar at the bottom center. `N ZOMBIES` is one centered element at
of the bar only while injured; it disappears at full health. The AFK indicator the health track's midpoint, using uniform font scale `1.25` so the whole group
sits above the zombie count. There are no panel backgrounds or timer labels. 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 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 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. 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. and the stock HUD are unaffected. `.hud` requires the HUD module.
Common provides one unboxed upper-right service area below the Plutonium tag. 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 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 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 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 (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 counter). Pages reuse elements; switching views or closing the service releases
them. Idle services hold no HUD elements, while the controller retains pending 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 HUD `1.4.2` requires common `1.4.0+` and
together where installed, then restart the map. Leaderboard `1.1.0` remains [Plutonium r5334+](https://plutonium.pw/docs/changelog/#r5334) for its
compatible. This change requires no new Plutonium/plugin/backend dependency or `SetTextUnlimited` counter, updated only when the count changes. Deploy the
full server-process restart. 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 ## 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 Check that branding, the health bar, damage-only HP, zombie count and timers
remain correct during each view and after it closes. remain correct during each view and after it closes.
Include other installed HUD mods; the live T5 SP element limit is unverified. 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** **Features**
- Upper-left match timer above round timer (round resets each round, both freeze on game over) - 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 - `.hud` toggles this player's core and AFK indicator; data updates continue while hidden
- Top-center `TWS ZOMBIES · discord.tws.lol` branding - Top-center `TWS ZOMBIES | discord.tws.lol` branding
- Bottom-center zombies-remaining counter, with the AFK indicator above it - 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 - Slim health bar with green / amber / red ramp; current/maximum HP appears
beside it only while injured beside it only while injured
- Spectator placeholders without starting the match clock on connection - 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`, `destroy_on_disconnect`, `format_mmss`, `register_help_command`,
`register_spawn_handler`, `register_connect_handler` `register_spawn_handler`, `register_connect_handler`
- Every `level.hud_*` config value from `tws_t5_common` - 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`, - Stock BO1 state: `level.round_number`, `level.zombie_total`,
`level.players`, `self.health`, `self.maxhealth`, AI entities' `.animname` `level.players`, `self.health`, `self.maxhealth`, AI entities' `.animname`
and `.ignore_enemy_count` and `.ignore_enemy_count`
+1 -1
View File
@@ -457,7 +457,7 @@ service_render(state)
{ {
header = "No recorded high round yet"; header = "No recorded high round yet";
if (snapshot.round > 0) 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) if (!isDefined(state.record_header_text) || state.record_header_text != header)
{ {
+7 -6
View File
@@ -52,12 +52,12 @@ hud_connect()
self.tws_hud_round = self hud_text(level.hud_overlay_timer_x, self.tws_hud_round = self hud_text(level.hud_overlay_timer_x,
level.hud_overlay_round_y, "left", 1.25, level.hud_c_dim, "--:--"); 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, 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, // Center the entire readout on the track, regardless of the digit count.
level.hud_overlay_count_y, "right", 1.5, level.hud_c_count, "--"); count_x = level.hud_overlay_bar_x + level.hud_overlay_bar_width / 2.0;
self hud_text(level.hud_overlay_count_label_x, level.hud_overlay_count_label_y, self.tws_hud_count = self hud_text(count_x,
"left", 1.05, level.hud_c_dim, "ZOMBIES"); level.hud_overlay_count_y, "center", 1.25, level.hud_c_count, "-- ZOMBIES");
// Independent numeric elements avoid a new config string on every tick. // Independent numeric elements avoid a new config string on every tick.
// The readout appears only after a valid damaged-health sample. // The readout appears only after a valid damaged-health sample.
@@ -233,7 +233,8 @@ hud_zombie_counter()
if (remaining != last) if (remaining != last)
{ {
count SetValue(remaining); // Plutonium r5334+: reuse text storage as the count changes.
count SetTextUnlimited(int(remaining) + " ZOMBIES");
last = remaining; last = remaining;
} }
+15 -7
View File
@@ -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 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 service or switching kinds destroys the view; idle holds no elements. Pages
reuse the active elements, separately from controller and pending-request state. 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) 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 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. 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 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 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, 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. 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 HUD `1.4.2` requires matching common `1.4.0+` and
where installed and restart the map. Leaderboard `1.1.0` remains compatible. [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 Overlay helpers use direct normalized `640 × 480` coordinates. Dynamic health
fills use `make_overlay_width()` like their tracks. Apply `int()` after `max()` fills use `make_overlay_width()` like their tracks. Apply `int()` after `max()`
in that helper: native `max()` returns a float, but `SetShader` requires an in that helper: native `max()` returns a float, but `SetShader` requires an
integer width. Legacy panel helpers retain their `0.75` horizontal conversion integer width. Legacy panel helpers retain their `0.75` horizontal conversion
for existing consumers. Font scale is at least `1.0`. This introduces no for existing consumers. Font scale is at least `1.0`. There is no new
Plutonium/native-plugin/backend dependency or full server-process restart native-plugin/backend dependency or full server-process restart requirement
requirement. when already using Plutonium r5334+.
HUD owns the exact, case-insensitive `.hud` chat command and registers it in 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 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, spectator placeholders without an early clock start. Check damage-only HP,
health/Juggernog changes, AFK placement and timers frozen at game over. health/Juggernog changes, AFK placement and timers frozen at game over.
Test safe-area settings and different resolutions against stock HUD elements. 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 2. `.afk` blocks movement, shooting and grenades; `.back` restores them. Try rapid
toggles, a round transition, death/respawn and game over. 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. 3. Lock fires at the threshold and clears on game over/restart. HUD must be installed.
+17 -11
View File
@@ -8,7 +8,7 @@ private server token.
| File | Role | Dependencies | | File | Role | Dependencies |
| --- | --- | --- | | --- | --- | --- |
| `tws_t6_common.gsc` | Shared HUD values, helpers, paginated `.help`, service window, connection and first-spawn dispatchers | Stock T6 | | `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_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_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_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 ## Core HUD and service window
The unboxed core puts match time above round time at the upper left, 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 `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 slim health bar at the bottom center. `N ZOMBIES` is one centered element at
of the bar only while injured; it disappears at full health. The AFK indicator the health track's midpoint, using uniform font scale `1.25` so the whole group
sits above the zombie count. There are no panel backgrounds or timer labels. 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 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 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. 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. and the stock HUD are unaffected. `.hud` requires the HUD module.
Common provides one unboxed upper-right service area below the Plutonium tag. 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 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 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 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 (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 counter). Pages reuse elements; switching views or closing the service releases
them. Idle services hold no HUD elements, while the controller retains pending 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 HUD `1.4.2` requires common `1.4.0+` and
together where installed, then restart the map. Leaderboard `1.1.0` remains [Plutonium r5334+](https://plutonium.pw/docs/changelog/#r5334) for its
compatible. This change requires no new Plutonium/plugin/backend dependency or `SetTextUnlimited` counter, updated only when the count changes. Deploy the
full server-process restart. 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 ## Map record announcements
+1 -1
View File
@@ -453,7 +453,7 @@ service_render(state)
{ {
header = "No recorded high round yet"; header = "No recorded high round yet";
if (snapshot.round > 0) 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) if (!isDefined(state.record_header_text) || state.record_header_text != header)
{ {
+7 -6
View File
@@ -54,12 +54,12 @@ hud_connect()
self.tws_hud_round = self hud_text(level.hud_overlay_timer_x, self.tws_hud_round = self hud_text(level.hud_overlay_timer_x,
level.hud_overlay_round_y, "left", 1.25, level.hud_c_dim, "--:--"); 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, 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, // Center the entire readout on the track, regardless of the digit count.
level.hud_overlay_count_y, "right", 1.5, level.hud_c_count, "--"); count_x = level.hud_overlay_bar_x + level.hud_overlay_bar_width / 2.0;
self hud_text(level.hud_overlay_count_label_x, level.hud_overlay_count_label_y, self.tws_hud_count = self hud_text(count_x,
"left", 1.05, level.hud_c_dim, "ZOMBIES"); level.hud_overlay_count_y, "center", 1.25, level.hud_c_count, "-- ZOMBIES");
// Independent numeric elements avoid a new config string on every tick. // Independent numeric elements avoid a new config string on every tick.
// The readout appears only after a valid damaged-health sample. // The readout appears only after a valid damaged-health sample.
@@ -239,7 +239,8 @@ hud_zombie_counter()
if (remaining != last) if (remaining != last)
{ {
count SetValue(remaining); // Plutonium r5334+: reuse text storage as the count changes.
count SetTextUnlimited(int(remaining) + " ZOMBIES");
last = remaining; last = remaining;
} }
+47 -15
View File
@@ -26,11 +26,12 @@ function install(context, source, names) {
const locals = [...new Set([...code.matchAll(/(?<![\w.])(\w+)\s*(?:=(?!=)|\+\+|--)/g)].map(match => match[1]))]; const locals = [...new Set([...code.matchAll(/(?<![\w.])(\w+)\s*(?:=(?!=)|\+\+|--)/g)].map(match => match[1]))];
code = code.replace(/(?:self|level) endon\([^;]+;/g, '') code = code.replace(/(?:self|level) endon\([^;]+;/g, '')
.replace(/scripts\\(?:sp|zm)\\tws_t[56]_common::/g, 'common.') .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 thread ([\w.]+)\(([^;]*)\);/g, (_, fn, values) => `queued.push(['${fn}', ${values}]);`)
.replace(/self common\.toggle_core_hud\(\)/g, 'common.toggle_core_hud()') .replace(/self common\.toggle_core_hud\(\)/g, 'common.toggle_core_hud()')
.replace(/self ((?:hud_|afk_)\w+)\(/g, '$1(') .replace(/self ((?:hud_|afk_)\w+)\(/g, '$1(')
.replace(/self (\w+)\(/g, 'self.$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 notify\(([^;]*)\);/g, 'level.notify($1);')
.replace(/level waittill\(([^;]*)\);/g, 'yield { event: $1 };') .replace(/level waittill\(([^;]*)\);/g, 'yield { event: $1 };')
.replace(/\.size\b/g, '.length') .replace(/\.size\b/g, '.length')
@@ -51,6 +52,7 @@ function engine(game, source, afk, hidden = false) {
function element(args, alpha = 1) { function element(args, alpha = 1) {
const result = { args, alpha, writes: [], const result = { args, alpha, writes: [],
SetText(value) { this.text = value; this.writes.push(['text', value]); }, 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]); }, SetValue(value) { this.value = value; this.writes.push(['value', value]); },
SetShader(...args) { this.shader = args; }, SetShader(...args) { this.shader = args; },
SetTimerUp(value) { this.timer = value; this.writes.push(['timer', value]); }, 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, player_active: player => player.active,
format_mmss: ms => `${Math.floor(ms / 60000)}:${String(Math.floor(ms / 1000) % 60).padStart(2, '0')}`, 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_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_brand_x: 320, hud_overlay_brand_y: 8,
hud_overlay_count_x: 300, hud_overlay_count_y: 440, 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_count: 'white', hud_c_dim: 'dim', hud_c_afk: 'yellow',
hud_c_high: 'green', hud_c_mid: 'yellow', hud_c_low: 'red', notify() {} }; hud_c_high: 'green', hud_c_mid: 'yellow', hud_c_low: 'red', notify() {} };
const context = vm.createContext({ self, level, common, queued, getTime: () => now, 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, isDefined: value => value !== undefined && value !== null, is_true: value => !!value,
int: Math.trunc, max: Math.max }); int: Math.trunc, max: Math.max });
install(context, source, ['hud_connect', 'hud_text', 'hud_start', 'hud_toggle', 'hud_timer_row', 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', install(context, afk, ['afk_spawn', 'afk_enter', 'afk_exit',
...(game === 't6' ? ['afk_release', 'afk_lock_controls', 'afk_restore_controls'] : [])]); ...(game === 't6' ? ['afk_release', 'afk_lock_controls', 'afk_restore_controls'] : [])]);
return { context, self, level, queued, elems, actions, common, 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(chat.includes('player thread hud_toggle()'), 'disconnect scope stays in the player worker');
assert(!body(source, 'hud_connect')[1].includes('getTime()')); assert(!body(source, 'hud_connect')[1].includes('getTime()'));
assert(!source.includes('tws_hud_hidden ='), 'spawn/construction must not reset the preference'); 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")')); assert(body(source, name)[1].includes('level endon("hud_game_over")'));
const bind = body(source, 'match_timer_bind')[1]; const bind = body(source, 'match_timer_bind')[1];
if (game === 't5') assert(bind.includes('elem SetTimerUp(0)')); 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(afk.includes('level.hud_overlay_afk_y'));
assert(!/\.alpha\s*=/.test(afk), 'AFK visibility must use the common mask'); 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(); 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.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.self.tws_hud_match.text, '--:--'); assert.equal(e.self.tws_hud_health.text, '--');
assert.equal(e.elems.length, 11); assert.equal(e.elems.length, 10);
assert.equal(e.self.tws_core_hud_elems.length, 11); assert.equal(e.self.tws_core_hud_elems.length, 10);
e.context.hud_connect(); assert.equal(e.elems.length, 11); 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.length, 1); assert.equal(e.queued[0][0], 'common.destroy_on_disconnect');
assert.equal(e.queued[0][1].length, e.elems.length); assert.equal(e.queued[0][1].length, e.elems.length);
const textAt = (text, x, y, align, scale, colour) => { 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_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']); 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'); 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', 320, 440, 'center', 1.25, 'white');
textAt('ZOMBIES', 306, 446, 'left', 1.05, 'dim'); assert(!e.elems.some(elem => elem.text === 'ZOMBIES'), 'count and label share one alignment');
textAt('HP', 449, 460, 'left', 1, 'white'); textAt('HP', 449, 460, 'left', 1, 'white');
for (const [key, x, align] of [['health', 414, 'right'], ['slash', 418, 'left'], ['maxhealth', 445, 'right']]) { for (const [key, x, align] of [['health', 414, 'right'], ['slash', 418, 'left'], ['maxhealth', 445, 'right']]) {
const elem = e.self[`tws_hud_${key}`]; 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.level.match_start_time, undefined); assert.equal(e.level.round_start_time, undefined);
assert.equal(e.queued.length, 1, 'toggle only queues disconnect cleanup'); assert.equal(e.queued.length, 1, 'toggle only queues disconnect cleanup');
e.context.hud_start(); e.context.hud_start(); 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']); assert.deepEqual(e.queued.slice(1).map(item => item[0]), ['hud_timer_row', 'hud_zombie_counter', 'hud_health_display']);
e.context.hud_timer_row(); e.context.hud_timer_row();
assert.equal(e.level.match_start_time, 123456); assert.equal(e.level.round_start_time, 123456); assert.equal(e.level.match_start_time, 123456); assert.equal(e.level.round_start_time, 123456);
const threads = e.queued.length; const threads = e.queued.length;
e.context.hud_toggle(); e.context.hud_toggle(); e.context.hud_start(); 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`, () => { 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; const e = make(); e.context.hud_connect(); const { self } = e;
@@ -216,7 +248,7 @@ for (const game of ['t5', 't6']) {
e.context.hud_toggle(); e.context.hud_toggle();
assert.equal(e.self.tws_hud_bar.alpha, 1); assert.equal(e.self.tws_hud_health.value, 50); 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(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(); 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_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); 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); e.context.hud_toggle(); assert.equal(indicator.alpha, 0);
exit.next(); assert.equal(e.self.ignoreme, false); exit.next(); assert.equal(e.self.ignoreme, false);
assert(e.actions.some(([name]) => name === 'disableInvulnerability')); 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', () => { if (game === 't6') check('t6: AFK release while hidden respects stock protection ownership', () => {
const e = make(); e.context.hud_connect(); const watcher = e.spawnAfk(); const e = make(); e.context.hud_connect(); const watcher = e.spawnAfk();
+13 -12
View File
@@ -45,7 +45,7 @@ function translate(source) {
.replace(/(self|level) notify\(([^;]+)\);/g, 'emit($1, $2);') .replace(/(self|level) notify\(([^;]+)\);/g, 'emit($1, $2);')
.replace(/self (\w+)\(/g, '$1(') .replace(/self (\w+)\(/g, '$1(')
.replace(/\bwait ([\d.]+);/g, 'yield { milliseconds: $1 * 1000 };') .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)]); .replace(/__literal_(\d+)__/g, (_, i) => literals[Number(i)]);
result.push(`function${coroutineNames.has(name) ? '*' : ''} ${name}(${match[2]}) { var ${[...locals].join(', ') || 'unused'}; ${body} }`); 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, owner,
alpha: 1, textCalls: 0, valueCalls: 0, destroyCalls: 0, alpha: 1, textCalls: 0, valueCalls: 0, destroyCalls: 0,
SetText(text) { assert(!this.destroyCalls); this.text = text; this.textCalls++; }, 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++; }, SetValue(value) { assert(!this.destroyCalls); this.value = value; this.valueCalls++; },
SetShader(shader, width, height) { SetShader(shader, width, height) {
assert(!this.destroyCalls); assert(!this.destroyCalls);
@@ -335,7 +336,7 @@ for (const game of ['t5', 't6']) {
assertCounter(e, 1, 1); assertCounter(e, 1, 1);
e.record(snapshot(['A', 'B', 'C', 'D'])); e.record(snapshot(['A', 'B', 'C', 'D']));
assert(first.every(elem => elem.destroyCalls === 1), 'pagedness changes release the old view first'); 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); assert.equal(e.live.length, 5);
const paged = [...e.live]; const paged = [...e.live];
assertCounter(e, 1, 2); assertCounter(e, 1, 2);
@@ -380,7 +381,7 @@ for (const game of ['t5', 't6']) {
e.advance(11000); e.advance(11000);
assert.equal(e.textCalls, calls); assert.equal(e.textCalls, calls);
assert.equal(e.elements.length, count); 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.equal(e.state.commands[3].command, '.join');
assert.match(e.c.help_description(e.state.commands[3]), /newcomer/); assert.match(e.c.help_description(e.state.commands[3]), /newcomer/);
assert.equal(e.c.help_command_label(e.state.commands[6].command), '.d <amount|all>'); assert.equal(e.c.help_command_label(e.state.commands[6].command), '.d <amount|all>');
@@ -522,9 +523,9 @@ check('T5 service transitions preserve actual core element identities, styles, d
const e = engine('t5', 8, true); const e = engine('t5', 8, true);
e.createCore(); e.createCore();
const core = Array.from(e.self.tws_core_hud_elems); 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(core.length, 11, 'ten core elements plus one optional AFK indicator');
assert.equal(e.live.length, 12); assert.equal(e.live.length, 11);
e.self.tws_hud_count.SetValue(6); e.self.tws_hud_count.SetTextUnlimited('6 ZOMBIES');
e.self.tws_hud_health.SetValue(100); e.self.tws_hud_health.SetValue(100);
e.self.tws_hud_maxhealth.SetValue(100); e.self.tws_hud_maxhealth.SetValue(100);
e.c.set_core_alpha(e.self, e.self.tws_hud_bar, 1); 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.advance(3000);
e.record(snapshot(['Jeff', 'Lokester', 'Moniialx'])); e.record(snapshot(['Jeff', 'Lokester', 'Moniialx']));
const controller = e.state, tasks = e.tasks.length; const controller = e.state, tasks = e.tasks.length;
assert.equal(e.live.length, 16); assert.equal(e.live.length, 15);
assert.deepEqual(state(), initial); assert.deepEqual(state(), initial);
e.advance(12000); e.advance(12000);
assert.equal(e.state, controller); assert.equal(e.state, controller);
assert.equal(e.state.closed, false, 'idle releases the view, not its controller'); assert.equal(e.state.closed, false, 'idle releases the view, not its controller');
assert.equal(e.state.elems.length, 0); assert.equal(e.state.elems.length, 0);
assert.equal(e.live.length, 12); assert.equal(e.live.length, 11);
assert.deepEqual(state(), initial); assert.deepEqual(state(), initial);
for (let i = 0; i < 4; i++) { for (let i = 0; i < 4; i++) {
e.record(snapshot(['One', 'Two', 'Three', 'Four'])); e.advance(15000); e.help(); 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); assert.equal(e.live.filter(elem => elem.archived === false).length, 19);
e.c.toggle_core_hud(); e.c.toggle_core_hud();
assert(core.every(elem => elem.alpha === 0)); 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); e.advance(12000);
assert.equal(e.state.rows[0].text, 'Latest'); assert.equal(e.state.rows[0].text, 'Latest');
assert.equal(e.state.record.remaining, 12000); 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)); assert(core.every(elem => elem.alpha === 0 && elem.destroyCalls === 0));
e.c.toggle_core_hud(); e.c.toggle_core_hud();
assert.deepEqual(state(), initial); assert.deepEqual(state(), initial);
e.advance(12000); 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.state.elems.length, 0);
assert.equal(e.tasks.length, tasks, 'reopening reuses the controller and cleanup watchers'); assert.equal(e.tasks.length, tasks, 'reopening reuses the controller and cleanup watchers');
assert.deepEqual(state(), initial); 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); assert.deepEqual(Array.from(e.self.tws_core_hud_elems), core);
e.event('disconnect'); e.event('disconnect');
assert.equal(e.live.length, 0); assert.equal(e.live.length, 0);
+4 -4
View File
@@ -1,16 +1,16 @@
{ {
"tws_t5_afk.gsc": "1.2.0", "tws_t5_afk.gsc": "1.2.0",
"tws_t5_bank.gsc": "2.0.3", "tws_t5_bank.gsc": "2.0.3",
"tws_t5_common.gsc": "1.4.0", "tws_t5_common.gsc": "1.4.1",
"tws_t5_hud.gsc": "1.4.0", "tws_t5_hud.gsc": "1.4.2",
"tws_t5_join.gsc": "1.0.0", "tws_t5_join.gsc": "1.0.0",
"tws_t5_leaderboard.gsc": "1.1.0", "tws_t5_leaderboard.gsc": "1.1.0",
"tws_t5_lock.gsc": "1.0.0", "tws_t5_lock.gsc": "1.0.0",
"tws_t5_telemetry.gsc": "1.0.0", "tws_t5_telemetry.gsc": "1.0.0",
"tws_t6_afk.gsc": "1.2.0", "tws_t6_afk.gsc": "1.2.0",
"tws_t6_bank.gsc": "2.0.3", "tws_t6_bank.gsc": "2.0.3",
"tws_t6_common.gsc": "1.4.0", "tws_t6_common.gsc": "1.4.1",
"tws_t6_hud.gsc": "1.4.0", "tws_t6_hud.gsc": "1.4.2",
"tws_t6_join.gsc": "1.0.1", "tws_t6_join.gsc": "1.0.1",
"tws_t6_leaderboard.gsc": "1.1.0", "tws_t6_leaderboard.gsc": "1.1.0",
"tws_t6_lock.gsc": "1.0.0", "tws_t6_lock.gsc": "1.0.0",