Simplify T5 and T6 HUD overlays and service displays
This commit is contained in:
+262
@@ -4,8 +4,270 @@ 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-15
|
||||
|
||||
### Replace the dashboard panels with unboxed HUD overlays
|
||||
|
||||
| Component | Version | Affected source |
|
||||
| --- | --- | --- |
|
||||
| `tws_t5_common.gsc` | `1.4.0` | `t5/scripts/tws_t5_common.gsc` |
|
||||
| `tws_t6_common.gsc` | `1.4.0` | `t6/scripts/tws_t6_common.gsc` |
|
||||
| `tws_t5_hud.gsc` | `1.4.0` | `t5/scripts/tws_t5_hud.gsc` |
|
||||
| `tws_t6_hud.gsc` | `1.4.0` | `t6/scripts/tws_t6_hud.gsc` |
|
||||
| `tws_t5_afk.gsc` | `1.2.0` | `t5/scripts/tws_t5_afk.gsc` |
|
||||
| `tws_t6_afk.gsc` | `1.2.0` | `t6/scripts/tws_t6_afk.gsc` |
|
||||
|
||||
- Both games use the supplied minimal layout: unlabeled match/round timers at
|
||||
upper left, combined branding at top center, zombies above a slim health bar
|
||||
at bottom center, and numeric current/maximum HP to its right only while
|
||||
injured. Center the AFK indicator above the zombie count. Remove dashboard
|
||||
backgrounds while retaining outlined text and health colors.
|
||||
- Keep `.hud` as a private visibility toggle for the entire core and AFK
|
||||
indicator. Hidden data updates, frozen game-over values, connection/spawn
|
||||
lifecycle, AFK protection and stock HUD behavior are unchanged.
|
||||
- Move services into an unboxed upper-right area below the Plutonium tag.
|
||||
Records use a compact right-aligned `Map record · Round N` header and up to
|
||||
three existing bounded holder lines, with no footer or single-page counter.
|
||||
Record data, cache behavior, all tied holders and 12-second paging remain
|
||||
unchanged; leaderboard source and versions are unchanged.
|
||||
- Show all nine installed commands, including `.hud`, in one help list with
|
||||
full `.d <amount|all>` / `.w <amount|all>` syntax. Both engines use explicit
|
||||
command/description rows; extra registrations use pages of nine, with a
|
||||
counter only for multi-page lists. Preserve private requests, help priority,
|
||||
record pause/resume, replay replacement and 12-second page timing.
|
||||
- Allocate only the active service view in both engines and destroy it when
|
||||
idle or before switching kinds. Core + AFK use 11 + 1 archived elements;
|
||||
nine-command help uses 19 nonarchived elements (20 with a multi-page counter),
|
||||
and records use four (five with a multi-page counter). Idle holds none.
|
||||
These counts do not establish live Plutonium rendering capacity.
|
||||
- Common adds fullscreen overlay helpers using direct normalized `640 × 480`
|
||||
positions and integer shader dimensions. Retain legacy panel helpers for
|
||||
compatibility. Both HUD and AFK implementations use the new helpers.
|
||||
- Validation: `node tools/check_hud.mjs` passed 15 core HUD/AFK and 56 service/
|
||||
static checks (71 total). Leaderboard passed 143 checks, bank 42, join 17 and
|
||||
telemetry 23 through their portable check scripts. JSON uniqueness, the
|
||||
16-script manifest, all six version/changelog matches, 66 local documentation
|
||||
links/anchors and `git diff --check` passed. Checks use engine shims; actual
|
||||
EC bank integration was not run. GSC compilation was not run because
|
||||
gsc-tool/PowerShell are unavailable; live Windows/Plutonium validation remains
|
||||
outstanding.
|
||||
- Compatibility: HUD `1.4.0` and AFK `1.2.0` require matching common `1.4.0+`.
|
||||
Both engines receive companion changes; leaderboard `1.1.0` remains
|
||||
compatible. No new Plutonium, native-plugin or backend requirement.
|
||||
- Deployment: copy each game's common, HUD and AFK scripts together where
|
||||
installed, then restart the map. No full server-process restart or plugin
|
||||
update is required. No deployment performed. Windows/Plutonium layout,
|
||||
element capacity, stock HUD overlap and lifecycle validation remain required.
|
||||
|
||||
## 2026-10-14
|
||||
|
||||
### Release T5 service elements when records or help close
|
||||
|
||||
| Component | Version | Affected source |
|
||||
| --- | --- | --- |
|
||||
| `tws_t5_common.gsc` | `1.3.1` | `t5/scripts/tws_t5_common.gsc` |
|
||||
|
||||
- Address core HUD elements disappearing when the first record announcement
|
||||
opens and remaining absent after it closes. The previous T5 service allocated
|
||||
all 24 help/record elements at once and retained them while idle. That pattern
|
||||
is consistent with HUD allocation pressure; the live T5 SP capacity has not
|
||||
been established.
|
||||
- Allocate only the active service view, with at most 12 elements for help or
|
||||
11 for records. Release the elements when the service becomes idle and before
|
||||
switching between help and records. Reuse them for pages within the same
|
||||
view, and render the page indicator as one cached text element. The single
|
||||
controller, help priority, paused record timing and request replacement remain
|
||||
intact. Core elements and their visibility setting are unchanged.
|
||||
- T6 needs no source change: the reported failure is on T5, whose explicit help
|
||||
rows expanded its former allocation to 24 elements. T6 retains its smaller
|
||||
18-element multiline renderer; validate its existing behavior separately.
|
||||
- Validation: `node tools/check_hud.mjs` passed 15 core HUD/AFK and 53 service/
|
||||
static checks (68 total), including active-view allocation, idle release,
|
||||
repeated service cycles and core preservation. Leaderboard passed 143 checks,
|
||||
bank 42, join 17 and telemetry 23 through their portable check scripts. These
|
||||
use engine shims; actual EC bank integration was not run. Live Windows/
|
||||
Plutonium rendering validation was not performed.
|
||||
- Compatibility: HUD `1.3.0`, AFK `1.1.0` and leaderboard `1.1.0` remain
|
||||
compatible. No new dependency requirement. Deployment: copy only
|
||||
`tws_t5_common.gsc` and restart the map. No plugin update or full server-process
|
||||
restart is required. No deployment performed; live T5 validation must confirm
|
||||
core integrity during automatic records, help, view switches and dismissal.
|
||||
|
||||
### Add a per-player HUD visibility command
|
||||
|
||||
| Component | Version | Affected source |
|
||||
| --- | --- | --- |
|
||||
| `tws_t5_common.gsc` | `1.3.0` | `t5/scripts/tws_t5_common.gsc` |
|
||||
| `tws_t6_common.gsc` | `1.3.0` | `t6/scripts/tws_t6_common.gsc` |
|
||||
| `tws_t5_hud.gsc` | `1.3.0` | `t5/scripts/tws_t5_hud.gsc` |
|
||||
| `tws_t6_hud.gsc` | `1.3.0` | `t6/scripts/tws_t6_hud.gsc` |
|
||||
| `tws_t5_afk.gsc` | `1.1.0` | `t5/scripts/tws_t5_afk.gsc` |
|
||||
| `tws_t6_afk.gsc` | `1.1.0` | `t6/scripts/tws_t6_afk.gsc` |
|
||||
|
||||
- Both HUD modules handle the exact, case-insensitive `.hud` chat command to
|
||||
toggle that player's core branding, timers, zombie count, HP/bar and AFK
|
||||
indicator. Visibility defaults on, persists across death/respawn on the
|
||||
current connection/map, and resets on reconnect or map restart.
|
||||
- Common tracks core elements and their intended alpha values so changing
|
||||
visibility preserves each element's state. Data and timers continue updating
|
||||
while hidden, and game-over values remain frozen. AFK routes only its
|
||||
indicator visibility through these helpers; protection behavior is unchanged.
|
||||
Help, records and stock HUD elements are unaffected.
|
||||
- Register `.hud` in help only when the HUD module is installed. The full
|
||||
command order is `.help`, `.hud`, `.afk`, `.back`, `.join`, `.leaders`, `.b`,
|
||||
`.d`, `.w`: nine commands across pages of four, four and one. T5 retains
|
||||
explicit help rows; T6 retains multiline columns.
|
||||
- Compatibility: HUD `1.3.0` and AFK `1.1.0` require their matching common
|
||||
`1.3.0+`. Both engines receive companion implementations. No new Plutonium,
|
||||
native-plugin or backend dependency requirement.
|
||||
- Deployment: update each game's common, HUD and AFK scripts together where
|
||||
installed, then restart the map. No plugin update or full server-process
|
||||
restart is required. No deployment performed; GSC compilation and live
|
||||
T5/T6 validation remain required.
|
||||
- Validation: `node tools/check_hud.mjs` passed 15 core HUD/AFK and 48 service/
|
||||
static checks (63 total), covering private visibility, intended opacity,
|
||||
late-created elements, hidden updates/frozen clocks, AFK transitions, service
|
||||
independence, conditional help and three-page ordering. Leaderboard passed
|
||||
143 checks, bank 42, join 17 and telemetry 23 through their existing portable
|
||||
check scripts. These use engine shims; actual EC bank integration was not run.
|
||||
GSC compiler and PowerShell were unavailable, and live Windows/Plutonium
|
||||
validation was not performed.
|
||||
|
||||
### Render T5 help as separate rows
|
||||
|
||||
| Component | Version | Affected source |
|
||||
| --- | --- | --- |
|
||||
| `tws_t5_common.gsc` | `1.2.6` | `t5/scripts/tws_t5_common.gsc` |
|
||||
|
||||
- Replace T5's newline-separated help columns with four preallocated
|
||||
command/description pairs, positioned on separate rows. This addresses BO1
|
||||
rendering the newline text as one concatenated line. Reuse the row elements
|
||||
across pages and hide unused rows on short pages.
|
||||
- Preserve panel dimensions, four commands per page, 12-second timing and the
|
||||
removed help footer. T5's service now uses 24 nonarchived HUD elements.
|
||||
T6 needs no source change: its existing multiline renderer remains in place
|
||||
with 18 service elements; the reported rendering issue was observed in T5.
|
||||
- Validation: `node tools/check_hud.mjs` passed 8 core and 42 service checks
|
||||
(50 total), covering both engines' pagination and T5's distinct row elements,
|
||||
partial-page hiding, reuse, record transitions and disconnect cleanup.
|
||||
GSC compilation and live T5 display/notification validation were not run.
|
||||
- Compatibility: existing HUD `1.2.1` remains compatible; no new dependency
|
||||
requirement. Deployment: copy only the updated T5 common script and restart
|
||||
the map. No plugin update or full server-process restart is required.
|
||||
No deployment performed; live T5 display validation remains required.
|
||||
|
||||
### Remove the help footer
|
||||
|
||||
| Component | Version | Affected source |
|
||||
| --- | --- | --- |
|
||||
| `tws_t5_common.gsc` | `1.2.5` | `t5/scripts/tws_t5_common.gsc` |
|
||||
| `tws_t6_common.gsc` | `1.2.5` | `t6/scripts/tws_t6_common.gsc` |
|
||||
|
||||
- Both games remove the `Four commands per page` footer and its HUD element.
|
||||
Help still displays four commands per page with its page indicator and
|
||||
existing timing. The shared service now uses 18 nonarchived elements.
|
||||
- Validation: `node tools/check_hud.mjs` passed 8 core and 41 service checks
|
||||
(49 total), including the 18-element allocation and removed help footer.
|
||||
GSC compilation and live Plutonium validation were not run.
|
||||
- Compatibility: existing HUD `1.2.1` remains compatible; no new dependency
|
||||
requirement. Deployment: copy only each game's updated common script and
|
||||
restart the map. No plugin update or full server-process restart is required.
|
||||
No deployment performed; live T5/T6 display validation remains required.
|
||||
|
||||
### Hide the page indicator for single-page records
|
||||
|
||||
| Component | Version | Affected source |
|
||||
| --- | --- | --- |
|
||||
| `tws_t5_common.gsc` | `1.2.4` | `t5/scripts/tws_t5_common.gsc` |
|
||||
| `tws_t6_common.gsc` | `1.2.4` | `t6/scripts/tws_t6_common.gsc` |
|
||||
|
||||
- Both games hide `1 / 1` when the record service has only one page. Records
|
||||
spanning multiple pages and all help pages retain their page indicators.
|
||||
- Validation: `node tools/check_hud.mjs` passed 8 core and 41 service checks
|
||||
(49 total), including empty/unavailable records, multi-page indicators,
|
||||
replacement by a single-page record, resuming after help, and single-page
|
||||
help indicators. GSC compilation and live Plutonium validation were not run.
|
||||
- Compatibility: existing HUD `1.2.1` remains compatible; no new dependency
|
||||
requirement. Deployment: copy only each game's updated common script and
|
||||
restart the map. No plugin update or full server-process restart is required.
|
||||
No deployment performed; live T5/T6 display validation remains required.
|
||||
|
||||
### Restore panel backgrounds and health bars
|
||||
|
||||
| Component | Version | Affected source |
|
||||
| --- | --- | --- |
|
||||
| `tws_t5_common.gsc` | `1.2.3` | `t5/scripts/tws_t5_common.gsc` |
|
||||
| `tws_t6_common.gsc` | `1.2.3` | `t6/scripts/tws_t6_common.gsc` |
|
||||
|
||||
- Both engines apply the final integer conversion after `max()` in
|
||||
`make_panel_width()`. Native `max()` returns a float, which `SetShader`
|
||||
rejects for its integer width argument; converting before `max()` left
|
||||
backgrounds and health-bar shaders missing. The corrected helper supplies
|
||||
integer widths to both panel creation and health-bar updates.
|
||||
- Retain panel color `(0.07, 0.10, 0.08)`, opacity `0.72`, and the corrected
|
||||
text positions/font sizes. HUD source is unchanged and remains `1.2.1`.
|
||||
- Validation: `node tools/check_hud.mjs` passed 8 core and 41 service checks
|
||||
(49 total). The new regression models native float typing and strict integer
|
||||
shader widths, and checks the existing panel color/opacity. GSC compiler and
|
||||
PowerShell were unavailable; GSC compilation and live Windows/Plutonium display
|
||||
validation were not performed.
|
||||
- Compatibility: use matching common `1.2.3+` with HUD `1.2.1`; no new
|
||||
Plutonium, native-plugin or backend dependency requirement. Deployment: if
|
||||
HUD `1.2.1` is already installed, copy only each game's updated common script
|
||||
and restart the map. No plugin update or full server-process restart is
|
||||
required. No deployment performed; verify restored shaders in T5 and T6.
|
||||
|
||||
### Correct footer font size and screen-edge panel placement
|
||||
|
||||
| Component | Version | Affected source |
|
||||
| --- | --- | --- |
|
||||
| `tws_t5_common.gsc` | `1.2.2` | `t5/scripts/tws_t5_common.gsc` |
|
||||
| `tws_t6_common.gsc` | `1.2.2` | `t6/scripts/tws_t6_common.gsc` |
|
||||
| `tws_t5_hud.gsc` | `1.2.1` | `t5/scripts/tws_t5_hud.gsc` |
|
||||
| `tws_t6_hud.gsc` | `1.2.1` | `t6/scripts/tws_t6_hud.gsc` |
|
||||
|
||||
- Both engines enforce a minimum text font scale of `1.0`, with both service
|
||||
footers explicitly at `1.0`, addressing the oversized `.leaders` footer.
|
||||
- Use fullscreen horizontal and vertical anchors so panels follow the actual
|
||||
upper-right screen edge instead of safe-area insets. Convert horizontal
|
||||
design offsets and shader widths by `0.75` into the normalized 640-wide
|
||||
viewport; use a top offset of 20. The 1920×1080 reference yields a 495-pixel
|
||||
panel, 22.5-pixel right margin and 45-pixel top margin, leaving space for the
|
||||
Plutonium tag. Other aspect ratios retain normalized horizontal geometry,
|
||||
so physical proportions vary; live font fit and tag clearance need validation.
|
||||
- Both HUD scripts use common's new `make_panel_width(width)` for health-bar
|
||||
updates so the fill uses the same width conversion as its track. T5 and T6
|
||||
receive the same fix with their matching namespaces.
|
||||
- Compatibility: HUD `1.2.1` requires matching common `1.2.2+`; upgrade common
|
||||
and HUD together. No new Plutonium, native-plugin or backend dependency is
|
||||
required.
|
||||
- Deployment: copy each game's updated common and HUD scripts and restart the
|
||||
map. No full server-process restart is required. No deployment performed;
|
||||
GSC compilation and live Windows/Plutonium display validation remain required.
|
||||
- Validation: `node tools/check_hud.mjs` passed 8 core and 39 service/static
|
||||
geometry checks (47 total); `node tools/check_leaderboard.mjs` passed 143
|
||||
checks. The geometry shims cover 4:3, 16:9 and ultrawide bounds in a normalized
|
||||
640×480 frame, not live glyph fit or tag clearance. GSC compiler and PowerShell
|
||||
were unavailable; no GSC compilation or live display validation was performed.
|
||||
|
||||
### Shorter map-record footer
|
||||
|
||||
| Component | Version | Affected source |
|
||||
| --- | --- | --- |
|
||||
| `tws_t5_common.gsc` | `1.2.1` | `t5/scripts/tws_t5_common.gsc` |
|
||||
| `tws_t6_common.gsc` | `1.2.1` | `t6/scripts/tws_t6_common.gsc` |
|
||||
|
||||
- Both games remove `Briefly shown` and its separator from the map-record
|
||||
footer, retaining `.leaders to view again`. Record timing and replay behavior
|
||||
are unchanged.
|
||||
- Validation: `node tools/check_hud.mjs` passed 8 core and 35 service checks
|
||||
using engine shims. GSC compilation and live Plutonium display validation
|
||||
were not run.
|
||||
- Compatibility: no new dependency requirement. Deployment: copy each game's
|
||||
updated common script and restart the map. No plugin update or full
|
||||
server-process restart is required. No deployment performed; GSC compilation
|
||||
and live T5/T6 display validation remain required.
|
||||
|
||||
### Fixed upper-right core HUD and shared service window
|
||||
|
||||
| Component | Version | Affected source |
|
||||
|
||||
@@ -361,39 +361,69 @@ settles legacy and public journals without duplicate ledger entries or lost poin
|
||||
|
||||
## Core HUD and service window (T5 and T6)
|
||||
|
||||
The HUD uses a fixed upper-right panel below the top edge, leaving room above
|
||||
for performance overlays and below for stock player scores, ammo and equipment.
|
||||
The header contains `TWS ZOMBIES` and `discord.tws.lol`; match and round timers
|
||||
share the next row. Zombie count and current/maximum HP sit above a full-width
|
||||
health bar. HP numbers remain visible at full health. A reserved footer holds
|
||||
the AFK indicator; the former co-op player-name row is removed.
|
||||
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.
|
||||
|
||||
Fullscreen X/Y anchors bypass safe-area insets. Overlay positions and shader
|
||||
widths use the normalized `640 × 480` viewport directly. Text uses a minimum
|
||||
font scale of `1.0`. Check the upper-left timers
|
||||
against stock map inventory/objectives and performance overlays, and keep the
|
||||
upper-right service area below the Plutonium tag. Physical proportions vary
|
||||
with aspect ratio, so verify text fit and screen-edge clearance in-game.
|
||||
|
||||
The core is created once per connection and stays in place when services open,
|
||||
across death and respawn, and while newcomers spectate. Unavailable values use
|
||||
placeholders until the first-spawn callback binds the data displays. Creating the
|
||||
panel does not start the match clock. Each engine retains its timer behavior;
|
||||
clocks, health and zombie-count displays freeze at game over. Health display
|
||||
fallbacks never modify gameplay max health.
|
||||
placeholders until the first-spawn callback binds the data displays. Creation
|
||||
does not start the match clock. Each engine retains its timer behavior; clocks,
|
||||
health and zombie-count displays freeze at game over. Health display fallbacks
|
||||
never modify gameplay max health.
|
||||
|
||||
Common owns one fixed-height service window directly below the core. `.help`
|
||||
and its `.commands` alias show four commands per page, with command and
|
||||
description columns plus a page count. Pages advance automatically every
|
||||
12 seconds and close after the last page. Repeating either command restarts
|
||||
page one. The order is `.help`, `.afk`, `.back`, `.join`, `.leaders`, `.b`, `.d`,
|
||||
`.w`, omitting commands whose modules are not registered. The `.join` description
|
||||
continues to identify its newcomer restriction.
|
||||
Type `.hud` in chat to hide or show your core, including its branding, timers,
|
||||
zombie count, health bar, damage-only HP and AFK indicator. The exact command
|
||||
is case-insensitive. Visibility defaults on and persists across death/respawn
|
||||
within the current connection/map; reconnecting or restarting the map restores
|
||||
the visible default. Data and timers continue updating while hidden, so showing
|
||||
the HUD restores current values or the frozen game-over values. Help, records
|
||||
and the stock HUD remain unaffected.
|
||||
|
||||
Records use the same service window. Help pauses a visible record and resumes
|
||||
its remaining page time when help ends. `.leaders` during help replaces the
|
||||
pending record with a fresh replay from page one. One service is visible at a
|
||||
time, and neither changes the core's position or height. Services are private
|
||||
to the requesting player and work when the HUD module is omitted.
|
||||
Common owns one unboxed service area at the upper right. `.help` and its
|
||||
`.commands` alias show all nine installed commands in one list with command
|
||||
and description columns: `.help`, `.hud`, `.afk`, `.back`, `.join`, `.leaders`,
|
||||
`.b`, `.d`, `.w`. Missing modules' commands are omitted. Bank syntax remains
|
||||
`.d <amount|all>` and `.w <amount|all>`, and `.join` identifies its newcomer
|
||||
restriction. Additional registered commands use pages of nine rows; a page
|
||||
count appears only when more than one page is needed. Each page lasts
|
||||
12 seconds and the list closes after the final page. Repeating either help
|
||||
command restarts page one. Both engines position rows explicitly, avoiding
|
||||
T5's newline-rendering problem.
|
||||
|
||||
Install HUD `1.2.0`, leaderboard `1.1.0` and AFK `1.0.1` with the matching
|
||||
game's common `1.2.0+`; update the installed scripts together and restart the map.
|
||||
There is no new native-plugin or backend requirement, and this change does not
|
||||
require a full server-process restart. Existing leaderboard plugin requirements
|
||||
still apply. Unchanged modules retain their existing common helper interfaces.
|
||||
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
|
||||
page time when help ends. `.leaders` during help replaces the pending record
|
||||
with a fresh replay from page one. One service is visible at a time. Services
|
||||
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
|
||||
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 validation
|
||||
|
||||
@@ -410,16 +440,23 @@ Before rollout, validate both engines in Plutonium:
|
||||
|
||||
1. Check 16:9 and narrower/wider resolutions, safe-area settings, long match
|
||||
timers, stock map inventory/objectives, performance overlays, player scores,
|
||||
ammo and interaction prompts. The core and service panels must stay clear of
|
||||
these areas and retain their geometry for every page.
|
||||
ammo and interaction prompts. Check upper-left timers, top-center branding,
|
||||
bottom-center zombies/health and the upper-right service area. Text must stay
|
||||
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.
|
||||
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. Toggle AFK and back
|
||||
without overlapping the health bar or changing existing protection behavior.
|
||||
3. Open `.help`, `.HELP` and `.commands` with all modules installed. Verify two
|
||||
pages of four commands, 12 seconds per page and automatic dismissal. Repeat
|
||||
on page two to return to page one. Remove optional modules and restart the
|
||||
map to confirm filtered pages, then restore them.
|
||||
health/Juggernog changes, and frozen timers at game over. HP numbers must
|
||||
appear only while injured and disappear on full healing. Toggle AFK and back
|
||||
without overlapping zombies/health or changing existing protection behavior.
|
||||
3. Open `.help`, `.HELP` and `.commands` with all modules installed. Verify
|
||||
one list of nine commands, full bank syntax, no single-page counter and
|
||||
automatic dismissal after 12 seconds. Add custom registered commands to
|
||||
exercise the nine-row page limit, page counter and repeat-to-restart behavior.
|
||||
Remove optional modules and restart the map to confirm filtering, including
|
||||
`.hud` disappearing without HUD, then restore them.
|
||||
4. Interrupt records with help and check that the interrupted page resumes for
|
||||
its remaining time. Repeat `.leaders` while help is open; only the latest
|
||||
replay should start, at page one. Check long holder names, ties spanning
|
||||
@@ -428,6 +465,16 @@ Before rollout, validate both engines in Plutonium:
|
||||
request them again. Verify no duplicate elements, stale timeout closures or
|
||||
surviving service windows. Confirm `.join`, bank commands and lobby lock
|
||||
retain their existing behavior with their required modules installed.
|
||||
6. Toggle `.hud` and `.HUD` before spawning, during play, while AFK and after
|
||||
game over. Check private visibility, all core elements and the AFK indicator,
|
||||
continued updates while hidden, and unchanged help/records/stock HUD. Verify
|
||||
death/respawn preserves the choice and reconnect/map restart restores visibility.
|
||||
7. On T5, watch the first automatic record announcement and its dismissal, then
|
||||
repeat `.leaders`, help and interrupted/resumed records. Verify the core
|
||||
health bar, damage-only HP, zombie count and timers remain intact while a
|
||||
service is visible and after it closes. Include stock round/connection
|
||||
notifications and other installed HUD mods; portable allocation checks do
|
||||
not establish live T5 SP rendering capacity.
|
||||
|
||||
## Map record announcements
|
||||
|
||||
@@ -438,8 +485,8 @@ round and every tied holder once per connection, including spectator newcomers.
|
||||
Type `.leaders` to replay the record privately from the latest usable cache.
|
||||
Repeating the command restarts the announcement from its first page. It appears
|
||||
in `.help` when the leaderboard module is enabled. If no usable record is
|
||||
available, the service window identifies the record as unavailable and keeps
|
||||
the `.leaders` retry reminder; this differs from a valid empty record history.
|
||||
available, the service identifies the record as unavailable; this differs from
|
||||
a valid empty record history. Neither state adds a replay footer.
|
||||
It reads Electric Cherry's public `/api/leaderboard` endpoint without a token;
|
||||
only completed matches contribute. Ties may come from the same run or separate
|
||||
runs. Names are the API's latest player names, with players identified by GUID.
|
||||
@@ -461,17 +508,18 @@ exist in the API's map list and match the current game and engine map. Clear it
|
||||
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 in the common-owned service
|
||||
window beneath the core, with a `MAP RECORD` heading, prominent round value,
|
||||
holder names, page count and `.leaders` replay hint. Each page shows up to three
|
||||
holder lines for 12 seconds. Opening `.help` pauses the record's remaining
|
||||
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
|
||||
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
|
||||
pending replay and starts from page one after help closes.
|
||||
All holders are included; colour/control codes are removed, and overlong
|
||||
individual names are abbreviated without splitting UTF-8 characters. Bounded
|
||||
lines and additional pages preserve every tied holder. The window hides when the
|
||||
announcement finishes and its elements are reused for subsequent services.
|
||||
Disconnect or game end cleans up service elements.
|
||||
lines and additional pages preserve every tied holder. The window closes when
|
||||
the announcement finishes. Both games release their view elements when idle or
|
||||
changing service kinds. Disconnect or game end cleans up any remaining service
|
||||
elements.
|
||||
|
||||
One level worker fetches the map list and pages of three players, stopping when
|
||||
the highest-round tie ends. Successful snapshots refresh every five minutes.
|
||||
|
||||
+49
-18
@@ -35,8 +35,9 @@ before copying the renamed scripts.
|
||||
- **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. Help updates two
|
||||
multiline columns only on page changes, and record rows skip unchanged text.
|
||||
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.
|
||||
- **`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
|
||||
@@ -58,21 +59,39 @@ before copying the renamed scripts.
|
||||
Many BO1 maps pre-set `round_number = 1` at init, so the round watcher
|
||||
fires on transitions only and the timer row seeds on first spawn as a
|
||||
fallback.
|
||||
- **Connection-time HUD creation must not seed clocks.** The upper-right core
|
||||
is available to waiting spectators with placeholders. Timer updates retain
|
||||
- **Connection-time HUD creation must not seed clocks.** The unboxed core is
|
||||
available to waiting spectators with placeholders: timers upper left,
|
||||
branding top center and zombies/health bottom center. Timer updates retain
|
||||
their existing spawn/round lifecycle, independently of element creation.
|
||||
- **One service window per player.** Common owns fixed geometry, ordered
|
||||
four-command help pages, record display state and cleanup. Help pauses record
|
||||
page time; `.leaders` during help replaces the pending replay from page one.
|
||||
Keep both services independent of the HUD module. The core never moves or
|
||||
resizes when a service opens, and the AFK indicator uses its reserved footer.
|
||||
- **Visibility is separate from updates.** HUD owns the exact, case-insensitive
|
||||
`.hud` chat command and its help entry. Common's core wrappers register each
|
||||
element and preserve its intended alpha behind a per-player visibility mask;
|
||||
health and AFK changes use `set_core_alpha()`. Hidden data/timers keep updating
|
||||
until game over freezes them. Help, records and stock elements remain separate.
|
||||
Visibility defaults on, survives death/respawn during the connection/map, and
|
||||
resets on reconnect or map restart. AFK protection is unaffected.
|
||||
- **One service view per player.** Common owns the unboxed upper-right service
|
||||
area, ordered help rows, record display state and cleanup. Help shows all
|
||||
nine installed commands; extra registrations use nine rows per page. Help
|
||||
pauses record page time; `.leaders` during help replaces the pending replay
|
||||
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
|
||||
15 + 1 archived elements; the service uses 19 nonarchived elements regardless
|
||||
of command/page count. Help reuses two multiline columns and one muted column
|
||||
backdrop. Hiding an element does not free its slot. The
|
||||
11 + 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
|
||||
its elements. Keep the controller and pending requests separate from element
|
||||
lifetime so paused records resume normally. Both engines use explicit help
|
||||
rows; BO1 can concatenate newline-separated HUD text. Hiding an element does
|
||||
not free its slot. The previous 24-element persistent service allocation was
|
||||
consistent with the reported core corruption after records first appeared.
|
||||
The exact live T5 SP capacity remains unverified. The
|
||||
[BO1 HUD update](https://github.com/MisaDev4/bo1-zombies-decompiled/blob/102f6992be332610aff587e681e3180b15f395bb/src/game/g_hudelem.cpp#L2596)
|
||||
shows 31 slots per bucket; this is not proof of current Plutonium stock HUD
|
||||
usage, which still needs live validation.
|
||||
comes from an MP-based reconstruction and is not proof of T5 SP limits or
|
||||
current Plutonium stock usage. Validate automatic records, help, view switches
|
||||
and dismissal alongside stock notifications and other installed HUD mods;
|
||||
the core must remain intact throughout and after every service closes.
|
||||
- **Game-over trigger is level-scope, raced across three candidate
|
||||
notifies** (`end_game` / `game_ended` / `intermission`) since BO1 maps
|
||||
are inconsistent. Per-player triggers do not work — `self waittill("death")`
|
||||
@@ -80,9 +99,21 @@ before copying the renamed scripts.
|
||||
polling `self.health` misses the zero-crossing (health is rewritten during
|
||||
the game-over sequence).
|
||||
|
||||
HUD `1.2.0`, leaderboard `1.1.0` and AFK `1.0.1` require matching common `1.2.0+`.
|
||||
Install the changed scripts together and restart the map. There is no new
|
||||
native-plugin/backend dependency or full server-process restart requirement.
|
||||
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.
|
||||
The full help order is `.help`, `.hud`, `.afk`, `.back`, `.join`, `.leaders`,
|
||||
`.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
|
||||
`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
|
||||
`int()` after `max()` in that helper: native `max()` returns a float, but
|
||||
`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.
|
||||
Run the [shared HUD checks](../../README.md#hud-validation); portable fixtures
|
||||
do not replace T5 live loading, layout and lifecycle validation.
|
||||
|
||||
@@ -91,7 +122,7 @@ do not replace T5 live loading, layout and lifecycle validation.
|
||||
| File | Role | Fires notifies | Reads notifies |
|
||||
|---|---|---|---|
|
||||
| `tws_t5_common` | Shared config, HUD helpers, paginated `.help`, service window, spawn-handler registry, single connect listener + dispatcher | — | `say`, game-end notifications |
|
||||
| `tws_t5_hud` | Upper-right core: branding, timer row, zombie count, HP and health bar | `hud_round_changed`, `hud_match_started`, `hud_game_over` | (internal) |
|
||||
| `tws_t5_hud` | Unboxed core overlays and private `.hud` visibility toggle | `hud_round_changed`, `hud_match_started`, `hud_game_over` | `say`, internal notifications |
|
||||
| `tws_t5_afk` | `.afk` / `.back` chat toggle, owns its [AFK] indicator | — | — |
|
||||
| `tws_t5_join` | `.join` for spectators who have not played on this connection | — | `say`, `end_game`, `game_ended`, `intermission` |
|
||||
| `tws_t5_lock` | Lobby `g_password` lock at milestone round (`tws_lock_round`, default 20) | — | `hud_round_changed`, `hud_game_over` |
|
||||
|
||||
+90
-42
@@ -11,7 +11,7 @@ Platform quirks and deploy commands: [NOTES.md](NOTES.md).
|
||||
| File | Role |
|
||||
|---|---|
|
||||
| [tws_t5_common.gsc](tws_t5_common.gsc) | Shared config, HUD helpers, paginated `.help`, service window, connect/spawn registries, single `connected` listener + dispatcher |
|
||||
| [tws_t5_hud.gsc](tws_t5_hud.gsc) | Fixed upper-right core: branding, timers, zombie count, current/maximum HP and health bar |
|
||||
| [tws_t5_hud.gsc](tws_t5_hud.gsc) | Unboxed timers, branding, zombies/health and per-player `.hud` visibility toggle |
|
||||
| [tws_t5_afk.gsc](tws_t5_afk.gsc) | `.afk` / `.back` chat toggle + `[AFK]` indicator |
|
||||
| [tws_t5_join.gsc](tws_t5_join.gsc) | `.join` lets newcomers spawn before the current round ends |
|
||||
| [tws_t5_lock.gsc](tws_t5_lock.gsc) | Lobby `g_password` lock at a milestone round |
|
||||
@@ -56,22 +56,43 @@ invalid identities fail privately without changing points. Follow the shared
|
||||
|
||||
## Core HUD and service window
|
||||
|
||||
The fixed upper-right core shows `TWS ZOMBIES`, `discord.tws.lol`, side-by-side
|
||||
match/round timers, zombie count, current/maximum HP and a full-width health bar.
|
||||
HP stays visible at full health. A reserved footer holds the AFK indicator;
|
||||
there is no co-op name row. The core is created once per connection, including
|
||||
for spectator newcomers with placeholders until values are available. Creation
|
||||
does not seed the match clock. It persists across death and respawn.
|
||||
Clocks, health and zombie-count displays freeze at game over. Health fallbacks
|
||||
only affect the display and never modify gameplay max health.
|
||||
The unboxed core puts match time above round time at the upper left,
|
||||
`TWS ZOMBIES · discord.tws.lol` at the top center, and zombies remaining above a
|
||||
slim health bar at the bottom center. Current/maximum HP appears to the right
|
||||
of the bar only while injured; it disappears at full health. The AFK indicator
|
||||
sits above the zombie count. There are no panel backgrounds or timer labels.
|
||||
Fullscreen X/Y anchors use the normalized `640 × 480` viewport directly and
|
||||
bypass safe-area insets. Font scale is at least `1.0`; verify text fit, stock
|
||||
HUD overlap and Plutonium tag clearance at each aspect ratio in-game.
|
||||
|
||||
Common provides one fixed-height service window beneath the core for help or
|
||||
records. Services never move or resize the core. They remain private and work
|
||||
without the HUD module installed. See the [shared layout and validation guide](../../README.md#core-hud-and-service-window-t5-and-t6).
|
||||
The core is created once per connection, including for spectator newcomers
|
||||
with placeholders until values are available. Creation does not seed the match
|
||||
clock. It persists across death and respawn. Clocks, health and zombie-count
|
||||
displays freeze at game over. Health fallbacks only affect the display and
|
||||
never modify gameplay max health.
|
||||
|
||||
Install HUD `1.2.0`, leaderboard `1.1.0` and AFK `1.0.1` with common `1.2.0+`.
|
||||
Update the installed scripts together and restart the map; this change requires
|
||||
no new plugin/backend dependency or full server-process restart.
|
||||
The exact, case-insensitive `.hud` command toggles your core and AFK indicator.
|
||||
It defaults visible, keeps your choice across death/respawn on the current
|
||||
connection/map, and resets visible on reconnect or map restart. Timers and data
|
||||
continue updating while hidden; game-over values remain frozen. Help, records
|
||||
and the stock HUD are unaffected. `.hud` requires the HUD module.
|
||||
|
||||
Common provides one unboxed upper-right service area below the Plutonium tag.
|
||||
Records are right aligned, with `Map record · Round N` followed by up to three
|
||||
holder lines; there is no replay footer or single-page counter. Help displays
|
||||
the full nine-command list in aligned rows. Services remain private, leave the
|
||||
core in place and work without the HUD module installed. See the
|
||||
[shared layout and validation guide](../../README.md#core-hud-and-service-window-t5-and-t6).
|
||||
Only the active service view holds HUD elements: 19 for nine-command help
|
||||
(20 with a multi-page counter), or four for records (five with a multi-page
|
||||
counter). Pages reuse elements; switching views or closing the service releases
|
||||
them. Idle services hold no HUD elements, while the controller retains pending
|
||||
requests and interruption/resume behavior. Core + AFK use 11 + 1 elements.
|
||||
|
||||
HUD `1.4.0` and AFK `1.2.0` require common `1.4.0+`. Update common, HUD and AFK
|
||||
together where installed, then restart the map. Leaderboard `1.1.0` remains
|
||||
compatible. This change requires no new Plutonium/plugin/backend dependency or
|
||||
full server-process restart.
|
||||
|
||||
## Map record announcements
|
||||
|
||||
@@ -93,14 +114,17 @@ apply to both games.
|
||||
Type `.help` or `.commands` in chat to see available player commands and a short description.
|
||||
The response is visible only to the requesting player and works while spectating,
|
||||
before the first spawn. Commands are case-insensitive.
|
||||
Help uses the service window below the core. It shows four commands per page,
|
||||
advances automatically every 12 seconds and closes after the last page. The
|
||||
panel retains its height on short pages. Typing either command again returns to
|
||||
page one with a fresh timer.
|
||||
Help uses the unboxed upper-right service area. All nine installed commands
|
||||
fit in one list with command and description columns. Additional registered
|
||||
commands use pages of nine rows, with a counter only for multi-page lists.
|
||||
Pages advance every 12 seconds and close after the last page. Typing either
|
||||
command again returns to page one with a fresh timer. Both engines use
|
||||
explicit row positions so T5 does not concatenate newline-separated text.
|
||||
|
||||
| Command | Effect |
|
||||
|---|---|
|
||||
| `.help`, `.commands` | List available commands |
|
||||
| `.hud` | Hide or show your core HUD and AFK indicator (HUD module required) |
|
||||
| `.afk` | Pause with protection |
|
||||
| `.back` | Return from AFK |
|
||||
| `.join` | Join mid-round (newcomers only) |
|
||||
@@ -112,6 +136,9 @@ page one with a fresh timer.
|
||||
Each module registers its own help entries. Commands follow the order above,
|
||||
independent of initialization order; removing a module also removes its entries
|
||||
from `.help` after a map restart.
|
||||
With all command modules installed, help shows one page of nine commands,
|
||||
including the full `.d <amount|all>` / `.w <amount|all>` syntax. Removing HUD
|
||||
removes `.hud` from the list.
|
||||
The enabled leaderboard module registers `.leaders`, which replays the latest
|
||||
cached map record privately. Repeating it restarts the record from the first page;
|
||||
help pauses the record's remaining page time and resumes it after the final
|
||||
@@ -170,13 +197,15 @@ single `level waittill("connected")` in the fleet. Adding a second
|
||||
connect listener anywhere else crashes the T5 SP handshake.
|
||||
|
||||
**Features**
|
||||
- Shared HUD constants for panel geometry, rows/columns, fonts, health bar,
|
||||
- Shared HUD constants for overlay positions, rows/columns, fonts, health bar,
|
||||
colors and update rate
|
||||
- Spawn-handler registry
|
||||
- Connection-handler registry for optional modules that serve spectator newcomers
|
||||
- `.help` / `.commands` listener and ordered help registry populated by installed modules
|
||||
- One reusable service window per player, with help/record pagination,
|
||||
interruption/resume, request replacement and disconnect/game-end cleanup
|
||||
- One service controller per player, with help/record pagination,
|
||||
interruption/resume, request replacement and disconnect/game-end cleanup;
|
||||
display elements exist only for the active view and are released when idle
|
||||
- Core element registry with per-element alpha state and a per-player visibility mask
|
||||
- Connect listener + per-player dispatch: iterates
|
||||
`level.tws_spawn_handlers` after `spawned_player` and threads each on
|
||||
the player
|
||||
@@ -191,8 +220,18 @@ connect listener anywhere else crashes the T5 SP handshake.
|
||||
| `register_help_command(command, description)` | Adds a player command to `.help`; call from the owning module's `init()` |
|
||||
| `make_text_elem(p, x, y, ax, s, c)`| `NewClientHudElem` text element with black-glow outline |
|
||||
| `make_shader_elem(p, x, y)` | `NewClientHudElem` shader element |
|
||||
| `make_panel_text(p, x, y, ax, s, c)` | Text positioned relative to the fixed upper-right core origin |
|
||||
| `make_panel_shader(p, x, y, w, h, c, alpha, sort)` | Background/bar shader positioned relative to the core origin |
|
||||
| `make_overlay_text(p, x, y, ax, s, c)` | Text at absolute fullscreen coordinates in the normalized `640 × 480` frame |
|
||||
| `make_overlay_shader(p, x, y, w, h, c, alpha, sort)` | Shader at absolute fullscreen coordinates |
|
||||
| `make_overlay_width(width)` | Clamp a normalized width and return the integer required by `SetShader` |
|
||||
| `make_core_overlay_text(p, x, y, ax, s, c)` | Create overlay text registered for `.hud` visibility |
|
||||
| `make_core_overlay_shader(p, x, y, w, h, c, alpha, sort)` | Create an overlay shader registered for `.hud` visibility |
|
||||
| `make_panel_text(p, x, y, ax, s, c)` | Legacy text positioned relative to the former upper-right panel origin |
|
||||
| `make_panel_shader(p, x, y, w, h, c, alpha, sort)` | Legacy shader positioned relative to that panel origin |
|
||||
| `make_panel_width(width)` | Retained legacy design-width conversion for existing consumers |
|
||||
| `make_core_text(p, x, y, ax, s, c)` | Retained legacy panel text with `.hud` visibility |
|
||||
| `make_core_shader(p, x, y, w, h, c, alpha, sort)` | Retained legacy panel shader with `.hud` visibility |
|
||||
| `set_core_alpha(p, elem, alpha)` | Preserve the element's intended alpha while applying the player's visibility choice |
|
||||
| `toggle_core_hud()` | Toggle the player's registered core elements without stopping updates |
|
||||
| `show_record(snapshot)` | Replace the player's record service request; an undefined snapshot shows unavailable |
|
||||
| `destroy_on_disconnect(elems)` | Waits on `disconnect`, destroys the array |
|
||||
| `int_dvar_or(name, fallback)` | Parse an integer dvar with default |
|
||||
@@ -203,31 +242,39 @@ connect listener anywhere else crashes the T5 SP handshake.
|
||||
**Live validation (requires a T5 server)**
|
||||
|
||||
1. Type `.help`, `.HELP` and `.commands` while playing and spectating before the
|
||||
first spawn. Verify four commands per page in the service window, private to
|
||||
the requesting player. Check that full and damaged HP remain visible above it.
|
||||
first spawn. Verify all nine commands appear in one unboxed list, private to
|
||||
the requesting player. Check explicit rows, full bank syntax and the absence
|
||||
of a single-page counter. HP should appear beside the bottom health bar only
|
||||
while injured.
|
||||
2. Remove AFK or join, restart the map, and verify its entries disappear while
|
||||
`.help` and the remaining commands still work. Restore the scripts afterward.
|
||||
3. Verify pages advance every 12 seconds and close after the last page. Repeat
|
||||
`.help` on a later page and check that it restarts page one. Interrupt a record
|
||||
3. Verify help closes after 12 seconds. Add custom registered commands to test
|
||||
the nine-row page limit, counter and repeat-to-restart behavior. Interrupt a record
|
||||
with help and verify remaining page time resumes; repeat `.leaders` during
|
||||
help and verify only the latest replay starts from page one.
|
||||
4. Reopen after expiry, die/respawn and reconnect; verify no duplicate text or
|
||||
leftover elements. Repeat without the HUD module. Follow the complete
|
||||
[HUD validation guide](../../README.md#hud-validation) before rollout.
|
||||
5. Watch the first automatic record announcement and its dismissal. Repeat
|
||||
records, help and interrupted/resumed records alongside stock notifications.
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## tws_t5_hud.gsc
|
||||
|
||||
Fixed upper-right core panel. Per-player elements are created on connection;
|
||||
Unboxed core overlays. Per-player elements are created on connection;
|
||||
gameplay updates start through the existing spawn lifecycle.
|
||||
|
||||
**Features**
|
||||
- Match timer + round timer (round resets each round, both freeze on game over)
|
||||
- `TWS ZOMBIES` / `discord.tws.lol` header and reserved AFK footer
|
||||
- Zombies-remaining counter
|
||||
- Full-width health bar with green / amber / red ramp; current/maximum HP
|
||||
remains visible at full health
|
||||
- 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
|
||||
- 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
|
||||
- Round-transition and game-over notify machinery
|
||||
|
||||
@@ -246,8 +293,9 @@ Connection handler registered: `::hud_connect` — creates the core with placeho
|
||||
without seeding clocks; `hud_start` calls it as a fallback if needed.
|
||||
|
||||
**Depends on**
|
||||
- `scripts\sp\tws_t5_common::` — `ensure_config`, `make_panel_text`,
|
||||
`make_panel_shader`, `destroy_on_disconnect`, `format_mmss`,
|
||||
- `scripts\sp\tws_t5_common::` — `ensure_config`, `make_core_overlay_text`,
|
||||
`make_core_overlay_shader`, `make_overlay_width`, `set_core_alpha`, `toggle_core_hud`,
|
||||
`destroy_on_disconnect`, `format_mmss`, `register_help_command`,
|
||||
`register_spawn_handler`, `register_connect_handler`
|
||||
- Every `level.hud_*` config value from `tws_t5_common`
|
||||
- Stock BO1 state: `level.round_number`, `level.zombie_total`,
|
||||
@@ -258,8 +306,8 @@ without seeding clocks; `hud_start` calls it as a fallback if needed.
|
||||
|
||||
## tws_t5_afk.gsc
|
||||
|
||||
Self-invoked pause mode. Owns the `[AFK]` indicator element, positioned in the
|
||||
core's reserved footer through common layout values. It does not require HUD.
|
||||
Self-invoked pause mode. Owns the `[AFK]` indicator element, centered above the
|
||||
zombie count through common layout values. It does not require HUD.
|
||||
|
||||
**Features**
|
||||
- Chat commands `.afk` and `.back` (via `level waittill("say")` — not
|
||||
@@ -269,7 +317,7 @@ core's reserved footer through common layout values. It does not require HUD.
|
||||
- On exit: reverse, then a 2s grace tail keeps invuln + ignoreme through
|
||||
the target-reacquisition window
|
||||
- `[AFK]` HUD element created per-player via spawn handler; alpha-toggled
|
||||
by enter / exit
|
||||
by enter / exit through common's core visibility helpers, respecting `.hud`
|
||||
|
||||
**Provides**
|
||||
- Per-player: `self.hud_afk_on` (bool), `self.hud_afk_elem` (HUD element),
|
||||
@@ -277,9 +325,9 @@ core's reserved footer through common layout values. It does not require HUD.
|
||||
- Level var: `level.afk_grace_seconds` (default 2)
|
||||
|
||||
**Depends on**
|
||||
- `scripts\sp\tws_t5_common::` — `ensure_config`, `make_panel_text`,
|
||||
`destroy_on_disconnect`, `register_spawn_handler`, `register_help_command`
|
||||
- Shared config: core footer position, `level.hud_font_small`, `level.hud_c_afk`
|
||||
- `scripts\sp\tws_t5_common::` — `ensure_config`, `make_core_overlay_text`,
|
||||
`set_core_alpha`, `destroy_on_disconnect`, `register_spawn_handler`, `register_help_command`
|
||||
- Shared config: overlay AFK position, `level.hud_font_afk`, `level.hud_c_afk`
|
||||
- Stock BO1: `enableInvulnerability`, `disableInvulnerability`,
|
||||
`freezeControls`, `.ignoreme`, `level waittill("say", message, player)`
|
||||
|
||||
|
||||
@@ -50,11 +50,11 @@ afk_spawn()
|
||||
{
|
||||
self endon("disconnect");
|
||||
|
||||
afk = scripts\sp\tws_t5_common::make_panel_text(
|
||||
self, level.hud_pad, level.hud_row_afk,
|
||||
"left", level.hud_font_afk, level.hud_c_afk);
|
||||
afk = scripts\sp\tws_t5_common::make_core_overlay_text(
|
||||
self, level.hud_overlay_afk_x, level.hud_overlay_afk_y,
|
||||
"center", 1.0, level.hud_c_afk);
|
||||
afk SetText("[AFK]");
|
||||
afk.alpha = 0;
|
||||
scripts\sp\tws_t5_common::set_core_alpha(self, afk, 0);
|
||||
self.hud_afk_elem = afk;
|
||||
|
||||
elems = [];
|
||||
@@ -75,7 +75,7 @@ afk_enter()
|
||||
self freezeControls(true);
|
||||
|
||||
if (isDefined(self.hud_afk_elem))
|
||||
self.hud_afk_elem.alpha = 1;
|
||||
scripts\sp\tws_t5_common::set_core_alpha(self, self.hud_afk_elem, 1);
|
||||
|
||||
self iPrintLnBold("AFK — type .back to return");
|
||||
}
|
||||
@@ -91,7 +91,7 @@ afk_exit()
|
||||
self freezeControls(false);
|
||||
|
||||
if (isDefined(self.hud_afk_elem))
|
||||
self.hud_afk_elem.alpha = 0;
|
||||
scripts\sp\tws_t5_common::set_core_alpha(self, self.hud_afk_elem, 0);
|
||||
|
||||
self iPrintLnBold("Back in the fight");
|
||||
|
||||
|
||||
+288
-123
@@ -28,8 +28,7 @@ common_config()
|
||||
if (isDefined(level.hud_x))
|
||||
return;
|
||||
|
||||
// Existing generic helpers retain their screen-anchor conventions. Panels
|
||||
// use their own right-edge origin and never push stock HUD areas around.
|
||||
// Retain legacy panel coordinates for existing helper consumers.
|
||||
level.hud_x = 10;
|
||||
level.hud_y = 30;
|
||||
// Retain the legacy fields for consumers of the original common config.
|
||||
@@ -43,7 +42,10 @@ common_config()
|
||||
level.hud_col_health = level.hud_x + 120 + 12;
|
||||
level.hud_health_threshold = 1.0;
|
||||
level.hud_panel_right = 10;
|
||||
level.hud_panel_top = 42;
|
||||
level.hud_panel_top = 20;
|
||||
// Fullscreen uses a 640x480 frame. Convert the panel's 480-height units
|
||||
// to that frame at the 16:9 reference aspect (640 / (480 * 16 / 9)).
|
||||
level.hud_panel_x_scale = 0.75;
|
||||
level.hud_panel_width = 220;
|
||||
level.hud_core_height = 100;
|
||||
level.hud_service_y = 108;
|
||||
@@ -73,6 +75,38 @@ common_config()
|
||||
level.hud_c_low = (1, 0.2, 0.2);
|
||||
level.hud_c_afk = (1, 0.8, 0.2);
|
||||
|
||||
// Minimal overlays use absolute coordinates in the fullscreen 640x480 frame.
|
||||
level.hud_overlay_timer_x = 6;
|
||||
level.hud_overlay_match_y = 8;
|
||||
level.hud_overlay_round_y = 30;
|
||||
level.hud_overlay_brand_x = 320;
|
||||
level.hud_overlay_brand_y = 8;
|
||||
level.hud_overlay_count_x = 300;
|
||||
level.hud_overlay_count_y = 440;
|
||||
level.hud_overlay_count_label_x = 306;
|
||||
level.hud_overlay_count_label_y = 446;
|
||||
level.hud_overlay_bar_x = 242;
|
||||
level.hud_overlay_bar_y = 466;
|
||||
level.hud_overlay_bar_width = 156;
|
||||
level.hud_overlay_bar_height = 4;
|
||||
level.hud_overlay_health_y = 460;
|
||||
level.hud_overlay_health_x = 414;
|
||||
level.hud_overlay_slash_x = 418;
|
||||
level.hud_overlay_maxhealth_x = 445;
|
||||
level.hud_overlay_hp_x = 449;
|
||||
level.hud_overlay_afk_x = 320;
|
||||
level.hud_overlay_afk_y = 421;
|
||||
level.hud_service_left = 432;
|
||||
level.hud_service_right = 633;
|
||||
level.hud_service_description_x = 538;
|
||||
level.hud_service_title_y = 54;
|
||||
level.hud_service_help_y = 80;
|
||||
level.hud_service_help_pitch = 24;
|
||||
level.hud_service_help_rows = 9;
|
||||
level.hud_service_record_y = 36;
|
||||
level.hud_service_holders_y = 60;
|
||||
level.hud_service_holders_pitch = 18;
|
||||
|
||||
if (!isDefined(level.tws_spawn_handlers))
|
||||
level.tws_spawn_handlers = [];
|
||||
}
|
||||
@@ -137,7 +171,7 @@ show_help()
|
||||
state = self.tws_service;
|
||||
service_advance(state, getTime());
|
||||
state.generation++;
|
||||
state.help = service_request("help", service_page_count(state.commands.size, 4), state.generation);
|
||||
state.help = service_request("help", service_page_count(state.commands.size, level.hud_service_help_rows), state.generation);
|
||||
service_render(state);
|
||||
}
|
||||
|
||||
@@ -209,14 +243,15 @@ service_advance(state, now)
|
||||
help_rank(command)
|
||||
{
|
||||
if (command == ".help") return 0;
|
||||
if (command == ".afk") return 1;
|
||||
if (command == ".back") return 2;
|
||||
if (command == ".join") return 3;
|
||||
if (command == ".leaders") return 4;
|
||||
if (command == ".b") return 5;
|
||||
if (command.size >= 3 && getSubStr(command, 0, 3) == ".d ") return 6;
|
||||
if (command.size >= 3 && getSubStr(command, 0, 3) == ".w ") return 7;
|
||||
return 8;
|
||||
if (command == ".hud") return 1;
|
||||
if (command == ".afk") return 2;
|
||||
if (command == ".back") return 3;
|
||||
if (command == ".join") return 4;
|
||||
if (command == ".leaders") return 5;
|
||||
if (command == ".b") return 6;
|
||||
if (command.size >= 3 && getSubStr(command, 0, 3) == ".d ") return 7;
|
||||
if (command.size >= 3 && getSubStr(command, 0, 3) == ".w ") return 8;
|
||||
return 9;
|
||||
}
|
||||
|
||||
help_entries()
|
||||
@@ -224,7 +259,7 @@ help_entries()
|
||||
entries = [];
|
||||
if (!isDefined(level.tws_help_commands))
|
||||
return entries;
|
||||
for (rank = 0; rank <= 8; rank++)
|
||||
for (rank = 0; rank <= 9; rank++)
|
||||
{
|
||||
for (i = 0; i < level.tws_help_commands.size; i++)
|
||||
{
|
||||
@@ -238,9 +273,6 @@ help_entries()
|
||||
|
||||
help_command_label(command)
|
||||
{
|
||||
rank = help_rank(command);
|
||||
if (rank == 6) return ".d <n|all>";
|
||||
if (rank == 7) return ".w <n|all>";
|
||||
return command;
|
||||
}
|
||||
|
||||
@@ -248,13 +280,14 @@ help_description(entry)
|
||||
{
|
||||
rank = help_rank(entry.command);
|
||||
if (rank == 0) return "Show commands";
|
||||
if (rank == 1) return "Enable protection";
|
||||
if (rank == 2) return "Return to play";
|
||||
if (rank == 3) return "Join as newcomer";
|
||||
if (rank == 4) return "View records";
|
||||
if (rank == 5) return "Check balance";
|
||||
if (rank == 6) return "Deposit points";
|
||||
if (rank == 7) return "Withdraw points";
|
||||
if (rank == 1) return "Toggle core HUD";
|
||||
if (rank == 2) return "Protect & freeze";
|
||||
if (rank == 3) return "Return to play";
|
||||
if (rank == 4) return "Join as newcomer";
|
||||
if (rank == 5) return "View records";
|
||||
if (rank == 6) return "Bank balance";
|
||||
if (rank == 7) return "Deposit";
|
||||
if (rank == 8) return "Withdraw";
|
||||
return entry.description;
|
||||
}
|
||||
|
||||
@@ -277,57 +310,82 @@ service_ensure()
|
||||
state.record_text = [];
|
||||
state.help_page = -1;
|
||||
state.rows = [];
|
||||
state.command_rows = [];
|
||||
state.description_rows = [];
|
||||
state.view_kind = undefined;
|
||||
self.tws_service = state;
|
||||
|
||||
y = level.hud_service_y;
|
||||
state.background = make_panel_shader(self, 0, y, level.hud_panel_width,
|
||||
level.hud_service_height, level.hud_c_panel, level.hud_panel_alpha, 0);
|
||||
state.elems[state.elems.size] = state.background;
|
||||
state.help_title = service_text(self, state, 10, 10, "left", 1.1, "COMMANDS", level.hud_c_label);
|
||||
state.record_title = service_text(self, state, 10, 10, "left", 1.1, "MAP RECORD", level.hud_c_label);
|
||||
state.help_footer = service_text(self, state, 10, 108, "left", 0.9,
|
||||
"Four commands per page", level.hud_c_dim);
|
||||
state.record_footer = service_text(self, state, 10, 108, "left", 0.9,
|
||||
"Briefly shown - .leaders to view again", level.hud_c_dim);
|
||||
state.page_number = service_text(self, state, 178, 10, "right", 1.1, "", level.hud_c_dim);
|
||||
state.page_slash = service_text(self, state, 184, 10, "center", 1.1, "/", level.hud_c_dim);
|
||||
state.page_total = service_text(self, state, 210, 10, "right", 1.1, "", level.hud_c_dim);
|
||||
// Two fixed text columns keep the HUD element budget independent of the
|
||||
// command count. Strings change only at page boundaries, never per tick.
|
||||
state.command_back = make_panel_shader(self, 10, y + 30, 65, 68,
|
||||
(0.16, 0.19, 0.17), 0.9, 5);
|
||||
state.elems[state.elems.size] = state.command_back;
|
||||
state.command_column = service_text(self, state, 14, 31,
|
||||
"left", 1.0, "", level.hud_c_name);
|
||||
state.description_column = service_text(self, state, 81, 31,
|
||||
"left", 1.0, "", level.hud_c_count);
|
||||
state.round_label = service_text(self, state, 10, 32, "left", 1.1, "ROUND", level.hud_c_label);
|
||||
state.round_number = service_text(self, state, 54, 27, "left", 1.9, "", level.hud_c_count);
|
||||
state.holders_label = service_text(self, state, 10, 52, "left", 1.0, "RECORD HOLDERS", level.hud_c_label);
|
||||
for (i = 0; i < 3; i++)
|
||||
state.rows[i] = service_text(self, state, 10, 65 + i * 13,
|
||||
"left", 1.1, "", level.hud_c_count);
|
||||
state.empty = service_text(self, state, 10, 40, "left", 1.1,
|
||||
"No recorded high round yet", level.hud_c_count);
|
||||
state.unavailable = service_text(self, state, 10, 40, "left", 1.1,
|
||||
"Map record unavailable", level.hud_c_count);
|
||||
for (i = 0; i < state.elems.size; i++)
|
||||
{
|
||||
state.elems[i].alpha = 0;
|
||||
// Keep temporary private services out of the archived HUD bucket.
|
||||
// Hidden elements still occupy slots; do not allocate one per page.
|
||||
state.elems[i].archived = false;
|
||||
}
|
||||
|
||||
self thread service_loop(state);
|
||||
self thread service_disconnect(state);
|
||||
self thread service_end_game(state);
|
||||
}
|
||||
|
||||
// Allocate only the active view. Alpha-zero elements still occupy HUD slots.
|
||||
service_view_create(state, request)
|
||||
{
|
||||
service_view_destroy(state);
|
||||
state.view_kind = request.kind;
|
||||
state.view_paged = request.pages > 1;
|
||||
if (state.view_paged)
|
||||
state.page_counter = service_text(self, state, level.hud_service_right,
|
||||
level.hud_service_title_y, "right", 1.0, "", level.hud_c_dim);
|
||||
|
||||
if (request.kind == "help")
|
||||
{
|
||||
state.help_title = service_text(self, state, level.hud_service_left,
|
||||
level.hud_service_title_y, "left", 1.1, "COMMANDS", level.hud_c_label);
|
||||
rows = state.commands.size;
|
||||
if (rows > level.hud_service_help_rows)
|
||||
rows = level.hud_service_help_rows;
|
||||
// Explicit lines also work on T5, which can collapse multiline strings.
|
||||
for (i = 0; i < rows; i++)
|
||||
{
|
||||
y = level.hud_service_help_y + i * level.hud_service_help_pitch;
|
||||
state.command_rows[i] = service_text(self, state, level.hud_service_left,
|
||||
y, "left", 1.0, "", level.hud_c_count);
|
||||
state.description_rows[i] = service_text(self, state, level.hud_service_description_x,
|
||||
y, "left", 1.0, "", level.hud_c_label);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
state.record_title = service_text(self, state, level.hud_service_right,
|
||||
level.hud_service_record_y, "right", 1.1, "", level.hud_c_count);
|
||||
for (i = 0; i < 3; i++)
|
||||
state.rows[i] = service_text(self, state, level.hud_service_right,
|
||||
level.hud_service_holders_y + i * level.hud_service_holders_pitch,
|
||||
"right", 1.05, "", level.hud_c_label);
|
||||
if (isDefined(state.page_counter))
|
||||
state.page_counter.y = level.hud_service_holders_y + 3 * level.hud_service_holders_pitch;
|
||||
}
|
||||
|
||||
service_view_destroy(state)
|
||||
{
|
||||
for (i = 0; i < state.elems.size; i++)
|
||||
{
|
||||
if (isDefined(state.elems[i]))
|
||||
state.elems[i] Destroy();
|
||||
}
|
||||
state.elems = [];
|
||||
state.view_kind = undefined;
|
||||
state.view_paged = undefined;
|
||||
state.page_counter = undefined;
|
||||
state.help_title = undefined;
|
||||
state.command_rows = [];
|
||||
state.description_rows = [];
|
||||
state.record_title = undefined;
|
||||
state.rows = [];
|
||||
state.page_text = undefined;
|
||||
state.record_header_text = undefined;
|
||||
state.record_text = [];
|
||||
state.help_page = -1;
|
||||
}
|
||||
|
||||
service_text(player, state, x, y, align, scale, text, colour)
|
||||
{
|
||||
elem = make_panel_text(player, x, level.hud_service_y + y, align, scale, colour);
|
||||
elem = make_overlay_text(player, x, y, align, scale, colour);
|
||||
elem.alpha = 0;
|
||||
elem.archived = false;
|
||||
if (text != "")
|
||||
elem SetText(text);
|
||||
state.elems[state.elems.size] = elem;
|
||||
@@ -351,74 +409,70 @@ service_render(state)
|
||||
state.rendered = true;
|
||||
state.last_generation = generation;
|
||||
state.last_page = page;
|
||||
if (!isDefined(request))
|
||||
{
|
||||
service_view_destroy(state);
|
||||
return;
|
||||
}
|
||||
paged = request.pages > 1;
|
||||
if (!isDefined(state.view_kind) || state.view_kind != request.kind || state.view_paged != paged)
|
||||
service_view_create(state, request);
|
||||
for (i = 0; i < state.elems.size; i++)
|
||||
state.elems[i].alpha = 0;
|
||||
if (!isDefined(request))
|
||||
return;
|
||||
|
||||
state.background.alpha = level.hud_panel_alpha;
|
||||
state.page_number SetValue(page + 1);
|
||||
state.page_total SetValue(request.pages);
|
||||
state.page_number.alpha = 1;
|
||||
state.page_slash.alpha = 1;
|
||||
state.page_total.alpha = 1;
|
||||
if (paged)
|
||||
{
|
||||
page_text = "" + (page + 1) + " / " + request.pages;
|
||||
if (!isDefined(state.page_text) || state.page_text != page_text)
|
||||
{
|
||||
state.page_counter SetText(page_text);
|
||||
state.page_text = page_text;
|
||||
}
|
||||
state.page_counter.alpha = 1;
|
||||
}
|
||||
if (request.kind == "help")
|
||||
{
|
||||
state.help_title.alpha = 1;
|
||||
state.help_footer.alpha = 1;
|
||||
if (state.help_page != page)
|
||||
for (i = 0; i < state.command_rows.size; i++)
|
||||
{
|
||||
command_text = "";
|
||||
description_text = "";
|
||||
for (i = 0; i < 4; i++)
|
||||
index = page * level.hud_service_help_rows + i;
|
||||
if (index >= state.commands.size)
|
||||
continue;
|
||||
if (state.help_page != page)
|
||||
{
|
||||
index = page * 4 + i;
|
||||
if (index < state.commands.size)
|
||||
{
|
||||
if (i > 0)
|
||||
{
|
||||
command_text += "\n";
|
||||
description_text += "\n";
|
||||
}
|
||||
entry = state.commands[index];
|
||||
command_text += help_command_label(entry.command);
|
||||
description_text += help_description(entry);
|
||||
}
|
||||
entry = state.commands[index];
|
||||
state.command_rows[i] SetText(help_command_label(entry.command));
|
||||
state.description_rows[i] SetText(help_description(entry));
|
||||
}
|
||||
state.command_column SetText(command_text);
|
||||
state.description_column SetText(description_text);
|
||||
state.help_page = page;
|
||||
state.command_rows[i].alpha = 1;
|
||||
state.description_rows[i].alpha = 1;
|
||||
}
|
||||
state.command_back.alpha = 0.9;
|
||||
state.command_column.alpha = 1;
|
||||
state.description_column.alpha = 1;
|
||||
state.help_page = page;
|
||||
return;
|
||||
}
|
||||
|
||||
state.record_title.alpha = 1;
|
||||
state.record_footer.alpha = 1;
|
||||
header = "Map record unavailable";
|
||||
snapshot = request.snapshot;
|
||||
if (!isDefined(snapshot))
|
||||
if (isDefined(snapshot))
|
||||
{
|
||||
state.unavailable.alpha = 1;
|
||||
return;
|
||||
header = "No recorded high round yet";
|
||||
if (snapshot.round > 0)
|
||||
header = "Map record · Round " + snapshot.round;
|
||||
}
|
||||
if (snapshot.round == 0)
|
||||
if (!isDefined(state.record_header_text) || state.record_header_text != header)
|
||||
{
|
||||
state.empty.alpha = 1;
|
||||
return;
|
||||
state.record_title SetText(header);
|
||||
state.record_header_text = header;
|
||||
}
|
||||
state.round_label.alpha = 1;
|
||||
state.round_number SetValue(snapshot.round);
|
||||
state.round_number.alpha = 1;
|
||||
state.holders_label.alpha = 1;
|
||||
state.record_title.alpha = 1;
|
||||
if (!isDefined(snapshot) || snapshot.round == 0)
|
||||
return;
|
||||
for (i = 0; i < 3; i++)
|
||||
{
|
||||
index = page * 3 + i;
|
||||
if (index < snapshot.lines.size)
|
||||
{
|
||||
text = snapshot.lines[index];
|
||||
// Skip identical text when replaying a page or resuming after help.
|
||||
if (!isDefined(state.record_text[i]) || state.record_text[i] != text)
|
||||
{
|
||||
state.rows[i] SetText(text);
|
||||
@@ -461,12 +515,7 @@ service_destroy(state)
|
||||
state.closed = true;
|
||||
state.help = undefined;
|
||||
state.record = undefined;
|
||||
for (i = 0; i < state.elems.size; i++)
|
||||
{
|
||||
if (isDefined(state.elems[i]))
|
||||
state.elems[i] Destroy();
|
||||
}
|
||||
state.elems = [];
|
||||
service_view_destroy(state);
|
||||
}
|
||||
|
||||
service_game_over_via(event)
|
||||
@@ -526,7 +575,8 @@ make_text_elem(player, x, y, align_x, scale, colour)
|
||||
elem.x = x;
|
||||
elem.y = y;
|
||||
elem.foreground = 1;
|
||||
elem.fontScale = scale;
|
||||
// Sub-1.0 text can render oversized on clients; keep a safe minimum.
|
||||
elem.fontScale = max(1.0, scale);
|
||||
elem.alpha = 1;
|
||||
elem.color = colour;
|
||||
|
||||
@@ -553,14 +603,59 @@ make_shader_elem(player, x, y)
|
||||
return elem;
|
||||
}
|
||||
|
||||
// Panel x/y are local to the fixed upper-right core origin. A right screen
|
||||
// anchor keeps the panel on-screen across aspect ratios; negative offsets grow
|
||||
// left from that edge. Keep each engine's existing vertical anchor convention.
|
||||
// Absolute fullscreen overlays leave stock HUD areas and the center clear.
|
||||
make_overlay_text(player, x, y, align_x, scale, colour)
|
||||
{
|
||||
elem = make_text_elem(player, x, y, align_x, scale, colour);
|
||||
elem.horzAlign = "fullscreen";
|
||||
elem.vertAlign = "fullscreen";
|
||||
elem.sort = 20;
|
||||
elem.archived = true;
|
||||
return elem;
|
||||
}
|
||||
|
||||
make_overlay_shader(player, x, y, width, height, colour, alpha, sort)
|
||||
{
|
||||
elem = make_shader_elem(player, x, y);
|
||||
elem.horzAlign = "fullscreen";
|
||||
elem.vertAlign = "fullscreen";
|
||||
elem.color = colour;
|
||||
elem.alpha = alpha;
|
||||
elem.sort = sort;
|
||||
elem.archived = true;
|
||||
elem SetShader("white", make_overlay_width(width), int(max(1, height)));
|
||||
return elem;
|
||||
}
|
||||
|
||||
make_overlay_width(width)
|
||||
{
|
||||
// Native max() returns a float; shader dimensions must be integers.
|
||||
return int(max(1, width));
|
||||
}
|
||||
|
||||
make_core_overlay_text(player, x, y, align_x, scale, colour)
|
||||
{
|
||||
elem = make_overlay_text(player, x, y, align_x, scale, colour);
|
||||
register_core_elem(player, elem);
|
||||
return elem;
|
||||
}
|
||||
|
||||
make_core_overlay_shader(player, x, y, width, height, colour, alpha, sort)
|
||||
{
|
||||
elem = make_overlay_shader(player, x, y, width, height, colour, alpha, sort);
|
||||
register_core_elem(player, elem);
|
||||
return elem;
|
||||
}
|
||||
|
||||
// Panel x/y are local to the upper-right core origin. Fullscreen bypasses the
|
||||
// engine safe area. Horizontal conversion preserves the original size at 16:9;
|
||||
// other aspects retain the right-edge anchor with proportional horizontal size.
|
||||
make_panel_text(player, x, y, align_x, scale, colour)
|
||||
{
|
||||
elem = make_text_elem(player, x - level.hud_panel_width - level.hud_panel_right,
|
||||
elem = make_text_elem(player, 640 + (x - level.hud_panel_width - level.hud_panel_right) * level.hud_panel_x_scale,
|
||||
level.hud_panel_top + y, align_x, scale, colour);
|
||||
elem.horzAlign = "right";
|
||||
elem.horzAlign = "fullscreen";
|
||||
elem.vertAlign = "fullscreen";
|
||||
elem.sort = 20;
|
||||
elem.archived = true;
|
||||
return elem;
|
||||
@@ -568,17 +663,87 @@ make_panel_text(player, x, y, align_x, scale, colour)
|
||||
|
||||
make_panel_shader(player, x, y, width, height, colour, alpha, sort)
|
||||
{
|
||||
elem = make_shader_elem(player, x - level.hud_panel_width - level.hud_panel_right,
|
||||
elem = make_shader_elem(player, 640 + (x - level.hud_panel_width - level.hud_panel_right) * level.hud_panel_x_scale,
|
||||
level.hud_panel_top + y);
|
||||
elem.horzAlign = "right";
|
||||
elem.horzAlign = "fullscreen";
|
||||
elem.vertAlign = "fullscreen";
|
||||
elem.color = colour;
|
||||
elem.alpha = alpha;
|
||||
elem.sort = sort;
|
||||
elem.archived = true;
|
||||
elem SetShader("white", width, height);
|
||||
elem SetShader("white", make_panel_width(width), height);
|
||||
return elem;
|
||||
}
|
||||
|
||||
// Native shader widths are integers; dynamic fills use the same conversion.
|
||||
make_panel_width(width)
|
||||
{
|
||||
// max() returns a float even for integer inputs; cast its final result.
|
||||
return int(max(1, width * level.hud_panel_x_scale));
|
||||
}
|
||||
|
||||
// Core elements share a visibility mask; services keep their independent alpha.
|
||||
make_core_text(player, x, y, align_x, scale, colour)
|
||||
{
|
||||
elem = make_panel_text(player, x, y, align_x, scale, colour);
|
||||
register_core_elem(player, elem);
|
||||
return elem;
|
||||
}
|
||||
|
||||
make_core_shader(player, x, y, width, height, colour, alpha, sort)
|
||||
{
|
||||
elem = make_panel_shader(player, x, y, width, height, colour, alpha, sort);
|
||||
register_core_elem(player, elem);
|
||||
return elem;
|
||||
}
|
||||
|
||||
register_core_elem(player, elem)
|
||||
{
|
||||
live = [];
|
||||
if (isDefined(player.tws_core_hud_elems))
|
||||
{
|
||||
for (i = 0; i < player.tws_core_hud_elems.size; i++)
|
||||
{
|
||||
old = player.tws_core_hud_elems[i];
|
||||
if (isDefined(old))
|
||||
live[live.size] = old;
|
||||
}
|
||||
}
|
||||
live[live.size] = elem;
|
||||
player.tws_core_hud_elems = live;
|
||||
set_core_alpha(player, elem, elem.alpha);
|
||||
}
|
||||
|
||||
set_core_alpha(player, elem, alpha)
|
||||
{
|
||||
if (!isDefined(elem))
|
||||
return;
|
||||
elem.tws_core_alpha = alpha;
|
||||
elem.alpha = alpha;
|
||||
if (isDefined(player.tws_hud_hidden) && player.tws_hud_hidden)
|
||||
elem.alpha = 0;
|
||||
}
|
||||
|
||||
toggle_core_hud()
|
||||
{
|
||||
hidden = false;
|
||||
if (isDefined(self.tws_hud_hidden))
|
||||
hidden = self.tws_hud_hidden;
|
||||
self.tws_hud_hidden = !hidden;
|
||||
if (!isDefined(self.tws_core_hud_elems))
|
||||
return;
|
||||
live = [];
|
||||
for (i = 0; i < self.tws_core_hud_elems.size; i++)
|
||||
{
|
||||
elem = self.tws_core_hud_elems[i];
|
||||
if (!isDefined(elem))
|
||||
continue;
|
||||
set_core_alpha(self, elem, elem.tws_core_alpha);
|
||||
live[live.size] = elem;
|
||||
}
|
||||
self.tws_core_hud_elems = live;
|
||||
}
|
||||
|
||||
destroy_on_disconnect(elems)
|
||||
{
|
||||
self waittill("disconnect");
|
||||
|
||||
+72
-47
@@ -1,4 +1,4 @@
|
||||
/* tws_t5_hud.gsc - fixed upper-right core HUD, visible from connection. */
|
||||
/* tws_t5_hud.gsc - minimal edge HUD, toggleable with .hud. */
|
||||
|
||||
#include maps\_utility;
|
||||
#include common_scripts\utility;
|
||||
@@ -9,14 +9,34 @@ init()
|
||||
return;
|
||||
|
||||
scripts\sp\tws_t5_common::ensure_config();
|
||||
scripts\sp\tws_t5_common::register_help_command(".hud", "Toggle core HUD");
|
||||
|
||||
level thread round_watcher();
|
||||
level thread game_over_watcher();
|
||||
level thread hud_chat_listener();
|
||||
|
||||
scripts\sp\tws_t5_common::register_connect_handler(::hud_connect);
|
||||
scripts\sp\tws_t5_common::register_spawn_handler(::hud_start);
|
||||
}
|
||||
|
||||
hud_chat_listener()
|
||||
{
|
||||
for (;;)
|
||||
{
|
||||
level waittill("say", message, player);
|
||||
if (!isDefined(player) || !isDefined(message))
|
||||
continue;
|
||||
if (toLower(message) == ".hud")
|
||||
player thread hud_toggle();
|
||||
}
|
||||
}
|
||||
|
||||
hud_toggle()
|
||||
{
|
||||
self hud_connect();
|
||||
self scripts\sp\tws_t5_common::toggle_core_hud();
|
||||
}
|
||||
|
||||
// Connection construction never seeds clocks or reads player gameplay state.
|
||||
// The first-spawn fallback also handles a spawn before the connect callback.
|
||||
hud_connect()
|
||||
@@ -27,48 +47,42 @@ hud_connect()
|
||||
return;
|
||||
|
||||
self.tws_hud_elems = [];
|
||||
background = scripts\sp\tws_t5_common::make_panel_shader(
|
||||
self, 0, 0, level.hud_panel_width, level.hud_core_height,
|
||||
level.hud_c_panel, level.hud_panel_alpha, 0);
|
||||
self.tws_hud_elems[self.tws_hud_elems.size] = background;
|
||||
self.tws_hud_match = self hud_text(level.hud_overlay_timer_x,
|
||||
level.hud_overlay_match_y, "left", 1.4, level.hud_c_count, "--:--");
|
||||
self.tws_hud_round = self hud_text(level.hud_overlay_timer_x,
|
||||
level.hud_overlay_round_y, "left", 1.25, level.hud_c_dim, "--:--");
|
||||
self hud_text(level.hud_overlay_brand_x, level.hud_overlay_brand_y,
|
||||
"center", 1.1, level.hud_c_dim, "TWS ZOMBIES · discord.tws.lol");
|
||||
|
||||
self hud_text(level.hud_pad, level.hud_row_header, "left",
|
||||
level.hud_font_small, level.hud_c_name, "TWS ZOMBIES");
|
||||
self hud_text(level.hud_panel_width - level.hud_pad, level.hud_row_header,
|
||||
"right", level.hud_font_small, level.hud_c_dim, "discord.tws.lol");
|
||||
|
||||
self hud_text(level.hud_pad, level.hud_row_timers, "left",
|
||||
level.hud_font_small, level.hud_c_label, "MATCH");
|
||||
self.tws_hud_match = self hud_text(104, level.hud_row_timers - 2, "right",
|
||||
level.hud_font_mid, level.hud_c_count, "--:--");
|
||||
self hud_text(114, level.hud_row_timers, "left",
|
||||
level.hud_font_small, level.hud_c_label, "ROUND");
|
||||
self.tws_hud_round = self hud_text(level.hud_panel_width - level.hud_pad,
|
||||
level.hud_row_timers - 2, "right",
|
||||
level.hud_font_mid, level.hud_c_count, "--:--");
|
||||
|
||||
self hud_text(level.hud_pad, level.hud_row_stats, "left",
|
||||
level.hud_font_small, level.hud_c_label, "ZOMBIES");
|
||||
self.tws_hud_count = self hud_text(67, level.hud_row_stats, "left",
|
||||
level.hud_font_mid, level.hud_c_count, "--");
|
||||
self hud_text(132, level.hud_row_stats, "left",
|
||||
level.hud_font_small, level.hud_c_label, "HP");
|
||||
self.tws_hud_count = self hud_text(level.hud_overlay_count_x,
|
||||
level.hud_overlay_count_y, "right", 1.5, level.hud_c_count, "--");
|
||||
self hud_text(level.hud_overlay_count_label_x, level.hud_overlay_count_label_y,
|
||||
"left", 1.05, level.hud_c_dim, "ZOMBIES");
|
||||
|
||||
// Independent numeric elements avoid a new config string on every tick.
|
||||
self.tws_hud_health = self hud_text(174, level.hud_row_stats, "right",
|
||||
level.hud_font_mid, level.hud_c_count, "--");
|
||||
self.tws_hud_slash = self hud_text(181, level.hud_row_stats, "center",
|
||||
level.hud_font_mid, level.hud_c_count, "/");
|
||||
self.tws_hud_maxhealth = self hud_text(level.hud_panel_width - level.hud_pad,
|
||||
level.hud_row_stats, "right", level.hud_font_mid, level.hud_c_count, "--");
|
||||
// The readout appears only after a valid damaged-health sample.
|
||||
self.tws_hud_health = self hud_text(level.hud_overlay_health_x,
|
||||
level.hud_overlay_health_y, "right", 1.0, level.hud_c_count, "--");
|
||||
self.tws_hud_slash = self hud_text(level.hud_overlay_slash_x,
|
||||
level.hud_overlay_health_y, "left", 1.0, level.hud_c_count, "/");
|
||||
self.tws_hud_maxhealth = self hud_text(level.hud_overlay_maxhealth_x,
|
||||
level.hud_overlay_health_y, "right", 1.0, level.hud_c_count, "--");
|
||||
self.tws_hud_hp = self hud_text(level.hud_overlay_hp_x,
|
||||
level.hud_overlay_health_y, "left", 1.0, level.hud_c_count, "HP");
|
||||
scripts\sp\tws_t5_common::set_core_alpha(self, self.tws_hud_health, 0);
|
||||
scripts\sp\tws_t5_common::set_core_alpha(self, self.tws_hud_slash, 0);
|
||||
scripts\sp\tws_t5_common::set_core_alpha(self, self.tws_hud_maxhealth, 0);
|
||||
scripts\sp\tws_t5_common::set_core_alpha(self, self.tws_hud_hp, 0);
|
||||
|
||||
track = scripts\sp\tws_t5_common::make_panel_shader(
|
||||
self, level.hud_pad, level.hud_row_bar,
|
||||
level.hud_bar_width, level.hud_bar_height, (0.18, 0.22, 0.19), 0.8, 10);
|
||||
track = scripts\sp\tws_t5_common::make_core_overlay_shader(
|
||||
self, level.hud_overlay_bar_x, level.hud_overlay_bar_y,
|
||||
level.hud_overlay_bar_width, level.hud_overlay_bar_height,
|
||||
(0.18, 0.22, 0.19), 0.8, 10);
|
||||
self.tws_hud_elems[self.tws_hud_elems.size] = track;
|
||||
self.tws_hud_bar = scripts\sp\tws_t5_common::make_panel_shader(
|
||||
self, level.hud_pad, level.hud_row_bar,
|
||||
level.hud_bar_width, level.hud_bar_height, level.hud_c_high, 0, 11);
|
||||
self.tws_hud_bar = scripts\sp\tws_t5_common::make_core_overlay_shader(
|
||||
self, level.hud_overlay_bar_x, level.hud_overlay_bar_y,
|
||||
level.hud_overlay_bar_width, level.hud_overlay_bar_height,
|
||||
level.hud_c_high, 0, 11);
|
||||
self.tws_hud_elems[self.tws_hud_elems.size] = self.tws_hud_bar;
|
||||
|
||||
self thread scripts\sp\tws_t5_common::destroy_on_disconnect(self.tws_hud_elems);
|
||||
@@ -76,7 +90,7 @@ hud_connect()
|
||||
|
||||
hud_text(x, y, align, scale, colour, text)
|
||||
{
|
||||
elem = scripts\sp\tws_t5_common::make_panel_text(self, x, y, align, scale, colour);
|
||||
elem = scripts\sp\tws_t5_common::make_core_overlay_text(self, x, y, align, scale, colour);
|
||||
elem SetText(text);
|
||||
self.tws_hud_elems[self.tws_hud_elems.size] = elem;
|
||||
return elem;
|
||||
@@ -145,7 +159,7 @@ hud_timer_row()
|
||||
{
|
||||
self endon("disconnect");
|
||||
|
||||
// A visible panel for a waiting newcomer must not start a pre-set round 1.
|
||||
// A visible HUD for a waiting newcomer must not start a pre-set round 1.
|
||||
if (!isDefined(level.match_start_time) &&
|
||||
isDefined(level.round_number) &&
|
||||
level.round_number >= 1)
|
||||
@@ -190,7 +204,7 @@ match_timer_bind(elem)
|
||||
elem SetTimerUp(0);
|
||||
}
|
||||
|
||||
// The panel remains visible at game over, with authoritative frozen clocks.
|
||||
// Preserve authoritative frozen clocks when the core is shown after game over.
|
||||
freeze_timers_on_game_over(match_elem, round_elem)
|
||||
{
|
||||
self endon("disconnect");
|
||||
@@ -249,11 +263,12 @@ hud_health_display()
|
||||
self endon("disconnect");
|
||||
level endon("hud_game_over");
|
||||
|
||||
bar_width = level.hud_bar_width;
|
||||
bar_width = level.hud_overlay_bar_width;
|
||||
bar = self.tws_hud_bar;
|
||||
cur = self.tws_hud_health;
|
||||
slash = self.tws_hud_slash;
|
||||
max_hp = self.tws_hud_maxhealth;
|
||||
hp = self.tws_hud_hp;
|
||||
last_width = -1;
|
||||
last_health = -1;
|
||||
last_max = -1;
|
||||
@@ -272,16 +287,17 @@ hud_health_display()
|
||||
if (frac > 1)
|
||||
frac = 1;
|
||||
|
||||
width = int(max(frac * bar_width, 1));
|
||||
width = scripts\sp\tws_t5_common::make_overlay_width(frac * bar_width);
|
||||
if (width != last_width)
|
||||
{
|
||||
bar SetShader("white", width, level.hud_bar_height);
|
||||
bar SetShader("white", width, level.hud_overlay_bar_height);
|
||||
last_width = width;
|
||||
}
|
||||
|
||||
bar.alpha = 1;
|
||||
bar_alpha = 1;
|
||||
if (frac == 0)
|
||||
bar.alpha = 0;
|
||||
bar_alpha = 0;
|
||||
scripts\sp\tws_t5_common::set_core_alpha(self, bar, bar_alpha);
|
||||
|
||||
colour = level.hud_c_high;
|
||||
if (frac <= 0.25)
|
||||
@@ -293,8 +309,17 @@ hud_health_display()
|
||||
cur.color = colour;
|
||||
slash.color = colour;
|
||||
max_hp.color = colour;
|
||||
hp.color = colour;
|
||||
|
||||
// Health numbers remain visible at full health.
|
||||
readout_alpha = 0;
|
||||
if (isDefined(self.health) && current < maximum)
|
||||
readout_alpha = 1;
|
||||
scripts\sp\tws_t5_common::set_core_alpha(self, cur, readout_alpha);
|
||||
scripts\sp\tws_t5_common::set_core_alpha(self, slash, readout_alpha);
|
||||
scripts\sp\tws_t5_common::set_core_alpha(self, max_hp, readout_alpha);
|
||||
scripts\sp\tws_t5_common::set_core_alpha(self, hp, readout_alpha);
|
||||
|
||||
// Keep health numbers current even at full health or while hidden.
|
||||
if (current != last_health)
|
||||
{
|
||||
cur SetValue(current);
|
||||
|
||||
+64
-31
@@ -9,7 +9,7 @@ telemetry, leaderboard and bank modules; neither includes direct-database module
|
||||
| --- | --- | --- |
|
||||
| Paths | `maps/mp/_utility`, `scripts/zm/tws_t6_common` | T6 namespaces and requested filenames |
|
||||
| Loading | No T5 `zombiemode` dvar gate; each module has `main()` and `init()` | Zombies-only folder loading; avoid r5346 missing-main warnings |
|
||||
| HUD positioning | Fullscreen upper-right anchors, fixed core and service-window geometry | Keep clear of top-edge overlays and stock scores/ammo without moving the core for services |
|
||||
| HUD positioning | Fullscreen X/Y anchors, direct normalized `640 × 480` coordinates, unboxed core and service area | Bypass safe-area insets; validate Plutonium tag clearance and font fit at each aspect ratio |
|
||||
| HUD rendering | Precached white shader, local max-health fallback, floating-point ratio, `getPlayers()` | T6 rendering/state handling without changing player max health |
|
||||
| Enemy count | Stock `_zm_utility::get_current_zombie_count()` plus queued zombies | Uses the map's zombie team and enemy filters |
|
||||
| Timers | Seed elapsed time for late joiners; stop round watcher/reset on game over | T6 timer behavior; keep frozen timers frozen |
|
||||
@@ -26,30 +26,56 @@ Common also listens for `.help` through the native `say` notification, independe
|
||||
of first-spawn initialization so newcomers can view commands while spectating.
|
||||
Each loaded command module registers its command and description with
|
||||
`register_help_command()` during `init()`. Lazy registry initialization preserves
|
||||
registrations regardless of module initialization order. Common owns one reusable
|
||||
service window beneath the fixed core. Help uses four commands per page in stable
|
||||
order, with 12 seconds per page and automatic dismissal after the final page.
|
||||
`.help` / `.commands` restart at page one. Help pauses record page time, and a
|
||||
`.leaders` request during help replaces the pending replay from page one. Service
|
||||
state prevents old requests from closing newer displays. Disconnect and game-end
|
||||
cleanup are independent of page timing. The command handlers retain their
|
||||
existing listeners, and help/records work without the HUD module.
|
||||
registrations regardless of module initialization order. Common owns one
|
||||
unboxed upper-right service area. Help shows all nine installed commands in
|
||||
explicit command/description rows; extra registered commands use pages of nine
|
||||
rows. Pages last 12 seconds, with automatic dismissal after the final page and
|
||||
a counter only for multi-page lists. `.help` / `.commands` restart at page one.
|
||||
Help pauses record page time, and a `.leaders` request during help replaces
|
||||
the pending replay from page one. Service state prevents old requests from
|
||||
closing newer displays. Disconnect and game-end cleanup are independent of
|
||||
page timing. Command handlers retain their existing listeners, and help/records
|
||||
work without the HUD module.
|
||||
|
||||
Help reuses two multiline text columns and a muted command-column backdrop.
|
||||
The service has 19 nonarchived elements independent of command/page count;
|
||||
core + AFK use 15 + 1 archived elements. Hidden elements still occupy slots.
|
||||
Both engines use explicit help rows; T5 no longer relies on newline rendering,
|
||||
and T6 has the same row geometry. Common allocates only the active service
|
||||
view: 19 nonarchived elements for nine-command help (20 with a multi-page
|
||||
counter), or four for records (five with a multi-page counter). Closing the
|
||||
service or switching kinds destroys the view; idle holds no elements. Pages
|
||||
reuse the active elements, separately from controller and pending-request state.
|
||||
Core + AFK use 11 + 1 archived elements. Hidden elements still occupy slots.
|
||||
The [T6 player HUD structure](https://github.com/skiff/t6-gsc-helper/blob/50ff6cb7397c9376894a195f48118d1743812d1d/src/game/structs.hpp#L184)
|
||||
has 31 slots in each bucket; it does not establish current Plutonium stock HUD
|
||||
usage. Keep the live overlap/element-budget checks alongside the portable checks.
|
||||
|
||||
The core is created on connection for waiting spectators with placeholders, while
|
||||
timer updates retain the existing spawn/round lifecycle. Connection-time creation
|
||||
must not seed the match clock. Current/maximum HP remains visible at full health;
|
||||
the former co-op name row is removed and AFK uses a reserved footer. Opening a
|
||||
service never changes core position or height. HUD `1.2.0`, leaderboard `1.1.0`
|
||||
and AFK `1.0.1` require matching common `1.2.0+`; deploy the changed scripts
|
||||
together and restart the map. This introduces no native-plugin/backend dependency
|
||||
or full server-process restart requirement.
|
||||
The core is created on connection for waiting spectators with placeholders,
|
||||
while timer updates retain the existing spawn/round lifecycle. Connection-time
|
||||
creation must not seed the match clock. The unboxed layout has timers at the
|
||||
upper left, branding at top center and zombies/health at bottom center. HP
|
||||
numbers appear to the right of the bar only while injured; AFK sits above the
|
||||
zombie count. Services never reposition the core. Records are right aligned,
|
||||
with `Map record · Round N` followed by up to three bounded holder lines, no
|
||||
footer and no single-page counter. Their 12-second page timing is unchanged.
|
||||
|
||||
HUD `1.4.0` and AFK `1.2.0` require matching common `1.4.0+`; update all three
|
||||
where installed and restart the map. Leaderboard `1.1.0` remains compatible.
|
||||
Overlay helpers use direct normalized `640 × 480` coordinates. Dynamic health
|
||||
fills use `make_overlay_width()` like their tracks. Apply `int()` after `max()`
|
||||
in that helper: native `max()` returns a float, but `SetShader` requires an
|
||||
integer width. Legacy panel helpers retain their `0.75` horizontal conversion
|
||||
for existing consumers. Font scale is at least `1.0`. This introduces no
|
||||
Plutonium/native-plugin/backend dependency or full server-process restart
|
||||
requirement.
|
||||
|
||||
HUD owns the exact, case-insensitive `.hud` chat command and registers it in
|
||||
help only while installed. Common's core wrappers preserve each element's
|
||||
intended alpha behind a per-player visibility mask; health and AFK changes use
|
||||
`set_core_alpha()`. Data/timers keep updating while hidden and freeze at game
|
||||
over. Help, records, stock HUD and AFK protection are unaffected. Visibility
|
||||
defaults on, survives death/respawn during the connection/map, and resets on
|
||||
reconnect or map restart. All nine installed commands use one help page,
|
||||
ordered `.help`, `.hud`, `.afk`, `.back`, `.join`, `.leaders`, `.b`, `.d`, `.w`.
|
||||
Bank commands display the full `<amount|all>` syntax.
|
||||
|
||||
AFK again listens to the native `say` notification, so it no longer needs plugin
|
||||
chat registration. The additional T6 controls are only held while AFK; `.back`
|
||||
@@ -164,10 +190,11 @@ command regressions. Compiler success does not certify runtime linking.
|
||||
|
||||
Live checks after deploying:
|
||||
|
||||
1. Join, late-join, die/respawn, and reconnect: one core per player, fixed
|
||||
upper-right position, spectator placeholders without an early clock start,
|
||||
visible full/damaged HP, health/Juggernog changes, zombies remaining, and
|
||||
timers frozen at game over. Test safe-area settings and different resolutions.
|
||||
1. Join, late-join, die/respawn, and reconnect: one unboxed core per player,
|
||||
upper-left timers, top-center branding, bottom-center zombies/health and
|
||||
spectator placeholders without an early clock start. Check damage-only HP,
|
||||
health/Juggernog changes, AFK placement and timers frozen at game over.
|
||||
Test safe-area settings and different resolutions against stock HUD elements.
|
||||
2. `.afk` blocks movement, shooting and grenades; `.back` restores them. Try rapid
|
||||
toggles, a round transition, death/respawn and game over.
|
||||
3. Lock fires at the threshold and clears on game over/restart. HUD must be installed.
|
||||
@@ -176,20 +203,26 @@ Live checks after deploying:
|
||||
endpoint. Check server ID/player count, same-round joins and an unreachable API.
|
||||
5. Repeat on the actual server maps, especially afterlife and special-enemy rounds.
|
||||
6. Type `.help`, `.HELP` and `.commands` before first spawn, during play and
|
||||
while AFK: four commands per page, only on the requesting player's screen,
|
||||
without moving or resizing the core. Verify 12 seconds per page and dismissal
|
||||
after the final page; repeat on page two to restart page one. Remove optional
|
||||
modules and restart the map to confirm their commands are absent. Restore
|
||||
the modules and confirm their command behavior is unchanged.
|
||||
while AFK: one page of nine commands with all modules installed, full bank
|
||||
syntax and no page counter, only on the requesting player's screen. Verify
|
||||
dismissal after 12 seconds and repeat-to-restart behavior. Add custom
|
||||
registered commands to exercise the nine-row page limit and page counter.
|
||||
Remove optional modules and restart the map to confirm their commands are
|
||||
absent. Restore them and confirm command behavior is unchanged.
|
||||
7. Interrupt a record with help and check that its remaining page time resumes.
|
||||
Repeat `.leaders` during help; only the latest replay should start from page
|
||||
one after help closes. Check long names across pages, no records and
|
||||
unavailable records. Reopen after expiry, disconnect/reconnect and end the
|
||||
game during a service; verify no duplicates or leftovers. Repeat without HUD.
|
||||
8. Toggle `.hud` before spawning, while playing/AFK and after game over. Verify
|
||||
the core and AFK indicator hide privately while help/records remain visible.
|
||||
Show it after taking damage or changing AFK state while hidden: current
|
||||
values and intended alpha should return. Confirm updates continue hidden,
|
||||
death/respawn keeps the choice, and reconnect/map restart restores visibility.
|
||||
|
||||
The stock T6 [HUD initialization](https://raw.githubusercontent.com/plutoniummod/t6-scripts/main/ZM/Core/maps/mp/gametypes_zm/_hud.gsc)
|
||||
sets a nominal font height of 12; the corresponding
|
||||
[font helper](https://raw.githubusercontent.com/plutoniummod/t6-scripts/main/ZM/Core/maps/mp/gametypes_zm/_hud_util.gsc)
|
||||
uses `int(level.fontheight * fontscale)` for its height. The portable HUD checks
|
||||
use that nominal box to prevent holder-row and AFK-footer overlaps. Exact glyph
|
||||
extents and multiline spacing still need live validation, especially on T5.
|
||||
use that nominal box to prevent holder-row, help-row and AFK overlaps. Exact
|
||||
glyph extents still need live validation, especially on T5.
|
||||
|
||||
+50
-22
@@ -8,8 +8,8 @@ private server token.
|
||||
| File | Role | Dependencies |
|
||||
| --- | --- | --- |
|
||||
| `tws_t6_common.gsc` | Shared HUD values, helpers, paginated `.help`, service window, connection and first-spawn dispatchers | Stock T6 |
|
||||
| `tws_t6_hud.gsc` | Fixed upper-right core: branding, timers, zombie count, current/maximum HP and health bar | Common 1.2.0+ |
|
||||
| `tws_t6_afk.gsc` | `.afk` / `.back`, protection, movement and weapon blocking | Common 1.2.0+ |
|
||||
| `tws_t6_hud.gsc` | Unboxed timers, branding, zombies/health and per-player `.hud` visibility toggle | Common 1.4.0+ |
|
||||
| `tws_t6_afk.gsc` | `.afk` / `.back`, protection, movement and weapon blocking | Common 1.4.0+ |
|
||||
| `tws_t6_lock.gsc` | Lock at round 20; clear password on game over | Common, HUD |
|
||||
| `tws_t6_telemetry.gsc` | Async queued game reports (game_start/heartbeat/round_change/game_end) | tws-t6-gsc-utils |
|
||||
| `tws_t6_join.gsc` | `.join` chat command lets a spectator newcomer enter mid-match once | Common |
|
||||
@@ -70,22 +70,43 @@ populates that field before the `connected` notification. Follow the shared
|
||||
|
||||
## Core HUD and service window
|
||||
|
||||
The fixed upper-right core shows `TWS ZOMBIES`, `discord.tws.lol`, side-by-side
|
||||
match/round timers, zombie count, current/maximum HP and a full-width health bar.
|
||||
HP stays visible at full health. A reserved footer holds the AFK indicator;
|
||||
there is no co-op name row. The core is created once per connection, including
|
||||
for spectator newcomers with placeholders until values are available. Creation
|
||||
does not seed the match clock. It persists across death and respawn.
|
||||
Clocks, health and zombie-count displays freeze at game over. Health fallbacks
|
||||
only affect the display and never modify gameplay max health.
|
||||
The unboxed core puts match time above round time at the upper left,
|
||||
`TWS ZOMBIES · discord.tws.lol` at the top center, and zombies remaining above a
|
||||
slim health bar at the bottom center. Current/maximum HP appears to the right
|
||||
of the bar only while injured; it disappears at full health. The AFK indicator
|
||||
sits above the zombie count. There are no panel backgrounds or timer labels.
|
||||
Fullscreen X/Y anchors use the normalized `640 × 480` viewport directly and
|
||||
bypass safe-area insets. Font scale is at least `1.0`; verify text fit, stock
|
||||
HUD overlap and Plutonium tag clearance at each aspect ratio in-game.
|
||||
|
||||
Common provides one fixed-height service window beneath the core for help or
|
||||
records. Services never move or resize the core. They remain private and work
|
||||
without the HUD module installed. See the [shared layout and validation guide](../../README.md#core-hud-and-service-window-t5-and-t6).
|
||||
The core is created once per connection, including for spectator newcomers
|
||||
with placeholders until values are available. Creation does not seed the match
|
||||
clock. It persists across death and respawn. Clocks, health and zombie-count
|
||||
displays freeze at game over. Health fallbacks only affect the display and
|
||||
never modify gameplay max health.
|
||||
|
||||
Install HUD `1.2.0`, leaderboard `1.1.0` and AFK `1.0.1` with common `1.2.0+`.
|
||||
Update the installed scripts together and restart the map; this change requires
|
||||
no new plugin/backend dependency or full server-process restart.
|
||||
The exact, case-insensitive `.hud` command toggles your core and AFK indicator.
|
||||
It defaults visible, keeps your choice across death/respawn on the current
|
||||
connection/map, and resets visible on reconnect or map restart. Timers and data
|
||||
continue updating while hidden; game-over values remain frozen. Help, records
|
||||
and the stock HUD are unaffected. `.hud` requires the HUD module.
|
||||
|
||||
Common provides one unboxed upper-right service area below the Plutonium tag.
|
||||
Records are right aligned, with `Map record · Round N` followed by up to three
|
||||
holder lines; there is no replay footer or single-page counter. Help displays
|
||||
the full nine-command list in aligned rows. Services remain private, leave the
|
||||
core in place and work without the HUD module installed. See the
|
||||
[shared layout and validation guide](../../README.md#core-hud-and-service-window-t5-and-t6).
|
||||
Only the active service view holds HUD elements: 19 for nine-command help
|
||||
(20 with a multi-page counter), or four for records (five with a multi-page
|
||||
counter). Pages reuse elements; switching views or closing the service releases
|
||||
them. Idle services hold no HUD elements, while the controller retains pending
|
||||
requests and interruption/resume behavior. Core + AFK use 11 + 1 elements.
|
||||
|
||||
HUD `1.4.0` and AFK `1.2.0` require common `1.4.0+`. Update common, HUD and AFK
|
||||
together where installed, then restart the map. Leaderboard `1.1.0` remains
|
||||
compatible. This change requires no new Plutonium/plugin/backend dependency or
|
||||
full server-process restart.
|
||||
|
||||
## Map record announcements
|
||||
|
||||
@@ -110,14 +131,17 @@ apply to both versions. All T6 script filenames use the `tws_t6_` prefix.
|
||||
Type `.help` or `.commands` in chat to see the commands supplied by the loaded TWS modules and
|
||||
a short description of each. The list is shown only to the requesting player,
|
||||
including spectators who have not spawned. Commands are case-insensitive.
|
||||
Help uses the service window below the core. It shows four commands per page,
|
||||
advances automatically every 12 seconds and closes after the last page. The
|
||||
panel retains its height on short pages. Typing either command again returns to
|
||||
page one with a fresh timer.
|
||||
Help uses the unboxed upper-right service area. All nine installed commands
|
||||
fit in one list with command and description columns. Additional registered
|
||||
commands use pages of nine rows, with a counter only for multi-page lists.
|
||||
Pages advance every 12 seconds and close after the last page. Typing either
|
||||
command again returns to page one with a fresh timer. Both engines use
|
||||
explicit row positions so T5 does not concatenate newline-separated text.
|
||||
|
||||
| Command | Description |
|
||||
| --- | --- |
|
||||
| `.help`, `.commands` | List available commands |
|
||||
| `.hud` | Hide or show your core HUD and AFK indicator (HUD module required) |
|
||||
| `.afk` | Pause with protection |
|
||||
| `.back` | Return from AFK |
|
||||
| `.join` | Join mid-round (newcomers only) |
|
||||
@@ -126,11 +150,14 @@ page one with a fresh timer.
|
||||
| `.d <amount\|all>` | Deposit points (bank module required) |
|
||||
| `.w <amount\|all>` | Withdraw points (bank module required) |
|
||||
|
||||
AFK and Join register their commands during `init()` through
|
||||
Command modules register their commands during `init()` through
|
||||
`tws_t6_common::register_help_command(command, description)`. If a module is
|
||||
omitted, its commands are omitted from `.help`. Commands follow the order above,
|
||||
independent of initialization order. Register new commands alongside their
|
||||
module's initialization so the list stays current.
|
||||
With all command modules installed, help shows one page of nine commands,
|
||||
including the full `.d <amount|all>` / `.w <amount|all>` syntax. Removing HUD
|
||||
removes `.hud` from the list.
|
||||
The enabled leaderboard module registers `.leaders`, which replays the latest
|
||||
cached map record privately. Repeating it restarts the record from the first page;
|
||||
help pauses the record's remaining page time and resumes it after the final
|
||||
@@ -140,7 +167,8 @@ help page. `.leaders` during help replaces the pending record replay from page o
|
||||
|
||||
- HUD values live in `tws_t6_common::common_config()`. That function and
|
||||
`ensure_config()` came from T5; they initialize in-script values and do not
|
||||
load a config file. Core and service panels use fullscreen upper-right anchors.
|
||||
load a config file. Core and service overlays use fullscreen X/Y anchors in
|
||||
normalized `640 × 480` coordinates to bypass safe-area insets.
|
||||
- AFK uses T5's native `level waittill("say", message, player)` listener.
|
||||
`.back` restores movement and weapons immediately, then ends protection after
|
||||
two seconds. The T6 control/weapon fix remains in place.
|
||||
|
||||
@@ -48,11 +48,11 @@ afk_spawn()
|
||||
self.hud_afk_on = false;
|
||||
self.tws_afk_protected = false;
|
||||
self.tws_afk_controls_locked = false;
|
||||
elem = scripts\zm\tws_t6_common::make_panel_text(self,
|
||||
level.hud_pad, level.hud_row_afk, "left",
|
||||
level.hud_font_afk, level.hud_c_afk);
|
||||
elem = scripts\zm\tws_t6_common::make_core_overlay_text(self,
|
||||
level.hud_overlay_afk_x, level.hud_overlay_afk_y, "center",
|
||||
1.0, level.hud_c_afk);
|
||||
elem SetText("[AFK]");
|
||||
elem.alpha = 0;
|
||||
scripts\zm\tws_t6_common::set_core_alpha(self, elem, 0);
|
||||
self.hud_afk_elem = elem;
|
||||
elems = [];
|
||||
elems[0] = elem;
|
||||
@@ -84,7 +84,7 @@ afk_enter()
|
||||
self.ignoreme = true;
|
||||
self enableInvulnerability();
|
||||
self afk_lock_controls();
|
||||
self.hud_afk_elem.alpha = 1;
|
||||
scripts\zm\tws_t6_common::set_core_alpha(self, self.hud_afk_elem, 1);
|
||||
self iPrintLnBold("AFK - type .back to return");
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ afk_exit()
|
||||
return;
|
||||
self.hud_afk_on = false;
|
||||
self afk_restore_controls();
|
||||
self.hud_afk_elem.alpha = 0;
|
||||
scripts\zm\tws_t6_common::set_core_alpha(self, self.hud_afk_elem, 0);
|
||||
self.tws_afk_generation++;
|
||||
generation = self.tws_afk_generation;
|
||||
self iPrintLnBold("Back in the fight");
|
||||
@@ -110,7 +110,7 @@ afk_release()
|
||||
self.hud_afk_on = false;
|
||||
self.tws_afk_protected = false;
|
||||
self afk_restore_controls();
|
||||
self.hud_afk_elem.alpha = 0;
|
||||
scripts\zm\tws_t6_common::set_core_alpha(self, self.hud_afk_elem, 0);
|
||||
// Stock scripts own protection during laststand/intermission/death.
|
||||
if (scripts\zm\tws_t6_common::player_active(self))
|
||||
{
|
||||
|
||||
+288
-123
@@ -25,8 +25,7 @@ common_config()
|
||||
if (isDefined(level.hud_x))
|
||||
return;
|
||||
|
||||
// Existing generic helpers retain their screen-anchor conventions. Panels
|
||||
// use their own right-edge origin and never push stock HUD areas around.
|
||||
// Retain legacy panel coordinates for existing helper consumers.
|
||||
level.hud_x = 10;
|
||||
level.hud_y = 30;
|
||||
// Retain the legacy fields for consumers of the original common config.
|
||||
@@ -40,7 +39,10 @@ common_config()
|
||||
level.hud_col_health = level.hud_x + 120 + 12;
|
||||
level.hud_health_threshold = 1.0;
|
||||
level.hud_panel_right = 10;
|
||||
level.hud_panel_top = 42;
|
||||
level.hud_panel_top = 20;
|
||||
// Fullscreen uses a 640x480 frame. Convert the panel's 480-height units
|
||||
// to that frame at the 16:9 reference aspect (640 / (480 * 16 / 9)).
|
||||
level.hud_panel_x_scale = 0.75;
|
||||
level.hud_panel_width = 220;
|
||||
level.hud_core_height = 100;
|
||||
level.hud_service_y = 108;
|
||||
@@ -70,6 +72,38 @@ common_config()
|
||||
level.hud_c_low = (1, 0.2, 0.2);
|
||||
level.hud_c_afk = (1, 0.8, 0.2);
|
||||
|
||||
// Minimal overlays use absolute coordinates in the fullscreen 640x480 frame.
|
||||
level.hud_overlay_timer_x = 6;
|
||||
level.hud_overlay_match_y = 8;
|
||||
level.hud_overlay_round_y = 30;
|
||||
level.hud_overlay_brand_x = 320;
|
||||
level.hud_overlay_brand_y = 8;
|
||||
level.hud_overlay_count_x = 300;
|
||||
level.hud_overlay_count_y = 440;
|
||||
level.hud_overlay_count_label_x = 306;
|
||||
level.hud_overlay_count_label_y = 446;
|
||||
level.hud_overlay_bar_x = 242;
|
||||
level.hud_overlay_bar_y = 466;
|
||||
level.hud_overlay_bar_width = 156;
|
||||
level.hud_overlay_bar_height = 4;
|
||||
level.hud_overlay_health_y = 460;
|
||||
level.hud_overlay_health_x = 414;
|
||||
level.hud_overlay_slash_x = 418;
|
||||
level.hud_overlay_maxhealth_x = 445;
|
||||
level.hud_overlay_hp_x = 449;
|
||||
level.hud_overlay_afk_x = 320;
|
||||
level.hud_overlay_afk_y = 421;
|
||||
level.hud_service_left = 432;
|
||||
level.hud_service_right = 633;
|
||||
level.hud_service_description_x = 538;
|
||||
level.hud_service_title_y = 54;
|
||||
level.hud_service_help_y = 80;
|
||||
level.hud_service_help_pitch = 24;
|
||||
level.hud_service_help_rows = 9;
|
||||
level.hud_service_record_y = 36;
|
||||
level.hud_service_holders_y = 60;
|
||||
level.hud_service_holders_pitch = 18;
|
||||
|
||||
if (!isDefined(level.tws_spawn_handlers))
|
||||
level.tws_spawn_handlers = [];
|
||||
}
|
||||
@@ -133,7 +167,7 @@ show_help()
|
||||
state = self.tws_service;
|
||||
service_advance(state, getTime());
|
||||
state.generation++;
|
||||
state.help = service_request("help", service_page_count(state.commands.size, 4), state.generation);
|
||||
state.help = service_request("help", service_page_count(state.commands.size, level.hud_service_help_rows), state.generation);
|
||||
service_render(state);
|
||||
}
|
||||
|
||||
@@ -205,14 +239,15 @@ service_advance(state, now)
|
||||
help_rank(command)
|
||||
{
|
||||
if (command == ".help") return 0;
|
||||
if (command == ".afk") return 1;
|
||||
if (command == ".back") return 2;
|
||||
if (command == ".join") return 3;
|
||||
if (command == ".leaders") return 4;
|
||||
if (command == ".b") return 5;
|
||||
if (command.size >= 3 && getSubStr(command, 0, 3) == ".d ") return 6;
|
||||
if (command.size >= 3 && getSubStr(command, 0, 3) == ".w ") return 7;
|
||||
return 8;
|
||||
if (command == ".hud") return 1;
|
||||
if (command == ".afk") return 2;
|
||||
if (command == ".back") return 3;
|
||||
if (command == ".join") return 4;
|
||||
if (command == ".leaders") return 5;
|
||||
if (command == ".b") return 6;
|
||||
if (command.size >= 3 && getSubStr(command, 0, 3) == ".d ") return 7;
|
||||
if (command.size >= 3 && getSubStr(command, 0, 3) == ".w ") return 8;
|
||||
return 9;
|
||||
}
|
||||
|
||||
help_entries()
|
||||
@@ -220,7 +255,7 @@ help_entries()
|
||||
entries = [];
|
||||
if (!isDefined(level.tws_help_commands))
|
||||
return entries;
|
||||
for (rank = 0; rank <= 8; rank++)
|
||||
for (rank = 0; rank <= 9; rank++)
|
||||
{
|
||||
for (i = 0; i < level.tws_help_commands.size; i++)
|
||||
{
|
||||
@@ -234,9 +269,6 @@ help_entries()
|
||||
|
||||
help_command_label(command)
|
||||
{
|
||||
rank = help_rank(command);
|
||||
if (rank == 6) return ".d <n|all>";
|
||||
if (rank == 7) return ".w <n|all>";
|
||||
return command;
|
||||
}
|
||||
|
||||
@@ -244,13 +276,14 @@ help_description(entry)
|
||||
{
|
||||
rank = help_rank(entry.command);
|
||||
if (rank == 0) return "Show commands";
|
||||
if (rank == 1) return "Enable protection";
|
||||
if (rank == 2) return "Return to play";
|
||||
if (rank == 3) return "Join as newcomer";
|
||||
if (rank == 4) return "View records";
|
||||
if (rank == 5) return "Check balance";
|
||||
if (rank == 6) return "Deposit points";
|
||||
if (rank == 7) return "Withdraw points";
|
||||
if (rank == 1) return "Toggle core HUD";
|
||||
if (rank == 2) return "Protect & freeze";
|
||||
if (rank == 3) return "Return to play";
|
||||
if (rank == 4) return "Join as newcomer";
|
||||
if (rank == 5) return "View records";
|
||||
if (rank == 6) return "Bank balance";
|
||||
if (rank == 7) return "Deposit";
|
||||
if (rank == 8) return "Withdraw";
|
||||
return entry.description;
|
||||
}
|
||||
|
||||
@@ -273,57 +306,82 @@ service_ensure()
|
||||
state.record_text = [];
|
||||
state.help_page = -1;
|
||||
state.rows = [];
|
||||
state.command_rows = [];
|
||||
state.description_rows = [];
|
||||
state.view_kind = undefined;
|
||||
self.tws_service = state;
|
||||
|
||||
y = level.hud_service_y;
|
||||
state.background = make_panel_shader(self, 0, y, level.hud_panel_width,
|
||||
level.hud_service_height, level.hud_c_panel, level.hud_panel_alpha, 0);
|
||||
state.elems[state.elems.size] = state.background;
|
||||
state.help_title = service_text(self, state, 10, 10, "left", 1.1, "COMMANDS", level.hud_c_label);
|
||||
state.record_title = service_text(self, state, 10, 10, "left", 1.1, "MAP RECORD", level.hud_c_label);
|
||||
state.help_footer = service_text(self, state, 10, 108, "left", 0.9,
|
||||
"Four commands per page", level.hud_c_dim);
|
||||
state.record_footer = service_text(self, state, 10, 108, "left", 0.9,
|
||||
"Briefly shown - .leaders to view again", level.hud_c_dim);
|
||||
state.page_number = service_text(self, state, 178, 10, "right", 1.1, "", level.hud_c_dim);
|
||||
state.page_slash = service_text(self, state, 184, 10, "center", 1.1, "/", level.hud_c_dim);
|
||||
state.page_total = service_text(self, state, 210, 10, "right", 1.1, "", level.hud_c_dim);
|
||||
// Two fixed text columns keep the HUD element budget independent of the
|
||||
// command count. Strings change only at page boundaries, never per tick.
|
||||
state.command_back = make_panel_shader(self, 10, y + 30, 65, 68,
|
||||
(0.16, 0.19, 0.17), 0.9, 5);
|
||||
state.elems[state.elems.size] = state.command_back;
|
||||
state.command_column = service_text(self, state, 14, 31,
|
||||
"left", 1.0, "", level.hud_c_name);
|
||||
state.description_column = service_text(self, state, 81, 31,
|
||||
"left", 1.0, "", level.hud_c_count);
|
||||
state.round_label = service_text(self, state, 10, 32, "left", 1.1, "ROUND", level.hud_c_label);
|
||||
state.round_number = service_text(self, state, 54, 27, "left", 1.9, "", level.hud_c_count);
|
||||
state.holders_label = service_text(self, state, 10, 52, "left", 1.0, "RECORD HOLDERS", level.hud_c_label);
|
||||
for (i = 0; i < 3; i++)
|
||||
state.rows[i] = service_text(self, state, 10, 65 + i * 13,
|
||||
"left", 1.1, "", level.hud_c_count);
|
||||
state.empty = service_text(self, state, 10, 40, "left", 1.1,
|
||||
"No recorded high round yet", level.hud_c_count);
|
||||
state.unavailable = service_text(self, state, 10, 40, "left", 1.1,
|
||||
"Map record unavailable", level.hud_c_count);
|
||||
for (i = 0; i < state.elems.size; i++)
|
||||
{
|
||||
state.elems[i].alpha = 0;
|
||||
// Keep temporary private services out of the archived HUD bucket.
|
||||
// Hidden elements still occupy slots; do not allocate one per page.
|
||||
state.elems[i].archived = false;
|
||||
}
|
||||
|
||||
self thread service_loop(state);
|
||||
self thread service_disconnect(state);
|
||||
self thread service_end_game(state);
|
||||
}
|
||||
|
||||
// Allocate only the active view. Alpha-zero elements still occupy HUD slots.
|
||||
service_view_create(state, request)
|
||||
{
|
||||
service_view_destroy(state);
|
||||
state.view_kind = request.kind;
|
||||
state.view_paged = request.pages > 1;
|
||||
if (state.view_paged)
|
||||
state.page_counter = service_text(self, state, level.hud_service_right,
|
||||
level.hud_service_title_y, "right", 1.0, "", level.hud_c_dim);
|
||||
|
||||
if (request.kind == "help")
|
||||
{
|
||||
state.help_title = service_text(self, state, level.hud_service_left,
|
||||
level.hud_service_title_y, "left", 1.1, "COMMANDS", level.hud_c_label);
|
||||
rows = state.commands.size;
|
||||
if (rows > level.hud_service_help_rows)
|
||||
rows = level.hud_service_help_rows;
|
||||
// Explicit lines also work on T5, which can collapse multiline strings.
|
||||
for (i = 0; i < rows; i++)
|
||||
{
|
||||
y = level.hud_service_help_y + i * level.hud_service_help_pitch;
|
||||
state.command_rows[i] = service_text(self, state, level.hud_service_left,
|
||||
y, "left", 1.0, "", level.hud_c_count);
|
||||
state.description_rows[i] = service_text(self, state, level.hud_service_description_x,
|
||||
y, "left", 1.0, "", level.hud_c_label);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
state.record_title = service_text(self, state, level.hud_service_right,
|
||||
level.hud_service_record_y, "right", 1.1, "", level.hud_c_count);
|
||||
for (i = 0; i < 3; i++)
|
||||
state.rows[i] = service_text(self, state, level.hud_service_right,
|
||||
level.hud_service_holders_y + i * level.hud_service_holders_pitch,
|
||||
"right", 1.05, "", level.hud_c_label);
|
||||
if (isDefined(state.page_counter))
|
||||
state.page_counter.y = level.hud_service_holders_y + 3 * level.hud_service_holders_pitch;
|
||||
}
|
||||
|
||||
service_view_destroy(state)
|
||||
{
|
||||
for (i = 0; i < state.elems.size; i++)
|
||||
{
|
||||
if (isDefined(state.elems[i]))
|
||||
state.elems[i] Destroy();
|
||||
}
|
||||
state.elems = [];
|
||||
state.view_kind = undefined;
|
||||
state.view_paged = undefined;
|
||||
state.page_counter = undefined;
|
||||
state.help_title = undefined;
|
||||
state.command_rows = [];
|
||||
state.description_rows = [];
|
||||
state.record_title = undefined;
|
||||
state.rows = [];
|
||||
state.page_text = undefined;
|
||||
state.record_header_text = undefined;
|
||||
state.record_text = [];
|
||||
state.help_page = -1;
|
||||
}
|
||||
|
||||
service_text(player, state, x, y, align, scale, text, colour)
|
||||
{
|
||||
elem = make_panel_text(player, x, level.hud_service_y + y, align, scale, colour);
|
||||
elem = make_overlay_text(player, x, y, align, scale, colour);
|
||||
elem.alpha = 0;
|
||||
elem.archived = false;
|
||||
if (text != "")
|
||||
elem SetText(text);
|
||||
state.elems[state.elems.size] = elem;
|
||||
@@ -347,74 +405,70 @@ service_render(state)
|
||||
state.rendered = true;
|
||||
state.last_generation = generation;
|
||||
state.last_page = page;
|
||||
if (!isDefined(request))
|
||||
{
|
||||
service_view_destroy(state);
|
||||
return;
|
||||
}
|
||||
paged = request.pages > 1;
|
||||
if (!isDefined(state.view_kind) || state.view_kind != request.kind || state.view_paged != paged)
|
||||
service_view_create(state, request);
|
||||
for (i = 0; i < state.elems.size; i++)
|
||||
state.elems[i].alpha = 0;
|
||||
if (!isDefined(request))
|
||||
return;
|
||||
|
||||
state.background.alpha = level.hud_panel_alpha;
|
||||
state.page_number SetValue(page + 1);
|
||||
state.page_total SetValue(request.pages);
|
||||
state.page_number.alpha = 1;
|
||||
state.page_slash.alpha = 1;
|
||||
state.page_total.alpha = 1;
|
||||
if (paged)
|
||||
{
|
||||
page_text = "" + (page + 1) + " / " + request.pages;
|
||||
if (!isDefined(state.page_text) || state.page_text != page_text)
|
||||
{
|
||||
state.page_counter SetText(page_text);
|
||||
state.page_text = page_text;
|
||||
}
|
||||
state.page_counter.alpha = 1;
|
||||
}
|
||||
if (request.kind == "help")
|
||||
{
|
||||
state.help_title.alpha = 1;
|
||||
state.help_footer.alpha = 1;
|
||||
if (state.help_page != page)
|
||||
for (i = 0; i < state.command_rows.size; i++)
|
||||
{
|
||||
command_text = "";
|
||||
description_text = "";
|
||||
for (i = 0; i < 4; i++)
|
||||
index = page * level.hud_service_help_rows + i;
|
||||
if (index >= state.commands.size)
|
||||
continue;
|
||||
if (state.help_page != page)
|
||||
{
|
||||
index = page * 4 + i;
|
||||
if (index < state.commands.size)
|
||||
{
|
||||
if (i > 0)
|
||||
{
|
||||
command_text += "\n";
|
||||
description_text += "\n";
|
||||
}
|
||||
entry = state.commands[index];
|
||||
command_text += help_command_label(entry.command);
|
||||
description_text += help_description(entry);
|
||||
}
|
||||
entry = state.commands[index];
|
||||
state.command_rows[i] SetText(help_command_label(entry.command));
|
||||
state.description_rows[i] SetText(help_description(entry));
|
||||
}
|
||||
state.command_column SetText(command_text);
|
||||
state.description_column SetText(description_text);
|
||||
state.help_page = page;
|
||||
state.command_rows[i].alpha = 1;
|
||||
state.description_rows[i].alpha = 1;
|
||||
}
|
||||
state.command_back.alpha = 0.9;
|
||||
state.command_column.alpha = 1;
|
||||
state.description_column.alpha = 1;
|
||||
state.help_page = page;
|
||||
return;
|
||||
}
|
||||
|
||||
state.record_title.alpha = 1;
|
||||
state.record_footer.alpha = 1;
|
||||
header = "Map record unavailable";
|
||||
snapshot = request.snapshot;
|
||||
if (!isDefined(snapshot))
|
||||
if (isDefined(snapshot))
|
||||
{
|
||||
state.unavailable.alpha = 1;
|
||||
return;
|
||||
header = "No recorded high round yet";
|
||||
if (snapshot.round > 0)
|
||||
header = "Map record · Round " + snapshot.round;
|
||||
}
|
||||
if (snapshot.round == 0)
|
||||
if (!isDefined(state.record_header_text) || state.record_header_text != header)
|
||||
{
|
||||
state.empty.alpha = 1;
|
||||
return;
|
||||
state.record_title SetText(header);
|
||||
state.record_header_text = header;
|
||||
}
|
||||
state.round_label.alpha = 1;
|
||||
state.round_number SetValue(snapshot.round);
|
||||
state.round_number.alpha = 1;
|
||||
state.holders_label.alpha = 1;
|
||||
state.record_title.alpha = 1;
|
||||
if (!isDefined(snapshot) || snapshot.round == 0)
|
||||
return;
|
||||
for (i = 0; i < 3; i++)
|
||||
{
|
||||
index = page * 3 + i;
|
||||
if (index < snapshot.lines.size)
|
||||
{
|
||||
text = snapshot.lines[index];
|
||||
// Skip identical text when replaying a page or resuming after help.
|
||||
if (!isDefined(state.record_text[i]) || state.record_text[i] != text)
|
||||
{
|
||||
state.rows[i] SetText(text);
|
||||
@@ -457,12 +511,7 @@ service_destroy(state)
|
||||
state.closed = true;
|
||||
state.help = undefined;
|
||||
state.record = undefined;
|
||||
for (i = 0; i < state.elems.size; i++)
|
||||
{
|
||||
if (isDefined(state.elems[i]))
|
||||
state.elems[i] Destroy();
|
||||
}
|
||||
state.elems = [];
|
||||
service_view_destroy(state);
|
||||
}
|
||||
|
||||
service_game_over_via(event)
|
||||
@@ -523,7 +572,8 @@ make_text_elem(player, x, y, align_x, scale, colour)
|
||||
elem.x = x;
|
||||
elem.y = y;
|
||||
elem.foreground = 1;
|
||||
elem.fontScale = scale;
|
||||
// Sub-1.0 text can render oversized on clients; keep a safe minimum.
|
||||
elem.fontScale = max(1.0, scale);
|
||||
elem.alpha = 1;
|
||||
elem.color = colour;
|
||||
|
||||
@@ -550,14 +600,59 @@ make_shader_elem(player, x, y)
|
||||
return elem;
|
||||
}
|
||||
|
||||
// Panel x/y are local to the fixed upper-right core origin. A right screen
|
||||
// anchor keeps the panel on-screen across aspect ratios; negative offsets grow
|
||||
// left from that edge. Keep each engine's existing vertical anchor convention.
|
||||
// Absolute fullscreen overlays leave stock HUD areas and the center clear.
|
||||
make_overlay_text(player, x, y, align_x, scale, colour)
|
||||
{
|
||||
elem = make_text_elem(player, x, y, align_x, scale, colour);
|
||||
elem.horzAlign = "fullscreen";
|
||||
elem.vertAlign = "fullscreen";
|
||||
elem.sort = 20;
|
||||
elem.archived = true;
|
||||
return elem;
|
||||
}
|
||||
|
||||
make_overlay_shader(player, x, y, width, height, colour, alpha, sort)
|
||||
{
|
||||
elem = make_shader_elem(player, x, y);
|
||||
elem.horzAlign = "fullscreen";
|
||||
elem.vertAlign = "fullscreen";
|
||||
elem.color = colour;
|
||||
elem.alpha = alpha;
|
||||
elem.sort = sort;
|
||||
elem.archived = true;
|
||||
elem SetShader("white", make_overlay_width(width), int(max(1, height)));
|
||||
return elem;
|
||||
}
|
||||
|
||||
make_overlay_width(width)
|
||||
{
|
||||
// Native max() returns a float; shader dimensions must be integers.
|
||||
return int(max(1, width));
|
||||
}
|
||||
|
||||
make_core_overlay_text(player, x, y, align_x, scale, colour)
|
||||
{
|
||||
elem = make_overlay_text(player, x, y, align_x, scale, colour);
|
||||
register_core_elem(player, elem);
|
||||
return elem;
|
||||
}
|
||||
|
||||
make_core_overlay_shader(player, x, y, width, height, colour, alpha, sort)
|
||||
{
|
||||
elem = make_overlay_shader(player, x, y, width, height, colour, alpha, sort);
|
||||
register_core_elem(player, elem);
|
||||
return elem;
|
||||
}
|
||||
|
||||
// Panel x/y are local to the upper-right core origin. Fullscreen bypasses the
|
||||
// engine safe area. Horizontal conversion preserves the original size at 16:9;
|
||||
// other aspects retain the right-edge anchor with proportional horizontal size.
|
||||
make_panel_text(player, x, y, align_x, scale, colour)
|
||||
{
|
||||
elem = make_text_elem(player, x - level.hud_panel_width - level.hud_panel_right,
|
||||
elem = make_text_elem(player, 640 + (x - level.hud_panel_width - level.hud_panel_right) * level.hud_panel_x_scale,
|
||||
level.hud_panel_top + y, align_x, scale, colour);
|
||||
elem.horzAlign = "right";
|
||||
elem.horzAlign = "fullscreen";
|
||||
elem.vertAlign = "fullscreen";
|
||||
elem.sort = 20;
|
||||
elem.archived = true;
|
||||
return elem;
|
||||
@@ -565,17 +660,87 @@ make_panel_text(player, x, y, align_x, scale, colour)
|
||||
|
||||
make_panel_shader(player, x, y, width, height, colour, alpha, sort)
|
||||
{
|
||||
elem = make_shader_elem(player, x - level.hud_panel_width - level.hud_panel_right,
|
||||
elem = make_shader_elem(player, 640 + (x - level.hud_panel_width - level.hud_panel_right) * level.hud_panel_x_scale,
|
||||
level.hud_panel_top + y);
|
||||
elem.horzAlign = "right";
|
||||
elem.horzAlign = "fullscreen";
|
||||
elem.vertAlign = "fullscreen";
|
||||
elem.color = colour;
|
||||
elem.alpha = alpha;
|
||||
elem.sort = sort;
|
||||
elem.archived = true;
|
||||
elem SetShader("white", width, height);
|
||||
elem SetShader("white", make_panel_width(width), height);
|
||||
return elem;
|
||||
}
|
||||
|
||||
// Native shader widths are integers; dynamic fills use the same conversion.
|
||||
make_panel_width(width)
|
||||
{
|
||||
// max() returns a float even for integer inputs; cast its final result.
|
||||
return int(max(1, width * level.hud_panel_x_scale));
|
||||
}
|
||||
|
||||
// Core elements share a visibility mask; services keep their independent alpha.
|
||||
make_core_text(player, x, y, align_x, scale, colour)
|
||||
{
|
||||
elem = make_panel_text(player, x, y, align_x, scale, colour);
|
||||
register_core_elem(player, elem);
|
||||
return elem;
|
||||
}
|
||||
|
||||
make_core_shader(player, x, y, width, height, colour, alpha, sort)
|
||||
{
|
||||
elem = make_panel_shader(player, x, y, width, height, colour, alpha, sort);
|
||||
register_core_elem(player, elem);
|
||||
return elem;
|
||||
}
|
||||
|
||||
register_core_elem(player, elem)
|
||||
{
|
||||
live = [];
|
||||
if (isDefined(player.tws_core_hud_elems))
|
||||
{
|
||||
for (i = 0; i < player.tws_core_hud_elems.size; i++)
|
||||
{
|
||||
old = player.tws_core_hud_elems[i];
|
||||
if (isDefined(old))
|
||||
live[live.size] = old;
|
||||
}
|
||||
}
|
||||
live[live.size] = elem;
|
||||
player.tws_core_hud_elems = live;
|
||||
set_core_alpha(player, elem, elem.alpha);
|
||||
}
|
||||
|
||||
set_core_alpha(player, elem, alpha)
|
||||
{
|
||||
if (!isDefined(elem))
|
||||
return;
|
||||
elem.tws_core_alpha = alpha;
|
||||
elem.alpha = alpha;
|
||||
if (isDefined(player.tws_hud_hidden) && player.tws_hud_hidden)
|
||||
elem.alpha = 0;
|
||||
}
|
||||
|
||||
toggle_core_hud()
|
||||
{
|
||||
hidden = false;
|
||||
if (isDefined(self.tws_hud_hidden))
|
||||
hidden = self.tws_hud_hidden;
|
||||
self.tws_hud_hidden = !hidden;
|
||||
if (!isDefined(self.tws_core_hud_elems))
|
||||
return;
|
||||
live = [];
|
||||
for (i = 0; i < self.tws_core_hud_elems.size; i++)
|
||||
{
|
||||
elem = self.tws_core_hud_elems[i];
|
||||
if (!isDefined(elem))
|
||||
continue;
|
||||
set_core_alpha(self, elem, elem.tws_core_alpha);
|
||||
live[live.size] = elem;
|
||||
}
|
||||
self.tws_core_hud_elems = live;
|
||||
}
|
||||
|
||||
destroy_on_disconnect(elems)
|
||||
{
|
||||
self waittill("disconnect");
|
||||
|
||||
+72
-47
@@ -1,4 +1,4 @@
|
||||
/* tws_t6_hud.gsc - fixed upper-right core HUD, visible from connection. */
|
||||
/* tws_t6_hud.gsc - minimal edge HUD, toggleable with .hud. */
|
||||
|
||||
#include maps\mp\_utility;
|
||||
#include common_scripts\utility;
|
||||
@@ -11,14 +11,34 @@ main()
|
||||
init()
|
||||
{
|
||||
scripts\zm\tws_t6_common::ensure_config();
|
||||
scripts\zm\tws_t6_common::register_help_command(".hud", "Toggle core HUD");
|
||||
|
||||
level thread round_watcher();
|
||||
level thread game_over_watcher();
|
||||
level thread hud_chat_listener();
|
||||
|
||||
scripts\zm\tws_t6_common::register_connect_handler(::hud_connect);
|
||||
scripts\zm\tws_t6_common::register_spawn_handler(::hud_start);
|
||||
}
|
||||
|
||||
hud_chat_listener()
|
||||
{
|
||||
for (;;)
|
||||
{
|
||||
level waittill("say", message, player);
|
||||
if (!isDefined(player) || !isDefined(message))
|
||||
continue;
|
||||
if (toLower(message) == ".hud")
|
||||
player thread hud_toggle();
|
||||
}
|
||||
}
|
||||
|
||||
hud_toggle()
|
||||
{
|
||||
self hud_connect();
|
||||
self scripts\zm\tws_t6_common::toggle_core_hud();
|
||||
}
|
||||
|
||||
// Connection construction never seeds clocks or reads player gameplay state.
|
||||
// The first-spawn fallback also handles a spawn before the connect callback.
|
||||
hud_connect()
|
||||
@@ -29,48 +49,42 @@ hud_connect()
|
||||
return;
|
||||
|
||||
self.tws_hud_elems = [];
|
||||
background = scripts\zm\tws_t6_common::make_panel_shader(
|
||||
self, 0, 0, level.hud_panel_width, level.hud_core_height,
|
||||
level.hud_c_panel, level.hud_panel_alpha, 0);
|
||||
self.tws_hud_elems[self.tws_hud_elems.size] = background;
|
||||
self.tws_hud_match = self hud_text(level.hud_overlay_timer_x,
|
||||
level.hud_overlay_match_y, "left", 1.4, level.hud_c_count, "--:--");
|
||||
self.tws_hud_round = self hud_text(level.hud_overlay_timer_x,
|
||||
level.hud_overlay_round_y, "left", 1.25, level.hud_c_dim, "--:--");
|
||||
self hud_text(level.hud_overlay_brand_x, level.hud_overlay_brand_y,
|
||||
"center", 1.1, level.hud_c_dim, "TWS ZOMBIES · discord.tws.lol");
|
||||
|
||||
self hud_text(level.hud_pad, level.hud_row_header, "left",
|
||||
level.hud_font_small, level.hud_c_name, "TWS ZOMBIES");
|
||||
self hud_text(level.hud_panel_width - level.hud_pad, level.hud_row_header,
|
||||
"right", level.hud_font_small, level.hud_c_dim, "discord.tws.lol");
|
||||
|
||||
self hud_text(level.hud_pad, level.hud_row_timers, "left",
|
||||
level.hud_font_small, level.hud_c_label, "MATCH");
|
||||
self.tws_hud_match = self hud_text(104, level.hud_row_timers - 2, "right",
|
||||
level.hud_font_mid, level.hud_c_count, "--:--");
|
||||
self hud_text(114, level.hud_row_timers, "left",
|
||||
level.hud_font_small, level.hud_c_label, "ROUND");
|
||||
self.tws_hud_round = self hud_text(level.hud_panel_width - level.hud_pad,
|
||||
level.hud_row_timers - 2, "right",
|
||||
level.hud_font_mid, level.hud_c_count, "--:--");
|
||||
|
||||
self hud_text(level.hud_pad, level.hud_row_stats, "left",
|
||||
level.hud_font_small, level.hud_c_label, "ZOMBIES");
|
||||
self.tws_hud_count = self hud_text(67, level.hud_row_stats, "left",
|
||||
level.hud_font_mid, level.hud_c_count, "--");
|
||||
self hud_text(132, level.hud_row_stats, "left",
|
||||
level.hud_font_small, level.hud_c_label, "HP");
|
||||
self.tws_hud_count = self hud_text(level.hud_overlay_count_x,
|
||||
level.hud_overlay_count_y, "right", 1.5, level.hud_c_count, "--");
|
||||
self hud_text(level.hud_overlay_count_label_x, level.hud_overlay_count_label_y,
|
||||
"left", 1.05, level.hud_c_dim, "ZOMBIES");
|
||||
|
||||
// Independent numeric elements avoid a new config string on every tick.
|
||||
self.tws_hud_health = self hud_text(174, level.hud_row_stats, "right",
|
||||
level.hud_font_mid, level.hud_c_count, "--");
|
||||
self.tws_hud_slash = self hud_text(181, level.hud_row_stats, "center",
|
||||
level.hud_font_mid, level.hud_c_count, "/");
|
||||
self.tws_hud_maxhealth = self hud_text(level.hud_panel_width - level.hud_pad,
|
||||
level.hud_row_stats, "right", level.hud_font_mid, level.hud_c_count, "--");
|
||||
// The readout appears only after a valid damaged-health sample.
|
||||
self.tws_hud_health = self hud_text(level.hud_overlay_health_x,
|
||||
level.hud_overlay_health_y, "right", 1.0, level.hud_c_count, "--");
|
||||
self.tws_hud_slash = self hud_text(level.hud_overlay_slash_x,
|
||||
level.hud_overlay_health_y, "left", 1.0, level.hud_c_count, "/");
|
||||
self.tws_hud_maxhealth = self hud_text(level.hud_overlay_maxhealth_x,
|
||||
level.hud_overlay_health_y, "right", 1.0, level.hud_c_count, "--");
|
||||
self.tws_hud_hp = self hud_text(level.hud_overlay_hp_x,
|
||||
level.hud_overlay_health_y, "left", 1.0, level.hud_c_count, "HP");
|
||||
scripts\zm\tws_t6_common::set_core_alpha(self, self.tws_hud_health, 0);
|
||||
scripts\zm\tws_t6_common::set_core_alpha(self, self.tws_hud_slash, 0);
|
||||
scripts\zm\tws_t6_common::set_core_alpha(self, self.tws_hud_maxhealth, 0);
|
||||
scripts\zm\tws_t6_common::set_core_alpha(self, self.tws_hud_hp, 0);
|
||||
|
||||
track = scripts\zm\tws_t6_common::make_panel_shader(
|
||||
self, level.hud_pad, level.hud_row_bar,
|
||||
level.hud_bar_width, level.hud_bar_height, (0.18, 0.22, 0.19), 0.8, 10);
|
||||
track = scripts\zm\tws_t6_common::make_core_overlay_shader(
|
||||
self, level.hud_overlay_bar_x, level.hud_overlay_bar_y,
|
||||
level.hud_overlay_bar_width, level.hud_overlay_bar_height,
|
||||
(0.18, 0.22, 0.19), 0.8, 10);
|
||||
self.tws_hud_elems[self.tws_hud_elems.size] = track;
|
||||
self.tws_hud_bar = scripts\zm\tws_t6_common::make_panel_shader(
|
||||
self, level.hud_pad, level.hud_row_bar,
|
||||
level.hud_bar_width, level.hud_bar_height, level.hud_c_high, 0, 11);
|
||||
self.tws_hud_bar = scripts\zm\tws_t6_common::make_core_overlay_shader(
|
||||
self, level.hud_overlay_bar_x, level.hud_overlay_bar_y,
|
||||
level.hud_overlay_bar_width, level.hud_overlay_bar_height,
|
||||
level.hud_c_high, 0, 11);
|
||||
self.tws_hud_elems[self.tws_hud_elems.size] = self.tws_hud_bar;
|
||||
|
||||
self thread scripts\zm\tws_t6_common::destroy_on_disconnect(self.tws_hud_elems);
|
||||
@@ -78,7 +92,7 @@ hud_connect()
|
||||
|
||||
hud_text(x, y, align, scale, colour, text)
|
||||
{
|
||||
elem = scripts\zm\tws_t6_common::make_panel_text(self, x, y, align, scale, colour);
|
||||
elem = scripts\zm\tws_t6_common::make_core_overlay_text(self, x, y, align, scale, colour);
|
||||
elem SetText(text);
|
||||
self.tws_hud_elems[self.tws_hud_elems.size] = elem;
|
||||
return elem;
|
||||
@@ -147,7 +161,7 @@ hud_timer_row()
|
||||
{
|
||||
self endon("disconnect");
|
||||
|
||||
// A visible panel for a waiting newcomer must not start a pre-set round 1.
|
||||
// A visible HUD for a waiting newcomer must not start a pre-set round 1.
|
||||
if (!isDefined(level.match_start_time) &&
|
||||
isDefined(level.round_number) &&
|
||||
level.round_number >= 1)
|
||||
@@ -194,7 +208,7 @@ match_timer_bind(elem)
|
||||
elem SetTimerUp(max(0, (getTime() - level.match_start_time) / 1000.0));
|
||||
}
|
||||
|
||||
// The panel remains visible at game over, with authoritative frozen clocks.
|
||||
// Preserve authoritative frozen clocks when the core is shown after game over.
|
||||
freeze_timers_on_game_over(match_elem, round_elem)
|
||||
{
|
||||
self endon("disconnect");
|
||||
@@ -238,11 +252,12 @@ hud_health_display()
|
||||
self endon("disconnect");
|
||||
level endon("hud_game_over");
|
||||
|
||||
bar_width = level.hud_bar_width;
|
||||
bar_width = level.hud_overlay_bar_width;
|
||||
bar = self.tws_hud_bar;
|
||||
cur = self.tws_hud_health;
|
||||
slash = self.tws_hud_slash;
|
||||
max_hp = self.tws_hud_maxhealth;
|
||||
hp = self.tws_hud_hp;
|
||||
last_width = -1;
|
||||
last_health = -1;
|
||||
last_max = -1;
|
||||
@@ -261,16 +276,17 @@ hud_health_display()
|
||||
if (frac > 1)
|
||||
frac = 1;
|
||||
|
||||
width = int(max(frac * bar_width, 1));
|
||||
width = scripts\zm\tws_t6_common::make_overlay_width(frac * bar_width);
|
||||
if (width != last_width)
|
||||
{
|
||||
bar SetShader("white", width, level.hud_bar_height);
|
||||
bar SetShader("white", width, level.hud_overlay_bar_height);
|
||||
last_width = width;
|
||||
}
|
||||
|
||||
bar.alpha = 1;
|
||||
bar_alpha = 1;
|
||||
if (frac == 0)
|
||||
bar.alpha = 0;
|
||||
bar_alpha = 0;
|
||||
scripts\zm\tws_t6_common::set_core_alpha(self, bar, bar_alpha);
|
||||
|
||||
colour = level.hud_c_high;
|
||||
if (frac <= 0.25)
|
||||
@@ -282,8 +298,17 @@ hud_health_display()
|
||||
cur.color = colour;
|
||||
slash.color = colour;
|
||||
max_hp.color = colour;
|
||||
hp.color = colour;
|
||||
|
||||
// Health numbers remain visible at full health.
|
||||
readout_alpha = 0;
|
||||
if (isDefined(self.health) && current < maximum)
|
||||
readout_alpha = 1;
|
||||
scripts\zm\tws_t6_common::set_core_alpha(self, cur, readout_alpha);
|
||||
scripts\zm\tws_t6_common::set_core_alpha(self, slash, readout_alpha);
|
||||
scripts\zm\tws_t6_common::set_core_alpha(self, max_hp, readout_alpha);
|
||||
scripts\zm\tws_t6_common::set_core_alpha(self, hp, readout_alpha);
|
||||
|
||||
// Keep health numbers current even at full health or while hidden.
|
||||
if (current != last_health)
|
||||
{
|
||||
cur SetValue(current);
|
||||
|
||||
+238
-59
@@ -1,5 +1,6 @@
|
||||
// Execute selected core HUD functions with engine-value shims.
|
||||
// Execute selected core HUD/AFK functions with engine-value shims.
|
||||
// These checks are not a GSC VM, compiler, linker, or live layout test.
|
||||
// Common visibility helpers are stubbed here and executed directly by check_hud.
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import vm from 'node:vm';
|
||||
@@ -10,86 +11,264 @@ function body(source, name) {
|
||||
const match = new RegExp(`^${name}\\(([^\\n]*)\\)\\s*\\n\\{`, 'm').exec(source);
|
||||
assert(match, name);
|
||||
let at = match.index + match[0].length, end = at, depth = 1;
|
||||
while (depth) { const ch = source[end++]; if (ch === '{') depth++; if (ch === '}') depth--; }
|
||||
while (depth) {
|
||||
assert(end < source.length, `unclosed ${name}`);
|
||||
const ch = source[end++];
|
||||
if (ch === '{') depth++;
|
||||
if (ch === '}') depth--;
|
||||
}
|
||||
return [match[1], source.slice(at, end - 1)];
|
||||
}
|
||||
function install(context, source, names) {
|
||||
for (const name of names) {
|
||||
const [args, original] = body(source, name);
|
||||
let code = original.replace(/\/\/[^\n]*/g, '');
|
||||
const locals = [...new Set([...code.matchAll(/(?<![\w.])(\w+)\s*(?:=(?!=)|\+\+|--)/g)].map(match => match[1]))];
|
||||
code = code.replace(/(?:self|level) endon\([^;]+;/g, '')
|
||||
.replace(/scripts\\(?:sp|zm)\\tws_t[56]_common::/g, 'common.')
|
||||
.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(/level notify\(([^;]*)\);/g, 'level.notify($1);')
|
||||
.replace(/level waittill\(([^;]*)\);/g, 'yield { event: $1 };')
|
||||
.replace(/\.size\b/g, '.length')
|
||||
.replace(/\((\d+\.\d+), (\d+\.\d+), (\d+\.\d+)\)/g, '[$1, $2, $3]')
|
||||
.replace(/\bwait\s+([^;]+);/g, 'yield $1;');
|
||||
vm.runInContext(`function${code.includes('yield ') ? '*' : ''} ${name}(${args}) { var ${locals.join(', ') || 'unused'}; ${code} }`, context);
|
||||
}
|
||||
}
|
||||
function engine(game, source, afk, hidden = false) {
|
||||
const queued = [], elems = [], actions = [];
|
||||
let now = 123456;
|
||||
const self = { active: true };
|
||||
if (hidden) self.tws_hud_hidden = true;
|
||||
for (const name of ['enableInvulnerability', 'disableInvulnerability', 'freezeControls',
|
||||
'disableWeapons', 'disableOffhandWeapons', 'enableWeapons', 'enableOffhandWeapons', 'iPrintLnBold']) {
|
||||
self[name] = (...args) => actions.push([name, ...args]);
|
||||
}
|
||||
function element(args, alpha = 1) {
|
||||
const result = { args, alpha, writes: [],
|
||||
SetText(value) { this.text = value; this.writes.push(['text', 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]); },
|
||||
};
|
||||
elems.push(result);
|
||||
return result;
|
||||
}
|
||||
function register(player, elem) {
|
||||
(player.tws_core_hud_elems ??= []).push(elem);
|
||||
common.set_core_alpha(player, elem, elem.alpha);
|
||||
return elem;
|
||||
}
|
||||
const common = {
|
||||
make_core_overlay_text: (...args) => register(args[0], element(args)),
|
||||
make_core_overlay_shader: (...args) => register(args[0], element(args, args[6])),
|
||||
make_overlay_width: width => Math.trunc(Math.max(1, width)),
|
||||
set_core_alpha(player, elem, alpha) {
|
||||
if (!elem) return;
|
||||
elem.tws_core_alpha = alpha;
|
||||
elem.alpha = player.tws_hud_hidden ? 0 : alpha;
|
||||
},
|
||||
toggle_core_hud() {
|
||||
self.tws_hud_hidden = !self.tws_hud_hidden;
|
||||
for (const elem of self.tws_core_hud_elems ?? []) common.set_core_alpha(self, elem, elem.tws_core_alpha);
|
||||
},
|
||||
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,
|
||||
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,
|
||||
hud_overlay_count_label_x: 306, hud_overlay_count_label_y: 446,
|
||||
hud_overlay_bar_x: 242, hud_overlay_bar_y: 466,
|
||||
hud_overlay_bar_width: 156, hud_overlay_bar_height: 4,
|
||||
hud_overlay_health_y: 460, hud_overlay_health_x: 414, hud_overlay_slash_x: 418,
|
||||
hud_overlay_maxhealth_x: 445, hud_overlay_hp_x: 449,
|
||||
hud_overlay_afk_x: 320, hud_overlay_afk_y: 421,
|
||||
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,
|
||||
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']);
|
||||
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,
|
||||
setTime(value) { now = value; },
|
||||
spawnAfk() { const watcher = context.afk_spawn(); watcher?.next(); return watcher; },
|
||||
};
|
||||
}
|
||||
for (const game of ['t5', 't6']) {
|
||||
const source = readFileSync(new URL(`${game}/scripts/tws_${game}_hud.gsc`, root), 'utf8');
|
||||
const afk = readFileSync(new URL(`${game}/scripts/tws_${game}_afk.gsc`, root), 'utf8');
|
||||
check(`${game}: shared lifecycle, frozen clocks and engine-specific timer seeds`, () => {
|
||||
const make = hidden => engine(game, source, afk, hidden);
|
||||
check(`${game}: native .hud chat and shared lifecycle retain engine-specific clock seeds`, () => {
|
||||
assert(source.includes('register_help_command(".hud", "Toggle core HUD")'));
|
||||
assert(source.includes('register_connect_handler(::hud_connect)'));
|
||||
assert(source.includes('register_spawn_handler(::hud_start)'));
|
||||
assert(!source.includes('waittill("connected"'));
|
||||
const chat = body(source, 'hud_chat_listener')[1];
|
||||
assert(chat.includes('level waittill("say", message, player)'));
|
||||
assert(chat.includes('toLower(message) == ".hud"'));
|
||||
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'])
|
||||
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)'));
|
||||
else assert(bind.includes('(getTime() - level.match_start_time) / 1000.0'));
|
||||
assert(afk.includes('make_panel_text'));
|
||||
assert(afk.includes('level.hud_row_afk'));
|
||||
assert(afk.includes('make_core_overlay_text'));
|
||||
assert(afk.includes('level.hud_overlay_afk_y'));
|
||||
assert(!/\.alpha\s*=/.test(afk), 'AFK visibility must use the common mask');
|
||||
});
|
||||
const queued = [], elems = [];
|
||||
const common = {
|
||||
make_panel_text: (...args) => element(args),
|
||||
make_panel_shader: (...args) => element(args),
|
||||
};
|
||||
function element(args) {
|
||||
const result = { args, alpha: 1, writes: [], SetText(value) { this.text = value; this.writes.push(['text', value]); },
|
||||
SetValue(value) { this.value = value; this.writes.push(['value', value]); },
|
||||
SetShader(...args) { this.shader = args; }, SetTimerUp(value) { this.timer = value; } };
|
||||
elems.push(result); return result;
|
||||
}
|
||||
const level = { round_number: 1, hud_panel_width: 220, hud_core_height: 100, hud_pad: 10,
|
||||
hud_row_header: 10, hud_row_timers: 34, hud_row_stats: 59, hud_row_bar: 80,
|
||||
hud_bar_width: 200, hud_bar_height: 4, hud_rate: .1,
|
||||
hud_font_small: 1.1, hud_font_mid: 1.25,
|
||||
hud_c_high: 'green', hud_c_mid: 'yellow', hud_c_low: 'red', notify() {} };
|
||||
const self = {};
|
||||
const context = vm.createContext({ self, level, common, queued, getTime: () => 123456,
|
||||
isDefined: value => value !== undefined && value !== null, is_true: value => !!value,
|
||||
int: Math.trunc, max: Math.max });
|
||||
for (const name of ['hud_connect', 'hud_text', 'hud_start', 'hud_timer_row', 'hud_health_display']) {
|
||||
const [args, original] = body(source, name);
|
||||
let code = original.replace(/\/\/[^\n]*/g, '');
|
||||
const locals = [...new Set([...code.matchAll(/(?<![\w.])(\w+)\s*=(?!=)/g)].map(match => match[1]))];
|
||||
code = code.replace(/(?:self|level) endon\([^;]+;/g, '')
|
||||
.replace(/scripts\\(?:sp|zm)\\tws_t[56]_common::/g, 'common.')
|
||||
.replace(/self thread ([\w.]+)\(([^;]*)\);/g, (_, name, args) => `queued.push(['${name}', ${args}]);`)
|
||||
.replace(/self (hud_\w+)\(/g, '$1(')
|
||||
.replace(/(\w+) (SetText|SetValue|SetTimerUp|SetShader)\(([^;]*)\);/g, '$1.$2($3);')
|
||||
.replace(/level notify\(([^;]*)\);/g, 'level.notify($1);')
|
||||
.replace(/\.size\b/g, '.length')
|
||||
.replace(/\((\d+\.\d+), (\d+\.\d+), (\d+\.\d+)\)/g, '[$1, $2, $3]')
|
||||
.replace(/wait \(([^;]*)\);/g, 'yield $1;');
|
||||
vm.runInContext(`function${name === 'hud_health_display' ? '*' : ''} ${name}(${args}) { var ${locals.join(', ') || 'unused'}; ${code} }`, context);
|
||||
}
|
||||
check(`${game}: connection constructs one fixed panel without seeding clocks`, () => {
|
||||
context.hud_connect();
|
||||
assert.equal(level.match_start_time, undefined); assert.equal(level.round_start_time, undefined);
|
||||
assert.equal(self.tws_hud_match.text, '--:--'); assert.equal(self.tws_hud_health.text, '--');
|
||||
const before = elems.length; context.hud_connect(); assert.equal(elems.length, before);
|
||||
assert.equal(queued.length, 1); assert.equal(queued[0][0], 'common.destroy_on_disconnect');
|
||||
assert.equal(queued[0][1].length, elems.length);
|
||||
check(`${game}: connection places 11 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.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) => {
|
||||
const elem = e.elems.find(item => item.text === text);
|
||||
assert(elem, text); assert.deepEqual(elem.args.slice(1), [x, y, align, scale, colour]);
|
||||
};
|
||||
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('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}`];
|
||||
assert.deepEqual(elem.args.slice(1), [x, 460, align, 1, 'white']);
|
||||
assert.equal(elem.alpha, 0); assert.equal(elem.tws_core_alpha, 0);
|
||||
}
|
||||
assert.equal(e.self.tws_hud_hp.alpha, 0);
|
||||
const shaders = e.elems.filter(elem => elem.args.length === 8);
|
||||
assert.equal(shaders.length, 2, 'only the health track and fill allocate shaders');
|
||||
for (const elem of shaders) assert.deepEqual(elem.args.slice(1, 5), [242, 466, 156, 4]);
|
||||
assert.equal(shaders[0].alpha, .8); assert.equal(shaders[1].alpha, 0);
|
||||
assert(!e.elems.some(elem => elem.text === 'MATCH' || elem.text === 'ROUND'));
|
||||
});
|
||||
check(`${game}: repeated first-spawn binding reuses panel and launches workers once`, () => {
|
||||
const before = elems.length; context.hud_start(); context.hud_start();
|
||||
assert.equal(elems.length, before);
|
||||
assert.deepEqual(queued.slice(1).map(item => item[0]), ['hud_timer_row', 'hud_zombie_counter', 'hud_health_display']);
|
||||
context.hud_timer_row(); assert.equal(level.match_start_time, 123456); assert.equal(level.round_start_time, 123456);
|
||||
check(`${game}: toggling before first spawn preserves hidden state and binds workers once`, () => {
|
||||
const e = make(); e.context.hud_toggle();
|
||||
assert.equal(e.self.tws_hud_hidden, true);
|
||||
assert(e.elems.every(elem => elem.alpha === 0));
|
||||
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.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);
|
||||
});
|
||||
check(`${game}: HP stays visible at full health and falls back without mutating maxhealth`, () => {
|
||||
self.health = 100; const run = context.hud_health_display(); run.next();
|
||||
assert.equal(self.maxhealth, undefined); assert.equal(self.tws_hud_health.value, 100);
|
||||
assert.equal(self.tws_hud_maxhealth.value, 100); assert.equal(self.tws_hud_health.alpha, 1);
|
||||
assert.equal(self.tws_hud_bar.alpha, 1); assert.equal(self.tws_hud_bar.shader[1], 200);
|
||||
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 numbers = [self.tws_hud_health, self.tws_hud_slash, self.tws_hud_maxhealth, self.tws_hud_hp];
|
||||
const run = e.context.hud_health_display(); run.next();
|
||||
assert(numbers.every(elem => elem.alpha === 0), 'undefined health does not flash a damage readout');
|
||||
assert.equal(self.health, undefined); assert.equal(self.maxhealth, undefined);
|
||||
self.health = 100; run.next();
|
||||
assert.equal(self.tws_hud_health.value, 100); assert.equal(self.tws_hud_maxhealth.value, 100);
|
||||
assert(numbers.every(elem => elem.alpha === 0));
|
||||
assert.equal(self.tws_hud_bar.alpha, 1); assert.equal(self.tws_hud_bar.shader[1], 156);
|
||||
const writes = self.tws_hud_health.writes.length; run.next(); assert.equal(self.tws_hud_health.writes.length, writes);
|
||||
self.health = 35; run.next();
|
||||
assert(numbers.every(elem => elem.alpha === 1 && elem.color === 'yellow'));
|
||||
assert.equal(self.tws_hud_health.value, 35); assert.equal(self.tws_hud_bar.shader[1], 54);
|
||||
self.maxhealth = 250; self.health = 50; run.next();
|
||||
assert.equal(self.tws_hud_maxhealth.value, 250); assert.equal(self.tws_hud_bar.shader[1], 40);
|
||||
assert.equal(self.tws_hud_maxhealth.value, 250); assert.equal(self.tws_hud_bar.shader[1], 31);
|
||||
assert(numbers.every(elem => elem.alpha === 1 && elem.color === 'red'));
|
||||
assert.equal(self.tws_hud_bar.color, 'red');
|
||||
self.health = 0; run.next(); assert.equal(self.tws_hud_bar.alpha, 0); assert.equal(self.tws_hud_health.value, 0);
|
||||
assert(numbers.every(elem => elem.alpha === 1));
|
||||
self.health = 250; run.next(); assert.equal(self.tws_hud_bar.alpha, 1); assert.equal(self.tws_hud_bar.color, 'green');
|
||||
assert(numbers.every(elem => elem.alpha === 0 && elem.tws_core_alpha === 0));
|
||||
self.health = 500; run.next(); assert.equal(self.tws_hud_bar.shader[1], 156);
|
||||
assert(numbers.every(elem => elem.alpha === 0), 'overheal must not show the damage readout');
|
||||
self.maxhealth = 0; self.health = 100; run.next();
|
||||
assert.equal(self.maxhealth, 0, 'invalid maximum is display-only fallback');
|
||||
assert.equal(self.tws_hud_maxhealth.value, 100); assert(numbers.every(elem => elem.alpha === 0));
|
||||
assert.equal(self.tws_hud_health.writes.filter(item => item[0] === 'text').length, 1);
|
||||
});
|
||||
check(`${game}: hidden health updates remain private and restore the current bar state`, () => {
|
||||
const e = make(true), other = make();
|
||||
e.context.hud_connect(); other.context.hud_connect();
|
||||
e.self.health = 100; const run = e.context.hud_health_display(); run.next();
|
||||
assert(e.elems.every(elem => elem.alpha === 0));
|
||||
assert.equal(e.self.tws_hud_bar.tws_core_alpha, 1);
|
||||
e.self.health = 0; run.next(); e.context.hud_toggle();
|
||||
assert.equal(e.self.tws_hud_health.value, 0); assert.equal(e.self.tws_hud_bar.alpha, 0);
|
||||
assert.equal(e.elems[0].alpha, 1); assert.equal(e.elems.at(-2).alpha, .8);
|
||||
e.context.hud_toggle(); e.self.health = 50; run.next();
|
||||
assert.equal(e.self.tws_hud_bar.alpha, 0); assert.equal(e.self.tws_hud_bar.shader[1], 78);
|
||||
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);
|
||||
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);
|
||||
});
|
||||
check(`${game}: toggling frozen clocks does not restart or reseed them`, () => {
|
||||
const e = make(); e.context.hud_start(); e.context.hud_timer_row();
|
||||
const match = e.self.tws_hud_match, round = e.self.tws_hud_round;
|
||||
const freeze = e.context.freeze_timers_on_game_over(match, round);
|
||||
assert.equal(freeze.next().value.event, 'hud_game_over');
|
||||
e.context.hud_toggle(); e.setTime(248456); e.level.tws_ended = true; freeze.next();
|
||||
assert.equal(match.text, '2:05'); assert.equal(round.text, '2:05');
|
||||
const before = JSON.stringify([match.writes, round.writes, e.queued.map(item => item[0])]);
|
||||
e.setTime(999999); e.context.hud_toggle(); e.context.hud_start();
|
||||
assert.equal(match.alpha, 1); assert.equal(round.alpha, 1);
|
||||
assert.equal(JSON.stringify([match.writes, round.writes, e.queued.map(item => item[0])]), before);
|
||||
assert.equal(e.level.match_start_time, 123456); assert.equal(e.level.round_start_time, 123456);
|
||||
});
|
||||
check(`${game}: inactive AFK stays hidden and protection survives HUD toggles`, () => {
|
||||
const e = make(); e.context.hud_connect(); e.spawnAfk();
|
||||
const indicator = e.self.hud_afk_elem;
|
||||
assert.equal(indicator.alpha, 0); assert.equal(indicator.tws_core_alpha, 0);
|
||||
assert.deepEqual(indicator.args.slice(1), [320, 421, 'center', 1, 'yellow']);
|
||||
e.context.hud_toggle(); e.context.hud_toggle(); assert.equal(indicator.alpha, 0);
|
||||
e.context.hud_toggle(); e.context.afk_enter();
|
||||
assert.equal(e.self.hud_afk_on, true); assert.equal(e.self.ignoreme, true);
|
||||
assert.equal(indicator.alpha, 0); assert.equal(indicator.tws_core_alpha, 1);
|
||||
assert(e.actions.some(([name]) => name === 'enableInvulnerability'));
|
||||
assert(e.actions.some(([name, enabled]) => name === 'freezeControls' && enabled));
|
||||
const actions = e.actions.length;
|
||||
e.context.hud_toggle(); assert.equal(indicator.alpha, 1); assert.equal(e.actions.length, actions);
|
||||
e.context.hud_toggle(); const exit = e.context.afk_exit();
|
||||
assert.equal(exit.next().value, 2);
|
||||
assert.equal(indicator.alpha, 0); assert.equal(indicator.tws_core_alpha, 0);
|
||||
assert.equal(e.self.ignoreme, true, 'the existing protection grace tail remains active');
|
||||
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);
|
||||
});
|
||||
if (game === 't6') check('t6: AFK release while hidden respects stock protection ownership', () => {
|
||||
const e = make(); e.context.hud_connect(); const watcher = e.spawnAfk();
|
||||
e.context.afk_enter(); e.context.hud_toggle(); e.self.active = false;
|
||||
watcher.next();
|
||||
assert.equal(e.self.hud_afk_on, false); assert.equal(e.self.tws_afk_protected, false);
|
||||
assert.equal(e.self.hud_afk_elem.tws_core_alpha, 0);
|
||||
assert.equal(e.self.ignoreme, true);
|
||||
assert(!e.actions.some(([name]) => name === 'disableInvulnerability'));
|
||||
e.context.hud_toggle(); assert.equal(e.self.hud_afk_elem.alpha, 0);
|
||||
e.self.active = true; watcher.next();
|
||||
assert.equal(e.self.tws_afk_controls_locked, false);
|
||||
assert(e.actions.some(([name]) => name === 'enableWeapons'));
|
||||
assert(e.actions.some(([name]) => name === 'enableOffhandWeapons'));
|
||||
});
|
||||
}
|
||||
console.log(`${checks} core HUD fixture checks passed (transpiled subset, not a GSC VM).`);
|
||||
console.log(`${checks} core HUD/AFK fixture checks passed (transpiled subset, not a GSC VM).`);
|
||||
|
||||
+350
-68
@@ -1,5 +1,6 @@
|
||||
// Execute the actual common GSC service functions with HUD/event shims.
|
||||
// These fixtures do not emulate the GSC VM, fonts, safe areas or HUD pool limits.
|
||||
// These fixtures do not emulate the GSC VM, fonts or HUD pool limits. Geometry
|
||||
// checks model the 640x480 fullscreen placement transform, not a live renderer.
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync, readdirSync } from 'node:fs';
|
||||
import vm from 'node:vm';
|
||||
@@ -11,7 +12,7 @@ function check(name, run) {
|
||||
try { run(); checks++; }
|
||||
catch (error) { throw new Error(name, { cause: error }); }
|
||||
}
|
||||
const coroutineNames = new Set(['service_loop', 'service_disconnect', 'service_end_game', 'service_game_over_via']);
|
||||
const coroutineNames = new Set(['service_loop', 'service_disconnect', 'service_end_game', 'service_game_over_via', 'destroy_on_disconnect']);
|
||||
function translate(source) {
|
||||
const literals = [];
|
||||
const protectedSource = source.replace(/"(?:\\.|[^"\\])*"|\/\/[^\n]*|\/\*[\s\S]*?\*\//g, token => {
|
||||
@@ -23,7 +24,7 @@ function translate(source) {
|
||||
const result = [];
|
||||
for (let match; (match = headers.exec(protectedSource));) {
|
||||
const name = match[1];
|
||||
if (!/^(service_|help_|make_)/.test(name) && !['common_config', 'ensure_config', 'register_help_command', 'show_help', 'show_record'].includes(name)) continue;
|
||||
if (!/^(service_|help_|make_)/.test(name) && !['common_config', 'ensure_config', 'register_help_command', 'show_help', 'show_record', 'register_core_elem', 'set_core_alpha', 'toggle_core_hud', 'hud_connect', 'hud_text', 'afk_spawn', 'destroy_on_disconnect'].includes(name)) continue;
|
||||
if (name === 'help_chat_listener') continue;
|
||||
let end = headers.lastIndex, depth = 1;
|
||||
while (depth) {
|
||||
@@ -35,6 +36,7 @@ function translate(source) {
|
||||
let body = protectedSource.slice(headers.lastIndex, end - 1);
|
||||
const locals = new Set([...body.matchAll(/(?<![\w.])(\w+)\s*(?:[+*/%-]?=(?!=)|\+\+|--)/g)].map(x => x[1]));
|
||||
body = body
|
||||
.replace(/scripts\\(?:sp|zm)\\tws_t[56]_common::/g, '')
|
||||
.replace(/\(([-\d.]+),\s*([-\d.]+),\s*([-\d.]+)\)/g, '[$1, $2, $3]')
|
||||
.replace(/\.size\b/g, '.length')
|
||||
.replace(/(self|level) endon\(([^;]+)\);/g, 'endon($1, $2);')
|
||||
@@ -57,10 +59,11 @@ const commands = [
|
||||
['.d <amount|all>', 'Deposit points'], ['.w <amount|all>', 'Withdraw points'],
|
||||
['.x', 'Extra command'],
|
||||
];
|
||||
function engine(game, count = 8) {
|
||||
function engine(game, count = 8, withHud = false) {
|
||||
const source = readFileSync(new URL(`${game}/scripts/tws_${game}_common.gsc`, root), 'utf8');
|
||||
let now = 0, running;
|
||||
let now = 0, running, peakLive = 0;
|
||||
const self = { sessionstate: 'spectator' }, level = {}, tasks = [], elements = [];
|
||||
const players = new Set([self]);
|
||||
function resume(task) {
|
||||
const prior = running;
|
||||
running = task;
|
||||
@@ -91,76 +94,190 @@ function engine(game, count = 8) {
|
||||
now = until;
|
||||
}
|
||||
function newElement(owner) {
|
||||
assert.equal(owner, self, 'every HUD element must be client-owned');
|
||||
assert(players.has(owner), 'every HUD element must be owned by a known client');
|
||||
const element = {
|
||||
owner,
|
||||
alpha: 1, textCalls: 0, valueCalls: 0, destroyCalls: 0,
|
||||
SetText(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); Object.assign(this, { shader, width, height }); },
|
||||
SetShader(shader, width, height) {
|
||||
assert(!this.destroyCalls);
|
||||
assert(Number.isInteger(width) && Number.isInteger(height), 'SetShader requires integer dimensions');
|
||||
Object.assign(this, { shader, width, height });
|
||||
},
|
||||
Destroy() { this.destroyCalls++; assert.equal(this.destroyCalls, 1, 'double destruction'); this.alpha = 0; },
|
||||
};
|
||||
elements.push(element);
|
||||
peakLive = Math.max(peakLive, elements.filter(elem => !elem.destroyCalls).length);
|
||||
return element;
|
||||
}
|
||||
const c = vm.createContext({
|
||||
self, level, NewClientHudElem: newElement, getTime: () => now,
|
||||
spawnStruct: () => ({}), isDefined: v => v !== undefined && v !== null,
|
||||
spawnStruct: () => ({}), isDefined: v => v !== undefined && v !== null && !v.destroyCalls,
|
||||
max: Math.max, int: Math.trunc,
|
||||
getSubStr: (text, begin, end) => { assert(end <= text.length); return text.slice(begin, end); },
|
||||
endon: (entity, event) => running.ends.push([entity, event]), startThread, emit,
|
||||
endon: (entity, event) => running?.ends.push([entity, event]), startThread, emit,
|
||||
});
|
||||
vm.runInContext(translate(source), c);
|
||||
c.common_config();
|
||||
for (const [command, description] of commands.slice(0, count).reverse()) c.register_help_command(command, description);
|
||||
if (withHud) c.register_help_command('.hud', 'Toggle core HUD');
|
||||
for (const event of ['end_game', 'game_ended', 'intermission']) startThread(c.service_game_over_via, [event]);
|
||||
return { c, self, level, elements, tasks, advance, source,
|
||||
return { game, c, self, level, elements, tasks, advance, source,
|
||||
createCore() {
|
||||
assert.equal(game, 't5', 'combined fixture currently covers T5 construction');
|
||||
for (const module of ['hud', 'afk'])
|
||||
vm.runInContext(translate(readFileSync(new URL(`${game}/scripts/tws_${game}_${module}.gsc`, root), 'utf8')), c);
|
||||
c.hud_connect(); c.afk_spawn();
|
||||
},
|
||||
addPlayer() { const player = { sessionstate: 'spectator' }; players.add(player); return player; },
|
||||
help: () => c.show_help(), record: snapshot => c.show_record(snapshot),
|
||||
event: name => emit(self, name), end: name => emit(level, name),
|
||||
get state() { return self.tws_service; },
|
||||
get live() { return elements.filter(e => !e.destroyCalls); },
|
||||
get peakLive() { return peakLive; },
|
||||
get visible() { return elements.filter(e => e.alpha > 0 && !e.destroyCalls); },
|
||||
get textCalls() { return elements.reduce((n, e) => n + e.textCalls, 0); },
|
||||
get helpCommands() {
|
||||
return Array.from(self.tws_service.command_rows).filter(row => row.alpha > 0).map(row => row.text);
|
||||
},
|
||||
get helpDescriptions() {
|
||||
return Array.from(self.tws_service.description_rows).filter(row => row.alpha > 0).map(row => row.text);
|
||||
},
|
||||
};
|
||||
}
|
||||
const snapshot = (lines = ['Alex, Sam, Jamie']) => ({ round: 82, saved_at: 0, lines });
|
||||
function assertCounter(e, page, pages) {
|
||||
const counter = e.state.page_counter;
|
||||
if (pages === 1) assert.equal(counter, undefined, 'single-page services do not allocate a counter');
|
||||
else {
|
||||
assert.equal(counter.alpha, 1);
|
||||
assert.equal(counter.text, `${page} / ${pages}`);
|
||||
}
|
||||
}
|
||||
function assertTextOnly(e) {
|
||||
assert(e.state.elems.every(elem => elem.shader === undefined && elem.archived === false),
|
||||
'service elements are private, non-archived text with no background shaders');
|
||||
}
|
||||
for (const game of ['t5', 't6']) {
|
||||
for (const count of [1, 4, 5, 8, 9]) check(`${game} ${count} registry entries paginate, order and dismiss`, () => {
|
||||
check(`${game} installed HUD joins all nine commands in one view`, () => {
|
||||
const e = engine(game, 8, true);
|
||||
e.help();
|
||||
assert.deepEqual(e.helpCommands,
|
||||
['.help', '.hud', '.afk', '.back', '.join', '.leaders', '.b', '.d <amount|all>', '.w <amount|all>']);
|
||||
assert.deepEqual(e.helpDescriptions,
|
||||
['Show commands', 'Toggle core HUD', 'Protect & freeze', 'Return to play', 'Join as newcomer',
|
||||
'View records', 'Bank balance', 'Deposit', 'Withdraw']);
|
||||
assert.equal(e.state.help.pages, 1);
|
||||
assertCounter(e, 1, 1);
|
||||
assert.equal(e.live.length, 19);
|
||||
assertTextOnly(e);
|
||||
e.advance(12000);
|
||||
assert.equal(e.live.length, 0);
|
||||
const withoutHud = engine(game);
|
||||
withoutHud.help();
|
||||
assert(!withoutHud.state.commands.some(entry => entry.command === '.hud'));
|
||||
});
|
||||
check(`${game} core toggle preserves desired opacity and leaves services and other players alone`, () => {
|
||||
const e = engine(game);
|
||||
const track = e.c.make_core_overlay_shader(e.self, 242, 466, 156, 4, [0, 0, 0], 0.8, 10);
|
||||
const bar = e.c.make_core_overlay_shader(e.self, 242, 466, 156, 4, [1, 1, 1], 0, 11);
|
||||
const label = e.c.make_core_overlay_text(e.self, 6, 8, 'left', 1.4, [1, 1, 1]);
|
||||
label.SetText('48:16');
|
||||
const other = e.addPlayer();
|
||||
const otherLabel = e.c.make_core_overlay_text(other, 6, 8, 'left', 1.4, [1, 1, 1]);
|
||||
e.record(snapshot()); e.help();
|
||||
const serviceAlpha = Array.from(e.state.elems, elem => elem.alpha);
|
||||
const help = e.state.help, record = e.state.record;
|
||||
e.c.toggle_core_hud();
|
||||
assert.equal(e.self.tws_hud_hidden, true);
|
||||
assert([track, bar, label].every(elem => elem.alpha === 0));
|
||||
assert.equal(otherLabel.alpha, 1);
|
||||
assert.equal(other.tws_hud_hidden, undefined);
|
||||
e.c.set_core_alpha(e.self, bar, 1);
|
||||
const afk = e.c.make_core_overlay_text(e.self, 320, 421, 'center', 1.0, [1, 1, 1]);
|
||||
assert.equal(afk.alpha, 0, 'late core elements inherit the hidden state');
|
||||
e.c.set_core_alpha(e.self, afk, 0);
|
||||
const count = e.elements.length;
|
||||
e.c.toggle_core_hud();
|
||||
assert.deepEqual([track.alpha, bar.alpha, label.alpha, afk.alpha], [0.8, 1, 1, 0]);
|
||||
assert.equal(label.text, '48:16', 'toggle does not rewrite displayed data');
|
||||
assert.equal(e.elements.length, count);
|
||||
assert.deepEqual(Array.from(e.state.elems, elem => elem.alpha), serviceAlpha);
|
||||
assert.equal(e.state.help, help); assert.equal(e.state.record, record);
|
||||
e.c.toggle_core_hud();
|
||||
e.c.set_core_alpha(e.self, bar, 0);
|
||||
e.c.set_core_alpha(e.self, afk, 1);
|
||||
assert.equal(afk.alpha, 0);
|
||||
e.c.toggle_core_hud();
|
||||
assert.equal(bar.alpha, 0);
|
||||
assert.equal(afk.alpha, 1);
|
||||
label.Destroy();
|
||||
e.c.toggle_core_hud();
|
||||
assert(!e.self.tws_core_hud_elems.includes(label), 'toggle prunes destroyed elements');
|
||||
assert.equal(label.destroyCalls, 1, 'registry never owns element destruction');
|
||||
});
|
||||
check(`${game} hiding before construction survives late core creation and prunes dead registrations`, () => {
|
||||
const e = engine(game);
|
||||
e.c.toggle_core_hud();
|
||||
const old = e.c.make_core_overlay_text(e.self, 6, 8, 'left', 1.4, [1, 1, 1]);
|
||||
assert.equal(old.alpha, 0);
|
||||
old.Destroy();
|
||||
const fresh = e.c.make_core_overlay_text(e.self, 6, 8, 'left', 1.4, [1, 1, 1]);
|
||||
assert.equal(fresh.alpha, 0);
|
||||
assert.equal(e.self.tws_core_hud_elems.length, 1);
|
||||
e.c.toggle_core_hud();
|
||||
assert.equal(fresh.alpha, 1);
|
||||
assert.equal(e.level.match_start_time, undefined);
|
||||
assert.equal(e.level.round_start_time, undefined);
|
||||
const reconnected = e.addPlayer();
|
||||
const newLabel = e.c.make_core_overlay_text(reconnected, 6, 8, 'left', 1.4, [1, 1, 1]);
|
||||
assert.equal(newLabel.alpha, 1, 'a new player defaults to visible');
|
||||
});
|
||||
for (const count of [0, 1, 4, 8, 9]) check(`${game} ${count} registry entries order and dismiss`, () => {
|
||||
const e = engine(game, count);
|
||||
assert.equal(e.elements.length, 0, 'service is lazy and independent of core');
|
||||
e.help();
|
||||
const s = e.state, background = [s.background.x, s.background.y, s.background.width, s.background.height];
|
||||
const pages = Math.ceil(count / 4);
|
||||
assert.equal(s.help.pages, pages);
|
||||
const s = e.state;
|
||||
assert.equal(s.help.pages, 1);
|
||||
assert.deepEqual(Array.from(s.commands, entry => entry.command), commands.slice(0, count).map(entry => entry[0]));
|
||||
for (let page = 0; page < pages; page++) {
|
||||
assert.equal(s.help.page, page);
|
||||
assert.equal(s.page_number.value, page + 1);
|
||||
assert.equal(s.page_total.value, pages);
|
||||
assert.equal(s.command_column.text.split('\n').length, Math.min(4, count - page * 4));
|
||||
assert.equal(s.description_column.text.split('\n').length, Math.min(4, count - page * 4));
|
||||
assert.deepEqual([s.background.x, s.background.y, s.background.width, s.background.height], background);
|
||||
e.advance(12000);
|
||||
}
|
||||
assertCounter(e, 1, 1);
|
||||
assert.deepEqual(e.helpCommands, Array.from(s.commands, entry => e.c.help_command_label(entry.command)));
|
||||
assert.deepEqual(e.helpDescriptions, Array.from(s.commands, entry => e.c.help_description(entry)));
|
||||
assert.equal(e.live.length, count * 2 + 1);
|
||||
assertTextOnly(e);
|
||||
e.advance(12000);
|
||||
assert.equal(s.help, undefined);
|
||||
assert.equal(e.visible.length, 0);
|
||||
assert.equal(e.live.length, 0, 'idle must release even hidden elements');
|
||||
});
|
||||
check(`${game} repeated help reuses strings/elements and resets page one`, () => {
|
||||
const e = engine(game);
|
||||
check(`${game} overflow help pages reuse rows and repeated help resets expiry`, () => {
|
||||
const e = engine(game, 9, true);
|
||||
e.help();
|
||||
assert.equal(e.state.help.pages, 2);
|
||||
assert.equal(e.live.length, 20);
|
||||
assertCounter(e, 1, 2);
|
||||
const rows = [...e.state.command_rows, ...e.state.description_rows];
|
||||
const count = e.elements.length, texts = e.textCalls;
|
||||
e.help();
|
||||
assert.equal(e.textCalls, texts, 'repeating current page does not reassign text');
|
||||
e.advance(12000);
|
||||
assert.equal(e.state.help.page, 1);
|
||||
assert.deepEqual(e.helpCommands, ['.x']);
|
||||
assertCounter(e, 2, 2);
|
||||
assert.deepEqual(Array.from(e.state.command_rows, row => row.alpha), [1, 0, 0, 0, 0, 0, 0, 0, 0]);
|
||||
e.help();
|
||||
assert.equal(e.state.help.page, 0);
|
||||
e.advance(11900);
|
||||
e.help();
|
||||
e.advance(100);
|
||||
assert.equal(e.state.help.page, 0, 'old expiry cannot advance replacement');
|
||||
e.advance(23900);
|
||||
assert.deepEqual([...e.state.command_rows, ...e.state.description_rows], rows);
|
||||
e.advance(11900);
|
||||
assertCounter(e, 2, 2);
|
||||
e.advance(12000);
|
||||
assert.equal(e.state.help, undefined);
|
||||
assert.equal(e.elements.length, count);
|
||||
assert.equal(e.textCalls, texts + 6, 'only three page transitions update the two text columns');
|
||||
assert.equal(e.live.length, 0);
|
||||
});
|
||||
check(`${game} help pauses record time and resumes the same page`, () => {
|
||||
const e = engine(game);
|
||||
@@ -169,16 +286,18 @@ for (const game of ['t5', 't6']) {
|
||||
assert.equal(e.state.record.page, 1);
|
||||
const remaining = e.state.record.remaining;
|
||||
e.help();
|
||||
const texts = e.textCalls;
|
||||
e.advance(24000);
|
||||
e.advance(12000);
|
||||
assert.equal(e.state.help, undefined);
|
||||
assert.equal(e.state.record.page, 1);
|
||||
assert.equal(e.state.record.remaining, remaining);
|
||||
assert.equal(e.state.rows[0].text, 'Four');
|
||||
assert.equal(e.textCalls, texts + 2, 'only the help page transition changes text; record resumption does not');
|
||||
assert.equal(e.live.length, 5);
|
||||
const resumedTexts = e.textCalls;
|
||||
e.c.service_render(e.state);
|
||||
assert.equal(e.textCalls, resumedTexts, 'subsequent ticks must reuse the rendered view');
|
||||
e.advance(remaining);
|
||||
assert.equal(e.state.record, undefined);
|
||||
assert.equal(e.visible.length, 0);
|
||||
assert.equal(e.live.length, 0);
|
||||
});
|
||||
check(`${game} latest record replaces pending one without disturbing help`, () => {
|
||||
const e = engine(game);
|
||||
@@ -190,10 +309,11 @@ for (const game of ['t5', 't6']) {
|
||||
assert.equal(e.state.help, help);
|
||||
assert.equal(e.state.record.page, 0);
|
||||
assert.equal(e.state.record.remaining, 12000);
|
||||
assert.equal(e.state.rows.filter(row => row.alpha > 0).length, 0);
|
||||
e.advance(21000);
|
||||
assert.equal(e.state.rows.length, 0, 'pending records allocate no holder elements');
|
||||
e.advance(9000);
|
||||
assert.equal(e.state.rows[0].text, 'Newest');
|
||||
assert.equal(e.state.record.remaining, 12000);
|
||||
assertCounter(e, 1, 1);
|
||||
e.advance(11900);
|
||||
e.record(snapshot(['Newest']));
|
||||
const texts = e.textCalls;
|
||||
@@ -201,26 +321,40 @@ for (const game of ['t5', 't6']) {
|
||||
assert(e.state.record, 'previous expiry cannot hide newer request');
|
||||
assert.equal(e.textCalls, texts);
|
||||
});
|
||||
check(`${game} no record, unavailable and short final record page are distinct`, () => {
|
||||
check(`${game} unavailable, empty and paged records rebuild only when their allocation changes`, () => {
|
||||
const e = engine(game);
|
||||
e.record(undefined);
|
||||
assert.equal(e.state.unavailable.alpha, 1);
|
||||
assert.equal(e.state.empty.alpha, 0);
|
||||
assert.equal(e.state.record_title.alpha, 1);
|
||||
assert.equal(e.state.record_title.text, 'Map record unavailable');
|
||||
assert.equal(e.live.length, 4);
|
||||
assertCounter(e, 1, 1);
|
||||
const first = [...e.live];
|
||||
e.record({ round: 0, lines: [] });
|
||||
assert.equal(e.state.unavailable.alpha, 0);
|
||||
assert.equal(e.state.empty.alpha, 1);
|
||||
assert.equal(e.state.record_title.text, 'No recorded high round yet');
|
||||
assert.deepEqual(e.live, first);
|
||||
assertCounter(e, 1, 1);
|
||||
e.record(snapshot(['A', 'B', 'C', 'D']));
|
||||
assert.equal(e.state.empty.alpha, 0);
|
||||
assert.equal(e.state.round_number.value, 82);
|
||||
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.live.length, 5);
|
||||
const paged = [...e.live];
|
||||
assertCounter(e, 1, 2);
|
||||
e.advance(12000);
|
||||
assert.deepEqual(Array.from(e.state.rows, row => row.alpha), [1, 0, 0]);
|
||||
assert.equal(e.state.rows[0].text, 'D');
|
||||
assertCounter(e, 2, 2);
|
||||
assert.deepEqual(e.live, paged, 'page changes reuse the same allocation');
|
||||
e.record(snapshot(['Single page']));
|
||||
assert(paged.every(elem => elem.destroyCalls === 1));
|
||||
assertCounter(e, 1, 1);
|
||||
assert.equal(e.state.rows[0].text, 'Single page');
|
||||
assert.equal(e.live.length, 4);
|
||||
assert.equal(e.peakLive, 5, 'new allocations never overlap the released view');
|
||||
assertTextOnly(e);
|
||||
});
|
||||
for (const event of ['end_game', 'game_ended', 'intermission', 'disconnect']) check(`${game} ${event} cleans active and pending pages once`, () => {
|
||||
const e = engine(game);
|
||||
e.record(snapshot());
|
||||
e.help();
|
||||
e.advance(2000);
|
||||
e.record(snapshot()); e.help(); e.advance(2000);
|
||||
if (event === 'disconnect') e.event(event); else e.end(event);
|
||||
assert(e.state.closed);
|
||||
assert.equal(e.state.help, undefined);
|
||||
@@ -239,42 +373,131 @@ for (const game of ['t5', 't6']) {
|
||||
e.end('end_game'); e.help(); e.record(snapshot());
|
||||
assert.equal(e.elements.length, 0);
|
||||
});
|
||||
check(`${game} no per-tick text updates, stable core origin and private elements`, () => {
|
||||
check(`${game} rendered records cache text and never allocate on timer ticks`, () => {
|
||||
const e = engine(game);
|
||||
e.record(snapshot());
|
||||
const calls = e.textCalls, count = e.elements.length;
|
||||
e.advance(11000);
|
||||
assert.equal(e.textCalls, calls);
|
||||
assert.equal(e.elements.length, count);
|
||||
assert.equal(e.state.background.horzAlign, 'right');
|
||||
assert.equal(e.state.background.vertAlign, game === 't5' ? 'top' : 'fullscreen');
|
||||
assert.equal(e.state.background.y, e.level.hud_panel_top + e.level.hud_service_y);
|
||||
assert(e.state.background.y > e.level.hud_panel_top + e.level.hud_core_height);
|
||||
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 <n|all>');
|
||||
assert.equal(e.elements.length, 19, 'service allocation is bounded even when hidden');
|
||||
assert(e.elements.every(elem => elem.archived === false));
|
||||
assert.equal(e.c.help_command_label(e.state.commands[6].command), '.d <amount|all>');
|
||||
assert.equal(e.elements.length, 4, 'record allocation includes all hidden holder rows');
|
||||
assertTextOnly(e);
|
||||
});
|
||||
check(`${game} nominal font boxes fit the fixed panels without row collisions`, () => {
|
||||
const e = engine(game);
|
||||
check(`${game} service coordinates clear the watermark and stock lower-right HUD`, () => {
|
||||
const e = engine(game, 8, true);
|
||||
e.record(snapshot(['One', 'Two', 'Three']));
|
||||
// Stock T6 _hud uses level.fontheight=12. This catches the original
|
||||
// 11-unit holder pitch / overflowing AFK footer, not actual glyph fit.
|
||||
const height = elem => Math.trunc(12 * elem.fontScale);
|
||||
const s = e.state;
|
||||
assert(s.holders_label.y + height(s.holders_label) <= s.rows[0].y);
|
||||
assert.deepEqual([s.record_title.x, s.record_title.y, s.record_title.alignX], [633, 36, 'right']);
|
||||
assert.deepEqual(Array.from(s.rows, row => [row.x, row.y, row.alignX]),
|
||||
[[633, 60, 'right'], [633, 78, 'right'], [633, 96, 'right']]);
|
||||
const records = [...s.elems];
|
||||
e.help();
|
||||
assert.deepEqual([s.help_title.x, s.help_title.y], [432, 54]);
|
||||
for (const [i, row] of s.command_rows.entries()) {
|
||||
assert.equal(row.x, 432); assert.equal(row.y, 80 + i * 24);
|
||||
assert.equal(s.description_rows[i].x, 538);
|
||||
assert.equal(s.description_rows[i].y, row.y);
|
||||
}
|
||||
for (const [width, height] of [[1920, 1080], [1280, 720], [1600, 1200], [3440, 1440]]) {
|
||||
const project = elem => ({ x: elem.x * width / 640, y: elem.y * height / 480 });
|
||||
for (const elem of [...records, ...s.elems]) {
|
||||
assert.equal(elem.horzAlign, 'fullscreen');
|
||||
assert.equal(elem.vertAlign, 'fullscreen');
|
||||
const point = project(elem);
|
||||
assert(point.x > width / 2 && point.x < width);
|
||||
assert(point.y < height * 0.6, 'leave scores and ammo clear');
|
||||
}
|
||||
assert.equal(width - project(records[0]).x, 7 * width / 640);
|
||||
if (width === 1920) assert(project(records[0]).y > 27, 'clear the reference Plutonium tag');
|
||||
}
|
||||
});
|
||||
check(`${game} new and legacy shader helpers retain integer dimensions`, () => {
|
||||
const e = engine(game);
|
||||
// Native GSC max returns VAR_FLOAT even for integral values. Boxed
|
||||
// numbers retain that distinction here; the explicit int() unboxes it.
|
||||
e.c.max = (...args) => new Number(Math.max(...args));
|
||||
const legacy = e.c.make_panel_shader(e.self, 0, 0, 220, 100,
|
||||
e.level.hud_c_panel, e.level.hud_panel_alpha, 0);
|
||||
assert.equal(legacy.width, 165);
|
||||
assert.equal(legacy.alpha, 0.72);
|
||||
assert.deepEqual(Array.from(legacy.color), [0.07, 0.10, 0.08]);
|
||||
const overlay = e.c.make_overlay_shader(e.self, 242, 466, 156, 4, [1, 1, 1], 0.8, 10);
|
||||
assert.deepEqual([overlay.x, overlay.y, overlay.width, overlay.height], [242, 466, 156, 4]);
|
||||
for (const [design, panelWidth, overlayWidth] of [[200, 150, 200], [40, 30, 40], [65.9, 49, 65], [0, 1, 1], [0.1, 1, 1]]) {
|
||||
assert.equal(e.c.make_panel_width(design), panelWidth);
|
||||
assert.equal(e.c.make_overlay_width(design), overlayWidth);
|
||||
legacy.SetShader('white', e.c.make_panel_width(design), 4);
|
||||
overlay.SetShader('white', e.c.make_overlay_width(design), 4);
|
||||
}
|
||||
assert.throws(() => overlay.SetShader('white', e.c.max(1, 156), 4), /requires integer dimensions/);
|
||||
const oldCore = e.c.make_core_text(e.self, 10, 10, 'left', 1.1, [1, 1, 1]);
|
||||
const oldShader = e.c.make_core_shader(e.self, 10, 80, 200, 4, [1, 1, 1], 0.5, 10);
|
||||
e.c.toggle_core_hud();
|
||||
assert.equal(oldCore.alpha, 0); assert.equal(oldShader.alpha, 0);
|
||||
e.c.toggle_core_hud();
|
||||
assert.equal(oldCore.alpha, 1); assert.equal(oldShader.alpha, 0.5);
|
||||
});
|
||||
check(`${game} explicit service rows fit nominal font bounds without newline dependence`, () => {
|
||||
const e = engine(game, 8, true);
|
||||
e.record(snapshot(['One', 'Two', 'Three']));
|
||||
const height = elem => Math.trunc(12 * elem.fontScale);
|
||||
let s = e.state;
|
||||
assert(s.record_title.y + height(s.record_title) <= s.rows[0].y);
|
||||
for (let i = 1; i < s.rows.length; i++)
|
||||
assert(s.rows[i - 1].y + height(s.rows[i - 1]) <= s.rows[i].y);
|
||||
assert(s.rows.at(-1).y + height(s.rows.at(-1)) <= s.record_footer.y);
|
||||
assert(s.record_footer.y + height(s.record_footer) < s.background.y + s.background.height);
|
||||
assert(e.level.hud_row_bar + e.level.hud_bar_height < e.level.hud_row_afk);
|
||||
assert(e.level.hud_row_afk + Math.trunc(12 * e.level.hud_font_afk) < e.level.hud_core_height);
|
||||
e.help();
|
||||
const helpHeight = 4 * height(s.command_column);
|
||||
assert(s.command_column.y + helpHeight <= s.command_back.y + s.command_back.height);
|
||||
assert(s.command_column.y + helpHeight <= s.help_footer.y);
|
||||
const rows = [...s.command_rows, ...s.description_rows];
|
||||
assert.equal(new Set(rows).size, 18);
|
||||
assert.equal(new Set(s.command_rows.map(row => row.y)).size, 9);
|
||||
assert(rows.every(row => row.alpha === 1 && !/[\r\n]/.test(row.text)));
|
||||
for (let i = 0; i < s.command_rows.length; i++) {
|
||||
const command = s.command_rows[i], description = s.description_rows[i];
|
||||
assert.equal(command.y, description.y);
|
||||
if (i > 0) assert(s.command_rows[i - 1].y + height(s.command_rows[i - 1]) < command.y);
|
||||
assert(command.y + height(command) < 320);
|
||||
}
|
||||
assert(e.elements.filter(elem => elem.fontScale !== undefined).every(elem => elem.fontScale >= 1.0));
|
||||
assert.equal(e.c.make_text_elem(e.self, 0, 0, 'left', 0.9, [1, 1, 1]).fontScale, 1.0);
|
||||
assert.equal(e.c.make_overlay_text(e.self, 0, 0, 'left', 0.9, [1, 1, 1]).fontScale, 1.0);
|
||||
assert.equal(e.c.make_overlay_text(e.self, 0, 0, 'left', 1.6, [1, 1, 1]).fontScale, 1.6);
|
||||
});
|
||||
check(`${game} help rebuilds after records, caching each new view without allocation overlap`, () => {
|
||||
const e = engine(game, 8, true);
|
||||
e.help();
|
||||
const rows = [...e.state.command_rows, ...e.state.description_rows];
|
||||
const calls = e.textCalls;
|
||||
e.help(); e.advance(11000);
|
||||
assert.equal(e.textCalls, calls);
|
||||
e.record(snapshot()); e.advance(1000);
|
||||
assert(rows.every(row => row.destroyCalls === 1));
|
||||
assert.equal(e.live.length, 4);
|
||||
e.help();
|
||||
assert.equal(e.live.length, 19);
|
||||
assert(e.state.command_rows.every(row => !rows.includes(row)));
|
||||
assert.equal(e.peakLive, 19);
|
||||
const recreatedCalls = e.textCalls;
|
||||
e.help();
|
||||
assert.equal(e.textCalls, recreatedCalls);
|
||||
e.event('disconnect');
|
||||
assert(e.elements.every(row => row.destroyCalls === 1));
|
||||
});
|
||||
for (const event of ['disconnect', 'end_game', 'game_ended', 'intermission'])
|
||||
check(`${game} ${event} after idle cannot double-destroy or recreate a released view`, () => {
|
||||
const e = engine(game);
|
||||
e.record(snapshot()); e.advance(12000);
|
||||
assert.equal(e.live.length, 0);
|
||||
const allocated = e.elements.length;
|
||||
if (event === 'disconnect') e.event(event); else e.end(event);
|
||||
assert(e.state.closed);
|
||||
e.advance(60000);
|
||||
if (event !== 'disconnect') { e.help(); e.record(snapshot()); e.event('disconnect'); }
|
||||
assert.equal(e.elements.length, allocated);
|
||||
assert(e.elements.every(elem => elem.destroyCalls === 1));
|
||||
});
|
||||
check(`${game} script entrypoints and common references resolve`, () => {
|
||||
const functions = new Set([...eSource(game).matchAll(/^(\w+)\([^\n]*\)\s*\n\{/gm)].map(m => m[1]));
|
||||
const folder = new URL(`${game}/scripts/`, root);
|
||||
@@ -295,8 +518,67 @@ for (const game of ['t5', 't6']) {
|
||||
});
|
||||
}
|
||||
function eSource(game) { return readFileSync(new URL(`${game}/scripts/tws_${game}_common.gsc`, root), 'utf8'); }
|
||||
check('paired service behavior remains identical', () => {
|
||||
const section = game => eSource(game).slice(eSource(game).indexOf('show_help()\n{'), eSource(game).indexOf('// The only connected listener;'));
|
||||
assert.equal(section('t5'), section('t6'));
|
||||
check('T5 service transitions preserve actual core element identities, styles, data and visibility', () => {
|
||||
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);
|
||||
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);
|
||||
const state = () => core.map(elem => ({ alpha: elem.alpha, text: elem.text, value: elem.value,
|
||||
x: elem.x, y: elem.y, width: elem.width, height: elem.height, shader: elem.shader,
|
||||
color: elem.color, fontScale: elem.fontScale, alignX: elem.alignX,
|
||||
textCalls: elem.textCalls, valueCalls: elem.valueCalls, destroyCalls: elem.destroyCalls }));
|
||||
const initial = state();
|
||||
e.advance(3000);
|
||||
e.record(snapshot(['Jeff', 'Lokester', 'Moniialx']));
|
||||
const controller = e.state, tasks = e.tasks.length;
|
||||
assert.equal(e.live.length, 16);
|
||||
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.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.filter(elem => elem.archived === false).length, 19);
|
||||
e.c.toggle_core_hud();
|
||||
assert(core.every(elem => elem.alpha === 0));
|
||||
const allocations = e.elements.length;
|
||||
e.record(snapshot(['Latest']));
|
||||
assert.equal(e.elements.length, allocations, 'a pending record must not allocate another view');
|
||||
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(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.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.deepEqual(Array.from(e.self.tws_core_hud_elems), core);
|
||||
e.event('disconnect');
|
||||
assert.equal(e.live.length, 0);
|
||||
assert(e.elements.every(elem => elem.destroyCalls === 1));
|
||||
});
|
||||
check('paired service requests, rendering and lifecycle remain identical', () => {
|
||||
const sections = game => {
|
||||
const source = eSource(game);
|
||||
const service = source.slice(source.indexOf('show_help()\n{'), source.indexOf('on_player_connect_dispatch()\n{'));
|
||||
assert(service.length > 500, 'parity comparison must include actual service functions');
|
||||
return service;
|
||||
};
|
||||
assert.deepEqual(sections('t5'), sections('t6'));
|
||||
});
|
||||
console.log(`PASS ${checks} service HUD fixture/static checks (both games). GSC compilation and Plutonium rendering remain separate.`);
|
||||
|
||||
+6
-6
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"tws_t5_afk.gsc": "1.0.1",
|
||||
"tws_t5_afk.gsc": "1.2.0",
|
||||
"tws_t5_bank.gsc": "2.0.3",
|
||||
"tws_t5_common.gsc": "1.2.0",
|
||||
"tws_t5_hud.gsc": "1.2.0",
|
||||
"tws_t5_common.gsc": "1.4.0",
|
||||
"tws_t5_hud.gsc": "1.4.0",
|
||||
"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.0.1",
|
||||
"tws_t6_afk.gsc": "1.2.0",
|
||||
"tws_t6_bank.gsc": "2.0.3",
|
||||
"tws_t6_common.gsc": "1.2.0",
|
||||
"tws_t6_hud.gsc": "1.2.0",
|
||||
"tws_t6_common.gsc": "1.4.0",
|
||||
"tws_t6_hud.gsc": "1.4.0",
|
||||
"tws_t6_join.gsc": "1.0.1",
|
||||
"tws_t6_leaderboard.gsc": "1.1.0",
|
||||
"tws_t6_lock.gsc": "1.0.0",
|
||||
|
||||
Reference in New Issue
Block a user