Add fixed core HUD and shared service window for T5 and T6
This commit is contained in:
+29
-5
@@ -32,9 +32,11 @@ before copying the renamed scripts.
|
||||
- **`SetTimerUp(N)` only ticks when `N == 0`** on T5 SP. Non-zero seeds
|
||||
render "0:00" but don't advance. Late joiners see 0, not real elapsed;
|
||||
authoritative elapsed lives in `level.match_start_time`.
|
||||
- **Dynamic text: use `SetValue`, not `SetText`.** `SetText` allocates a
|
||||
config string per call; per-tick rebuilds overflow and take the server
|
||||
down. Composite strings like `"87/160"` are three elements.
|
||||
- **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.
|
||||
- **`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
|
||||
@@ -56,18 +58,40 @@ 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
|
||||
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.
|
||||
- **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
|
||||
[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.
|
||||
- **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")`
|
||||
is intercepted by the zombiemode scripts before it reaches our scope, and
|
||||
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.
|
||||
Run the [shared HUD checks](../../README.md#hud-validation); portable fixtures
|
||||
do not replace T5 live loading, layout and lifecycle validation.
|
||||
|
||||
## Module map
|
||||
|
||||
| File | Role | Fires notifies | Reads notifies |
|
||||
|---|---|---|---|
|
||||
| `tws_t5_common` | Shared config, HUD helpers, `.help` registry, spawn-handler registry, single connect listener + dispatcher | — | `say` |
|
||||
| `tws_t5_hud` | Timer row, zombie count, health bar | `hud_round_changed`, `hud_match_started`, `hud_game_over` | (internal) |
|
||||
| `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_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` |
|
||||
|
||||
+67
-38
@@ -10,8 +10,8 @@ Platform quirks and deploy commands: [NOTES.md](NOTES.md).
|
||||
|
||||
| File | Role |
|
||||
|---|---|
|
||||
| [tws_t5_common.gsc](tws_t5_common.gsc) | Shared config, HUD helpers, `.help`, connect/spawn registries, single `connected` listener + dispatcher |
|
||||
| [tws_t5_hud.gsc](tws_t5_hud.gsc) | Top-left Discord URL, timers, zombie count, health bar |
|
||||
| [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_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 |
|
||||
@@ -54,14 +54,24 @@ requests or journal writes, preserving existing strings exactly. Missing or
|
||||
invalid identities fail privately without changing points. Follow the shared
|
||||
[identity and deployment checks](../../README.md#bank-identity-validation).
|
||||
|
||||
## Persistent Discord text
|
||||
## Core HUD and service window
|
||||
|
||||
The HUD shows `https://discord.tws.lol` in small muted-gray text
|
||||
at the top-left, including for newcomers waiting in spectator mode. The label
|
||||
is created once per connection and persists across death and respawn. The
|
||||
gameplay HUD starts one line lower, keeping timers, help and records clear.
|
||||
Install HUD `1.1.2` with common `1.1.0` or later, then restart the map; no plugin
|
||||
update or full server-process restart is required.
|
||||
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.
|
||||
|
||||
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).
|
||||
|
||||
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.
|
||||
|
||||
## Map record announcements
|
||||
|
||||
@@ -83,8 +93,10 @@ 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 appears below the health bar for 12 seconds, clear of the timers, zombie
|
||||
counter and health display. Typing either command again restarts that display time.
|
||||
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.
|
||||
|
||||
| Command | Effect |
|
||||
|---|---|
|
||||
@@ -93,12 +105,17 @@ counter and health display. Typing either command again restarts that display ti
|
||||
| `.back` | Return from AFK |
|
||||
| `.join` | Join mid-round (newcomers only) |
|
||||
| `.leaders` | Show the map record again (leaderboard module required) |
|
||||
| `.b` | Check bank balance (bank module required) |
|
||||
| `.d <amount\|all>` | Deposit points (bank module required) |
|
||||
| `.w <amount\|all>` | Withdraw points (bank module required) |
|
||||
|
||||
Each module registers its own help entries; removing AFK or join also removes
|
||||
those entries from `.help` after a map restart.
|
||||
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.
|
||||
The enabled leaderboard module registers `.leaders`, which replays the latest
|
||||
cached map record privately. Repeating it restarts the record from the first page;
|
||||
help temporarily hides the record until its own display ends.
|
||||
help pauses the record's remaining page time and resumes it after the final
|
||||
help page. `.leaders` during help replaces the pending record replay from page one.
|
||||
|
||||
## Dependency graph
|
||||
|
||||
@@ -148,16 +165,18 @@ graph BT
|
||||
|
||||
## tws_t5_common.gsc
|
||||
|
||||
Hub file. Shared config, HUD helpers, spawn-handler registry, and the
|
||||
Hub file. Shared config, HUD helpers, service window, spawn-handler registry, and the
|
||||
single `level waittill("connected")` in the fleet. Adding a second
|
||||
connect listener anywhere else crashes the T5 SP handshake.
|
||||
|
||||
**Features**
|
||||
- Shared HUD constants: `level.hud_x`, `hud_y`, `hud_row_*`, `hud_col_*`,
|
||||
`hud_font_*`, `hud_bar_*`, `hud_c_*`, `hud_rate`, `hud_health_threshold`
|
||||
- Shared HUD constants for panel geometry, rows/columns, fonts, health bar,
|
||||
colors and update rate
|
||||
- Spawn-handler registry
|
||||
- Connection-handler registry for optional modules that serve spectator newcomers
|
||||
- `.help` listener and help registry populated by installed command modules
|
||||
- `.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
|
||||
- Connect listener + per-player dispatch: iterates
|
||||
`level.tws_spawn_handlers` after `spawned_player` and threads each on
|
||||
the player
|
||||
@@ -172,6 +191,9 @@ 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 |
|
||||
| `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 |
|
||||
| `format_mmss(elapsed_ms)` | Format milliseconds as `M:SS` |
|
||||
@@ -180,27 +202,33 @@ connect listener anywhere else crashes the T5 SP handshake.
|
||||
|
||||
**Live validation (requires a T5 server)**
|
||||
|
||||
1. Type `.help` and `.HELP` while playing and while spectating before the first
|
||||
spawn. Verify all four descriptions appear below the health bar, only for the
|
||||
requesting player. Take damage so health numbers are visible and check that
|
||||
they do not cover the help text.
|
||||
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.
|
||||
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. Repeat `.help` before 12 seconds elapse; verify one list stays visible for
|
||||
12 seconds after the latest request, then hides. Reopen it after expiry,
|
||||
die/respawn and reconnect; verify no duplicate text or leftover help elements.
|
||||
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
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## tws_t5_hud.gsc
|
||||
|
||||
Bottom-left cluster. Per-player elements spawn via `::hud_start`.
|
||||
Fixed upper-right core panel. 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)
|
||||
- Player-name row (only visible in co-op)
|
||||
- `TWS ZOMBIES` / `discord.tws.lol` header and reserved AFK footer
|
||||
- Zombies-remaining counter
|
||||
- Health bar with green / amber / red ramp; numbers fade in below full HP
|
||||
- Full-width health bar with green / amber / red ramp; current/maximum HP
|
||||
remains visible at full health
|
||||
- Spectator placeholders without starting the match clock on connection
|
||||
- Round-transition and game-over notify machinery
|
||||
|
||||
**Provides**
|
||||
@@ -214,22 +242,24 @@ Bottom-left cluster. Per-player elements spawn via `::hud_start`.
|
||||
|
||||
Spawn handler registered: `::hud_start` — threads `hud_timer_row`,
|
||||
`hud_zombie_counter`, `hud_health_display`.
|
||||
Connection handler registered: `::hud_connect` — creates the core with placeholders
|
||||
without seeding clocks; `hud_start` calls it as a fallback if needed.
|
||||
|
||||
**Depends on**
|
||||
- `scripts\sp\tws_t5_common::` — `ensure_config`, `make_text_elem`,
|
||||
`make_shader_elem`, `destroy_on_disconnect`, `format_mmss`,
|
||||
`register_spawn_handler`
|
||||
- `scripts\sp\tws_t5_common::` — `ensure_config`, `make_panel_text`,
|
||||
`make_panel_shader`, `destroy_on_disconnect`, `format_mmss`,
|
||||
`register_spawn_handler`, `register_connect_handler`
|
||||
- Every `level.hud_*` config value from `tws_t5_common`
|
||||
- Stock BO1 state: `level.round_number`, `level.zombie_total`,
|
||||
`level.players`, `self.health`, `self.maxhealth`, `self.name` /
|
||||
`self.playername`, AI entities' `.animname` and `.ignore_enemy_count`
|
||||
`level.players`, `self.health`, `self.maxhealth`, AI entities' `.animname`
|
||||
and `.ignore_enemy_count`
|
||||
|
||||
---
|
||||
|
||||
## tws_t5_afk.gsc
|
||||
|
||||
Self-invoked pause mode. Owns the `[AFK]` indicator element entirely — HUD
|
||||
module has zero knowledge of AFK.
|
||||
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.
|
||||
|
||||
**Features**
|
||||
- Chat commands `.afk` and `.back` (via `level waittill("say")` — not
|
||||
@@ -247,10 +277,9 @@ module has zero knowledge of AFK.
|
||||
- Level var: `level.afk_grace_seconds` (default 2)
|
||||
|
||||
**Depends on**
|
||||
- `scripts\sp\tws_t5_common::` — `ensure_config`, `make_text_elem`,
|
||||
- `scripts\sp\tws_t5_common::` — `ensure_config`, `make_panel_text`,
|
||||
`destroy_on_disconnect`, `register_spawn_handler`, `register_help_command`
|
||||
- Shared config: `level.hud_x`, `level.hud_row_match`,
|
||||
`level.hud_font_small`, `level.hud_c_afk`
|
||||
- Shared config: core footer position, `level.hud_font_small`, `level.hud_c_afk`
|
||||
- Stock BO1: `enableInvulnerability`, `disableInvulnerability`,
|
||||
`freezeControls`, `.ignoreme`, `level waittill("say", message, player)`
|
||||
|
||||
|
||||
@@ -50,9 +50,9 @@ afk_spawn()
|
||||
{
|
||||
self endon("disconnect");
|
||||
|
||||
afk = scripts\sp\tws_t5_common::make_text_elem(
|
||||
self, level.hud_x + 42, level.hud_row_match,
|
||||
"left", level.hud_font_small, level.hud_c_afk);
|
||||
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 SetText("[AFK]");
|
||||
afk.alpha = 0;
|
||||
self.hud_afk_elem = afk;
|
||||
|
||||
+409
-68
@@ -3,6 +3,11 @@
|
||||
#include maps\_utility;
|
||||
#include common_scripts\utility;
|
||||
|
||||
main()
|
||||
{
|
||||
precacheShader("white");
|
||||
}
|
||||
|
||||
init()
|
||||
{
|
||||
if (GetDvar(#"zombiemode") != "1")
|
||||
@@ -11,6 +16,9 @@ init()
|
||||
common_config();
|
||||
|
||||
register_help_command(".help", "List available commands");
|
||||
level thread service_game_over_via("end_game");
|
||||
level thread service_game_over_via("game_ended");
|
||||
level thread service_game_over_via("intermission");
|
||||
level thread help_chat_listener();
|
||||
level thread on_player_connect_dispatch();
|
||||
}
|
||||
@@ -20,41 +28,50 @@ common_config()
|
||||
if (isDefined(level.hud_x))
|
||||
return;
|
||||
|
||||
// Anchor the gameplay stack below the persistent Discord line.
|
||||
// Existing generic helpers retain their screen-anchor conventions. Panels
|
||||
// use their own right-edge origin and never push stock HUD areas around.
|
||||
level.hud_x = 10;
|
||||
level.hud_y = 30;
|
||||
|
||||
level.hud_row_match = level.hud_y;
|
||||
level.hud_row_round = level.hud_y + 15;
|
||||
level.hud_row_name = level.hud_y + 32;
|
||||
level.hud_row_count = level.hud_y + 54;
|
||||
level.hud_row_bar = level.hud_y + 84;
|
||||
|
||||
level.hud_font_small = 1.4;
|
||||
level.hud_font_large = 2.0;
|
||||
level.hud_font_mid = 1.6;
|
||||
|
||||
level.hud_bar_width = 120;
|
||||
level.hud_bar_height = 8;
|
||||
|
||||
level.hud_col_count = level.hud_x + 90;
|
||||
level.hud_col_name = level.hud_x + 60;
|
||||
level.hud_col_health = level.hud_x + level.hud_bar_width + 12;
|
||||
|
||||
level.hud_rate = 0.1;
|
||||
|
||||
// green/amber/red ramp is health-only
|
||||
level.hud_c_label = (0.85, 0.85, 0.85);
|
||||
level.hud_c_dim = (0.55, 0.55, 0.55);
|
||||
level.hud_c_count = (1, 1, 1);
|
||||
level.hud_c_name = (0.4, 0.9, 0.5);
|
||||
level.hud_c_high = (0.2, 1, 0.4);
|
||||
level.hud_c_mid = (1, 0.8, 0);
|
||||
level.hud_c_low = (1, 0.2, 0.2);
|
||||
level.hud_c_afk = (1, 0.8, 0.2);
|
||||
|
||||
// numbers fade in below this fraction of max hp
|
||||
// Retain the legacy fields for consumers of the original common config.
|
||||
// The panel renderers below use dedicated row/column offsets instead.
|
||||
level.hud_row_match = level.hud_y;
|
||||
level.hud_row_round = level.hud_y + 15;
|
||||
level.hud_row_name = level.hud_y + 32;
|
||||
level.hud_row_count = level.hud_y + 54;
|
||||
level.hud_col_count = level.hud_x + 90;
|
||||
level.hud_col_name = level.hud_x + 60;
|
||||
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_width = 220;
|
||||
level.hud_core_height = 100;
|
||||
level.hud_service_y = 108;
|
||||
level.hud_service_height = 124;
|
||||
level.hud_pad = 10;
|
||||
level.hud_row_header = 10;
|
||||
level.hud_row_timers = 34;
|
||||
level.hud_row_stats = 59;
|
||||
level.hud_row_bar = 80;
|
||||
level.hud_row_afk = 86;
|
||||
|
||||
level.hud_font_small = 1.1;
|
||||
level.hud_font_large = 1.6;
|
||||
level.hud_font_mid = 1.25;
|
||||
level.hud_font_afk = 1.0;
|
||||
level.hud_bar_width = 200;
|
||||
level.hud_bar_height = 4;
|
||||
level.hud_rate = 0.1;
|
||||
level.hud_c_panel = (0.07, 0.10, 0.08);
|
||||
level.hud_panel_alpha = 0.72;
|
||||
level.hud_c_label = (0.72, 0.77, 0.74);
|
||||
level.hud_c_dim = (0.65, 0.70, 0.67);
|
||||
level.hud_c_count = (0.93, 0.96, 0.94);
|
||||
level.hud_c_name = (0.65, 0.85, 0.70);
|
||||
level.hud_c_high = (0.65, 0.85, 0.70);
|
||||
level.hud_c_mid = (1, 0.8, 0);
|
||||
level.hud_c_low = (1, 0.2, 0.2);
|
||||
level.hud_c_afk = (1, 0.8, 0.2);
|
||||
|
||||
if (!isDefined(level.tws_spawn_handlers))
|
||||
level.tws_spawn_handlers = [];
|
||||
@@ -111,58 +128,356 @@ help_chat_listener()
|
||||
}
|
||||
}
|
||||
|
||||
// A single per-player controller owns service priority, pages and expiry.
|
||||
show_help()
|
||||
{
|
||||
// Cache the text per connection; repeated SetText calls consume config strings.
|
||||
if (!isDefined(self.tws_help_elems))
|
||||
{
|
||||
elems = [];
|
||||
// Use the same anchors as the HUD, with room below the health numbers.
|
||||
y = level.hud_row_bar + level.hud_bar_height + 20;
|
||||
title = make_text_elem(self, level.hud_x, y,
|
||||
"left", level.hud_font_mid, level.hud_c_afk);
|
||||
title SetText("Commands");
|
||||
elems[elems.size] = title;
|
||||
if (isDefined(level.tws_service_ended))
|
||||
return;
|
||||
self service_ensure();
|
||||
state = self.tws_service;
|
||||
service_advance(state, getTime());
|
||||
state.generation++;
|
||||
state.help = service_request("help", service_page_count(state.commands.size, 4), state.generation);
|
||||
service_render(state);
|
||||
}
|
||||
|
||||
// An undefined snapshot is the explicit unavailable view for manual requests.
|
||||
show_record(snapshot)
|
||||
{
|
||||
if (isDefined(level.tws_service_ended))
|
||||
return;
|
||||
self service_ensure();
|
||||
state = self.tws_service;
|
||||
service_advance(state, getTime());
|
||||
count = 0;
|
||||
if (isDefined(snapshot))
|
||||
count = snapshot.lines.size;
|
||||
state.generation++;
|
||||
state.record = service_request("record", service_page_count(count, 3), state.generation);
|
||||
state.record.snapshot = snapshot;
|
||||
service_render(state);
|
||||
}
|
||||
|
||||
service_page_count(count, rows)
|
||||
{
|
||||
return int((max(count, 1) + rows - 1) / rows);
|
||||
}
|
||||
|
||||
service_request(kind, pages, generation)
|
||||
{
|
||||
request = spawnStruct();
|
||||
request.kind = kind;
|
||||
request.generation = generation;
|
||||
request.page = 0;
|
||||
request.pages = pages;
|
||||
request.remaining = 12000;
|
||||
return request;
|
||||
}
|
||||
|
||||
service_current(state)
|
||||
{
|
||||
if (isDefined(state.help))
|
||||
return state.help;
|
||||
return state.record;
|
||||
}
|
||||
|
||||
service_advance(state, now)
|
||||
{
|
||||
elapsed = max(0, now - state.updated_at);
|
||||
state.updated_at = now;
|
||||
request = service_current(state);
|
||||
if (!isDefined(request) || state.closed)
|
||||
return;
|
||||
|
||||
// Only the visible request consumes time. Help leaves the record untouched.
|
||||
request.remaining -= elapsed;
|
||||
if (request.remaining > 0)
|
||||
return;
|
||||
request.page++;
|
||||
request.remaining = 12000;
|
||||
if (request.page >= request.pages)
|
||||
{
|
||||
if (request.kind == "help")
|
||||
state.help = undefined;
|
||||
else
|
||||
state.record = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// Stable order is independent of module initialization order; absent commands
|
||||
// stay absent. Unknown future commands follow the built-in ones in registry order.
|
||||
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;
|
||||
}
|
||||
|
||||
help_entries()
|
||||
{
|
||||
entries = [];
|
||||
if (!isDefined(level.tws_help_commands))
|
||||
return entries;
|
||||
for (rank = 0; rank <= 8; rank++)
|
||||
{
|
||||
for (i = 0; i < level.tws_help_commands.size; i++)
|
||||
{
|
||||
entry = level.tws_help_commands[i];
|
||||
elem = make_text_elem(self, level.hud_x, y + 22 + i * 18,
|
||||
"left", level.hud_font_small, level.hud_c_count);
|
||||
elem SetText("^3" + entry.command + "^7 - " + entry.description);
|
||||
elems[elems.size] = elem;
|
||||
if (help_rank(entry.command) == rank)
|
||||
entries[entries.size] = entry;
|
||||
}
|
||||
|
||||
self.tws_help_elems = elems;
|
||||
self thread destroy_on_disconnect(elems);
|
||||
}
|
||||
|
||||
for (i = 0; i < self.tws_help_elems.size; i++)
|
||||
{
|
||||
elem = self.tws_help_elems[i];
|
||||
elem.alpha = 1;
|
||||
}
|
||||
|
||||
// Only the expiry timer is replaced; the say listener and cleanup stay alive.
|
||||
self notify("tws_help_refresh");
|
||||
self thread hide_help_after_delay();
|
||||
return entries;
|
||||
}
|
||||
|
||||
hide_help_after_delay()
|
||||
help_command_label(command)
|
||||
{
|
||||
rank = help_rank(command);
|
||||
if (rank == 6) return ".d <n|all>";
|
||||
if (rank == 7) return ".w <n|all>";
|
||||
return command;
|
||||
}
|
||||
|
||||
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";
|
||||
return entry.description;
|
||||
}
|
||||
|
||||
service_ensure()
|
||||
{
|
||||
if (isDefined(self.tws_service))
|
||||
return;
|
||||
ensure_config();
|
||||
state = spawnStruct();
|
||||
state.elems = [];
|
||||
state.commands = help_entries();
|
||||
state.help = undefined;
|
||||
state.record = undefined;
|
||||
state.closed = false;
|
||||
state.updated_at = getTime();
|
||||
state.rendered = false;
|
||||
state.generation = 0;
|
||||
state.last_generation = 0;
|
||||
state.last_page = -1;
|
||||
state.record_text = [];
|
||||
state.help_page = -1;
|
||||
state.rows = [];
|
||||
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);
|
||||
}
|
||||
|
||||
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.alpha = 0;
|
||||
if (text != "")
|
||||
elem SetText(text);
|
||||
state.elems[state.elems.size] = elem;
|
||||
return elem;
|
||||
}
|
||||
|
||||
service_render(state)
|
||||
{
|
||||
if (state.closed)
|
||||
return;
|
||||
request = service_current(state);
|
||||
page = -1;
|
||||
generation = 0;
|
||||
if (isDefined(request))
|
||||
{
|
||||
page = request.page;
|
||||
generation = request.generation;
|
||||
}
|
||||
if (state.rendered && state.last_generation == generation && state.last_page == page)
|
||||
return;
|
||||
state.rendered = true;
|
||||
state.last_generation = generation;
|
||||
state.last_page = page;
|
||||
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 (request.kind == "help")
|
||||
{
|
||||
state.help_title.alpha = 1;
|
||||
state.help_footer.alpha = 1;
|
||||
if (state.help_page != page)
|
||||
{
|
||||
command_text = "";
|
||||
description_text = "";
|
||||
for (i = 0; i < 4; i++)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
state.command_column SetText(command_text);
|
||||
state.description_column SetText(description_text);
|
||||
state.help_page = page;
|
||||
}
|
||||
state.command_back.alpha = 0.9;
|
||||
state.command_column.alpha = 1;
|
||||
state.description_column.alpha = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
state.record_title.alpha = 1;
|
||||
state.record_footer.alpha = 1;
|
||||
snapshot = request.snapshot;
|
||||
if (!isDefined(snapshot))
|
||||
{
|
||||
state.unavailable.alpha = 1;
|
||||
return;
|
||||
}
|
||||
if (snapshot.round == 0)
|
||||
{
|
||||
state.empty.alpha = 1;
|
||||
return;
|
||||
}
|
||||
state.round_label.alpha = 1;
|
||||
state.round_number SetValue(snapshot.round);
|
||||
state.round_number.alpha = 1;
|
||||
state.holders_label.alpha = 1;
|
||||
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);
|
||||
state.record_text[i] = text;
|
||||
}
|
||||
state.rows[i].alpha = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
service_loop(state)
|
||||
{
|
||||
self endon("disconnect");
|
||||
self endon("tws_help_refresh");
|
||||
|
||||
wait 12;
|
||||
|
||||
for (i = 0; i < self.tws_help_elems.size; i++)
|
||||
level endon("tws_service_game_over");
|
||||
for (;;)
|
||||
{
|
||||
elem = self.tws_help_elems[i];
|
||||
elem.alpha = 0;
|
||||
service_advance(state, getTime());
|
||||
service_render(state);
|
||||
wait 0.1;
|
||||
}
|
||||
}
|
||||
|
||||
// only connect listener in the fleet; a second one crashes the T5 SP handshake
|
||||
service_disconnect(state)
|
||||
{
|
||||
self waittill("disconnect");
|
||||
service_destroy(state);
|
||||
}
|
||||
|
||||
service_end_game(state)
|
||||
{
|
||||
self endon("disconnect");
|
||||
level waittill("tws_service_game_over");
|
||||
service_destroy(state);
|
||||
}
|
||||
|
||||
service_destroy(state)
|
||||
{
|
||||
if (state.closed)
|
||||
return;
|
||||
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_game_over_via(event)
|
||||
{
|
||||
level endon("tws_service_game_over");
|
||||
level waittill(event);
|
||||
level.tws_service_ended = true;
|
||||
level notify("tws_service_game_over");
|
||||
}
|
||||
|
||||
// The only connected listener; modules use the shared registries.
|
||||
on_player_connect_dispatch()
|
||||
{
|
||||
for (;;)
|
||||
@@ -238,6 +553,32 @@ 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.
|
||||
make_panel_text(player, x, y, align_x, scale, colour)
|
||||
{
|
||||
elem = make_text_elem(player, x - level.hud_panel_width - level.hud_panel_right,
|
||||
level.hud_panel_top + y, align_x, scale, colour);
|
||||
elem.horzAlign = "right";
|
||||
elem.sort = 20;
|
||||
elem.archived = true;
|
||||
return elem;
|
||||
}
|
||||
|
||||
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,
|
||||
level.hud_panel_top + y);
|
||||
elem.horzAlign = "right";
|
||||
elem.color = colour;
|
||||
elem.alpha = alpha;
|
||||
elem.sort = sort;
|
||||
elem.archived = true;
|
||||
elem SetShader("white", width, height);
|
||||
return elem;
|
||||
}
|
||||
|
||||
destroy_on_disconnect(elems)
|
||||
{
|
||||
self waittill("disconnect");
|
||||
|
||||
+112
-171
@@ -1,4 +1,4 @@
|
||||
/* tws_t5_hud.gsc — top-left Discord URL, timers, zombie count and health bar. */
|
||||
/* tws_t5_hud.gsc - fixed upper-right core HUD, visible from connection. */
|
||||
|
||||
#include maps\_utility;
|
||||
#include common_scripts\utility;
|
||||
@@ -13,34 +13,90 @@ init()
|
||||
level thread round_watcher();
|
||||
level thread game_over_watcher();
|
||||
|
||||
scripts\sp\tws_t5_common::register_connect_handler(::hud_connect);
|
||||
scripts\sp\tws_t5_common::register_spawn_handler(::hud_start);
|
||||
scripts\sp\tws_t5_common::register_connect_handler(::hud_discord);
|
||||
}
|
||||
|
||||
// One static label per connection, including newcomers waiting to spawn.
|
||||
hud_discord()
|
||||
// Connection construction never seeds clocks or reads player gameplay state.
|
||||
// The first-spawn fallback also handles a spawn before the connect callback.
|
||||
hud_connect()
|
||||
{
|
||||
self endon("disconnect");
|
||||
|
||||
label = scripts\sp\tws_t5_common::make_text_elem(
|
||||
self, 10, 10, "left", 1.2, (0.65, 0.65, 0.65));
|
||||
label SetText("https://discord.tws.lol");
|
||||
if (isDefined(self.tws_hud_elems))
|
||||
return;
|
||||
|
||||
elems = [];
|
||||
elems[elems.size] = label;
|
||||
self thread scripts\sp\tws_t5_common::destroy_on_disconnect(elems);
|
||||
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 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");
|
||||
|
||||
// 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, "--");
|
||||
|
||||
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);
|
||||
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_elems[self.tws_hud_elems.size] = self.tws_hud_bar;
|
||||
|
||||
self thread scripts\sp\tws_t5_common::destroy_on_disconnect(self.tws_hud_elems);
|
||||
}
|
||||
|
||||
hud_text(x, y, align, scale, colour, text)
|
||||
{
|
||||
elem = scripts\sp\tws_t5_common::make_panel_text(self, x, y, align, scale, colour);
|
||||
elem SetText(text);
|
||||
self.tws_hud_elems[self.tws_hud_elems.size] = elem;
|
||||
return elem;
|
||||
}
|
||||
|
||||
hud_start()
|
||||
{
|
||||
self endon("disconnect");
|
||||
|
||||
self hud_connect();
|
||||
if (isDefined(self.tws_hud_bound) || is_true(level.tws_ended))
|
||||
return;
|
||||
|
||||
self.tws_hud_bound = true;
|
||||
self thread hud_timer_row();
|
||||
self thread hud_zombie_counter();
|
||||
self thread hud_health_display();
|
||||
}
|
||||
|
||||
// BO1 maps use different end-of-match notify names; race all three
|
||||
// Race the map-dependent game-over notifications once at level scope.
|
||||
game_over_watcher()
|
||||
{
|
||||
level thread game_over_via("end_game");
|
||||
@@ -52,17 +108,14 @@ game_over_via(notify_name)
|
||||
{
|
||||
level endon("hud_game_over");
|
||||
level waittill(notify_name);
|
||||
level.tws_ended = true;
|
||||
level notify("hud_game_over");
|
||||
}
|
||||
|
||||
/*
|
||||
Watch level.round_number rather than a notify — the notify names vary
|
||||
between BO1 map scripts, the variable doesn't. Seed on OBSERVED
|
||||
transitions only: many maps pre-set round_number = 1 at level init
|
||||
and we don't want getTime() captured there (see NOTES.md).
|
||||
*/
|
||||
// Pre-set round 1 is seeded on first spawn; only observed transitions seed here.
|
||||
round_watcher()
|
||||
{
|
||||
level endon("hud_game_over");
|
||||
last = 0;
|
||||
|
||||
if (isDefined(level.round_number))
|
||||
@@ -92,7 +145,7 @@ hud_timer_row()
|
||||
{
|
||||
self endon("disconnect");
|
||||
|
||||
// fallback seed for the round-1-already-active case (round_watcher misses it)
|
||||
// A visible panel 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)
|
||||
@@ -102,59 +155,17 @@ hud_timer_row()
|
||||
level notify("hud_match_started");
|
||||
}
|
||||
|
||||
match = scripts\sp\tws_t5_common::make_text_elem(
|
||||
self, level.hud_x, level.hud_row_match,
|
||||
"left", level.hud_font_small, level.hud_c_dim);
|
||||
match.alpha = 0; // revealed by match_timer_bind when round 1 opens
|
||||
|
||||
round = scripts\sp\tws_t5_common::make_text_elem(
|
||||
self, level.hud_x, level.hud_row_round,
|
||||
"left", level.hud_font_small, level.hud_c_dim);
|
||||
round = self.tws_hud_round;
|
||||
round SetTimerUp(0);
|
||||
|
||||
// name only shows in co-op — your own name is noise in solo
|
||||
name = scripts\sp\tws_t5_common::make_text_elem(
|
||||
self, level.hud_x, level.hud_row_name,
|
||||
"left", level.hud_font_small, level.hud_c_name);
|
||||
|
||||
if (isDefined(self.name))
|
||||
name SetText(self.name);
|
||||
else
|
||||
name SetText(self.playername);
|
||||
|
||||
name.alpha = 0;
|
||||
|
||||
elems = [];
|
||||
elems[elems.size] = match;
|
||||
elems[elems.size] = round;
|
||||
elems[elems.size] = name;
|
||||
self thread scripts\sp\tws_t5_common::destroy_on_disconnect(elems);
|
||||
|
||||
self thread reset_timer_each_round(round);
|
||||
self thread match_timer_bind(match);
|
||||
self thread freeze_timers_on_game_over(match, round);
|
||||
|
||||
coop = -1;
|
||||
|
||||
for (;;)
|
||||
{
|
||||
is_coop = 0;
|
||||
|
||||
if (isDefined(level.players) && level.players.size > 1)
|
||||
is_coop = 1;
|
||||
|
||||
if (is_coop != coop)
|
||||
{
|
||||
name.alpha = is_coop;
|
||||
coop = is_coop;
|
||||
}
|
||||
|
||||
wait (1);
|
||||
}
|
||||
self thread match_timer_bind(self.tws_hud_match);
|
||||
self thread freeze_timers_on_game_over(self.tws_hud_match, round);
|
||||
}
|
||||
|
||||
reset_timer_each_round(timer)
|
||||
{
|
||||
level endon("hud_game_over");
|
||||
self endon("disconnect");
|
||||
|
||||
for (;;)
|
||||
@@ -166,26 +177,20 @@ reset_timer_each_round(timer)
|
||||
}
|
||||
}
|
||||
|
||||
// SetTimerUp only ticks with arg 0 on T5 SP; late joiners start at 0
|
||||
// T5 SP only ticks with a zero seed; late joiners retain that engine limitation.
|
||||
match_timer_bind(elem)
|
||||
{
|
||||
self endon("disconnect");
|
||||
level endon("hud_game_over");
|
||||
|
||||
if (!isDefined(level.match_start_time))
|
||||
level waittill("hud_match_started");
|
||||
|
||||
if (isDefined(elem))
|
||||
{
|
||||
elem SetTimerUp(0);
|
||||
elem.alpha = 1;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Level-scope trigger only — per-player death/health polling is intercepted
|
||||
by the zombiemode scripts. Freeze value math relies on the timers being
|
||||
anchored at first spawn / first round transition, not level init.
|
||||
*/
|
||||
// The panel remains visible at game over, with authoritative frozen clocks.
|
||||
freeze_timers_on_game_over(match_elem, round_elem)
|
||||
{
|
||||
self endon("disconnect");
|
||||
@@ -201,27 +206,16 @@ freeze_timers_on_game_over(match_elem, round_elem)
|
||||
hud_zombie_counter()
|
||||
{
|
||||
self endon("disconnect");
|
||||
level endon("hud_game_over");
|
||||
|
||||
label = scripts\sp\tws_t5_common::make_text_elem(
|
||||
self, level.hud_x, level.hud_row_count,
|
||||
"left", level.hud_font_large, level.hud_c_label);
|
||||
label SetText("Zombies");
|
||||
|
||||
count = scripts\sp\tws_t5_common::make_text_elem(
|
||||
self, level.hud_col_count, level.hud_row_count,
|
||||
"left", level.hud_font_large, level.hud_c_count);
|
||||
count SetValue(0);
|
||||
|
||||
elems = [];
|
||||
elems[elems.size] = label;
|
||||
elems[elems.size] = count;
|
||||
self thread scripts\sp\tws_t5_common::destroy_on_disconnect(elems);
|
||||
|
||||
count = self.tws_hud_count;
|
||||
last = -1;
|
||||
|
||||
for (;;)
|
||||
{
|
||||
remaining = get_enemy_count() + level.zombie_total;
|
||||
remaining = get_enemy_count();
|
||||
if (isDefined(level.zombie_total))
|
||||
remaining += max(0, level.zombie_total);
|
||||
|
||||
if (remaining != last)
|
||||
{
|
||||
@@ -253,117 +247,64 @@ get_enemy_count()
|
||||
hud_health_display()
|
||||
{
|
||||
self endon("disconnect");
|
||||
|
||||
if (!isDefined(self.maxhealth) || self.maxhealth <= 0)
|
||||
self.maxhealth = 100;
|
||||
level endon("hud_game_over");
|
||||
|
||||
bar_width = level.hud_bar_width;
|
||||
|
||||
// -2 on both axes so the back frames the bar with 2px padding
|
||||
back = scripts\sp\tws_t5_common::make_shader_elem(self, level.hud_x - 2, level.hud_row_bar - 2);
|
||||
back.alpha = 0.5;
|
||||
back.color = (0, 0, 0);
|
||||
back SetShader("black", bar_width + 4, level.hud_bar_height + 4);
|
||||
|
||||
bar = scripts\sp\tws_t5_common::make_shader_elem(self, level.hud_x, level.hud_row_bar);
|
||||
bar.color = level.hud_c_high;
|
||||
bar SetShader("white", bar_width, level.hud_bar_height);
|
||||
|
||||
/*
|
||||
"87/160" is three elements, not a rebuilt string. SetText allocates
|
||||
a config string per call; per-tick rebuilds crash the server.
|
||||
cur is right-aligned so digits grow leftward and the slash + max
|
||||
never shift.
|
||||
*/
|
||||
cur = scripts\sp\tws_t5_common::make_text_elem(
|
||||
self, level.hud_col_health + 26, level.hud_row_bar + 3,
|
||||
"right", level.hud_font_mid, level.hud_c_high);
|
||||
cur SetValue(self.health);
|
||||
|
||||
slash = scripts\sp\tws_t5_common::make_text_elem(
|
||||
self, level.hud_col_health + 30, level.hud_row_bar + 3,
|
||||
"center", level.hud_font_mid, level.hud_c_high);
|
||||
slash SetText("/");
|
||||
|
||||
max_hp = scripts\sp\tws_t5_common::make_text_elem(
|
||||
self, level.hud_col_health + 34, level.hud_row_bar + 3,
|
||||
"left", level.hud_font_mid, level.hud_c_high);
|
||||
max_hp SetValue(self.maxhealth);
|
||||
|
||||
cur.alpha = 0;
|
||||
slash.alpha = 0;
|
||||
max_hp.alpha = 0;
|
||||
|
||||
elems = [];
|
||||
elems[elems.size] = back;
|
||||
elems[elems.size] = bar;
|
||||
elems[elems.size] = cur;
|
||||
elems[elems.size] = slash;
|
||||
elems[elems.size] = max_hp;
|
||||
self thread scripts\sp\tws_t5_common::destroy_on_disconnect(elems);
|
||||
|
||||
last_width = bar_width;
|
||||
bar = self.tws_hud_bar;
|
||||
cur = self.tws_hud_health;
|
||||
slash = self.tws_hud_slash;
|
||||
max_hp = self.tws_hud_maxhealth;
|
||||
last_width = -1;
|
||||
last_health = -1;
|
||||
last_max = -1;
|
||||
last_shown = -1;
|
||||
last_max = -1;
|
||||
|
||||
for (;;)
|
||||
{
|
||||
if (!isDefined(self.maxhealth) || self.maxhealth <= 0)
|
||||
self.maxhealth = 100;
|
||||
|
||||
frac = self.health / self.maxhealth;
|
||||
// Display fallbacks must not alter gameplay health values.
|
||||
maximum = 100;
|
||||
if (isDefined(self.maxhealth) && self.maxhealth > 0)
|
||||
maximum = self.maxhealth;
|
||||
current = 0;
|
||||
if (isDefined(self.health))
|
||||
current = max(0, self.health);
|
||||
frac = current / (maximum * 1.0);
|
||||
|
||||
if (frac > 1)
|
||||
frac = 1;
|
||||
|
||||
if (frac < 0)
|
||||
frac = 0;
|
||||
|
||||
width = int(max(frac * bar_width, 1));
|
||||
|
||||
if (width != last_width)
|
||||
{
|
||||
bar SetShader("white", width, level.hud_bar_height);
|
||||
last_width = width;
|
||||
}
|
||||
|
||||
colour = level.hud_c_high;
|
||||
bar.alpha = 1;
|
||||
if (frac == 0)
|
||||
bar.alpha = 0;
|
||||
|
||||
colour = level.hud_c_high;
|
||||
if (frac <= 0.25)
|
||||
colour = level.hud_c_low;
|
||||
else if (frac <= 0.5)
|
||||
colour = level.hud_c_mid;
|
||||
|
||||
bar.color = colour;
|
||||
cur.color = colour;
|
||||
slash.color = colour;
|
||||
bar.color = colour;
|
||||
cur.color = colour;
|
||||
slash.color = colour;
|
||||
max_hp.color = colour;
|
||||
|
||||
// numbers only show once you're hurt
|
||||
show = 0;
|
||||
|
||||
if (frac < level.hud_health_threshold)
|
||||
show = 1;
|
||||
|
||||
if (show != last_shown)
|
||||
// Health numbers remain visible at full health.
|
||||
if (current != last_health)
|
||||
{
|
||||
cur.alpha = show;
|
||||
slash.alpha = show;
|
||||
max_hp.alpha = show;
|
||||
last_shown = show;
|
||||
cur SetValue(current);
|
||||
last_health = current;
|
||||
}
|
||||
|
||||
if (self.health != last_health)
|
||||
if (maximum != last_max)
|
||||
{
|
||||
cur SetValue(self.health);
|
||||
last_health = self.health;
|
||||
}
|
||||
|
||||
if (self.maxhealth != last_max)
|
||||
{
|
||||
max_hp SetValue(self.maxhealth);
|
||||
last_max = self.maxhealth;
|
||||
max_hp SetValue(maximum);
|
||||
last_max = maximum;
|
||||
}
|
||||
|
||||
wait (level.hud_rate);
|
||||
|
||||
@@ -124,120 +124,17 @@ lb_request(manual)
|
||||
level endon("game_ended");
|
||||
level endon("intermission");
|
||||
|
||||
snapshot = lb_cached_record();
|
||||
if (!isDefined(snapshot))
|
||||
{
|
||||
if (manual)
|
||||
self iPrintLnBold("^3Map record unavailable; try .leaders again shortly");
|
||||
return;
|
||||
}
|
||||
// A manual replay replaces any welcome announcement still waiting to run.
|
||||
// This also applies when the cache is unavailable.
|
||||
if (manual)
|
||||
self notify("tws_lb_requested");
|
||||
self lb_show_hud(snapshot);
|
||||
}
|
||||
snapshot = lb_cached_record();
|
||||
if (!isDefined(snapshot) && !manual)
|
||||
return;
|
||||
|
||||
lb_show_hud(snapshot)
|
||||
{
|
||||
// Stop the previous display and its cleanup threads before replacing it.
|
||||
self notify("tws_lb_hud_done");
|
||||
if (isDefined(self.tws_lb_elems))
|
||||
lb_hud_destroy(self.tws_lb_elems);
|
||||
self endon("disconnect");
|
||||
self endon("tws_lb_hud_done");
|
||||
|
||||
scripts\sp\tws_t5_common::ensure_config();
|
||||
// Match .help's position, typography and row spacing below the health bar.
|
||||
y = level.hud_row_bar + level.hud_bar_height + 20;
|
||||
elems = [];
|
||||
title = scripts\sp\tws_t5_common::make_text_elem(
|
||||
self, level.hud_x, y, "left", level.hud_font_mid, level.hud_c_afk);
|
||||
title.alpha = 0;
|
||||
if (snapshot.round == 0)
|
||||
title SetText("No recorded high round yet");
|
||||
else
|
||||
title SetText("Record: Round " + snapshot.round);
|
||||
elems[elems.size] = title;
|
||||
|
||||
for (i = 0; i < 3; i++)
|
||||
{
|
||||
row = scripts\sp\tws_t5_common::make_text_elem(
|
||||
self, level.hud_x, y + 22 + i * 18,
|
||||
"left", level.hud_font_small, level.hud_c_count);
|
||||
row.alpha = 0;
|
||||
elems[elems.size] = row;
|
||||
}
|
||||
|
||||
// Cleanup runs independently of the display and ends when it is replaced.
|
||||
self.tws_lb_elems = elems;
|
||||
self thread lb_hud_disconnect(elems);
|
||||
self thread lb_hud_end_game(elems, "end_game");
|
||||
self thread lb_hud_end_game(elems, "game_ended");
|
||||
self thread lb_hud_end_game(elems, "intermission");
|
||||
|
||||
offset = 0;
|
||||
for (;;)
|
||||
{
|
||||
// Text changes only once per page, never during the visibility loop.
|
||||
for (i = 0; i < 3; i++)
|
||||
{
|
||||
row = elems[i + 1];
|
||||
if (offset + i < snapshot.lines.size)
|
||||
row SetText("^3Held by: ^7" + snapshot.lines[offset + i]);
|
||||
else
|
||||
row SetText("");
|
||||
}
|
||||
|
||||
remaining = 12;
|
||||
while (remaining > 0)
|
||||
{
|
||||
visible = !self lb_hud_help_visible();
|
||||
for (i = 0; i < elems.size; i++)
|
||||
elems[i].alpha = visible;
|
||||
wait 0.1;
|
||||
// Pause the page while .help occupies the same area.
|
||||
if (visible)
|
||||
remaining -= 0.1;
|
||||
}
|
||||
offset += 3;
|
||||
if (offset >= snapshot.lines.size)
|
||||
break;
|
||||
}
|
||||
lb_hud_destroy(elems);
|
||||
self.tws_lb_elems = undefined;
|
||||
self notify("tws_lb_hud_done");
|
||||
}
|
||||
|
||||
lb_hud_help_visible()
|
||||
{
|
||||
return isDefined(self.tws_help_elems) && self.tws_help_elems.size > 0 &&
|
||||
isDefined(self.tws_help_elems[0]) && self.tws_help_elems[0].alpha > 0;
|
||||
}
|
||||
|
||||
lb_hud_disconnect(elems)
|
||||
{
|
||||
self endon("tws_lb_hud_done");
|
||||
self waittill("disconnect");
|
||||
lb_hud_destroy(elems);
|
||||
}
|
||||
|
||||
lb_hud_end_game(elems, event)
|
||||
{
|
||||
self endon("disconnect");
|
||||
self endon("tws_lb_hud_done");
|
||||
level waittill(event);
|
||||
lb_hud_destroy(elems);
|
||||
self.tws_lb_elems = undefined;
|
||||
self notify("tws_lb_hud_done");
|
||||
}
|
||||
|
||||
lb_hud_destroy(elems)
|
||||
{
|
||||
for (i = 0; i < elems.size; i++)
|
||||
{
|
||||
if (isDefined(elems[i]))
|
||||
elems[i] Destroy();
|
||||
}
|
||||
// Common owns pagination, help priority and the single service window.
|
||||
// Undefined snapshots produce a private unavailable page for manual replay.
|
||||
self scripts\sp\tws_t5_common::show_record(snapshot);
|
||||
}
|
||||
|
||||
lb_refresh()
|
||||
@@ -480,22 +377,85 @@ lb_holder_lines(names)
|
||||
{
|
||||
lines = [];
|
||||
line = "";
|
||||
columns = 0;
|
||||
for (i = 0; i < names.size; i++)
|
||||
{
|
||||
if (line != "" && line.size + names[i].size + 2 > 100)
|
||||
name = lb_holder_name(names[i]);
|
||||
width = lb_holder_width(name);
|
||||
if (line != "" && columns + width + 2 > 16)
|
||||
{
|
||||
lines[lines.size] = line;
|
||||
line = "";
|
||||
columns = 0;
|
||||
}
|
||||
if (line != "")
|
||||
{
|
||||
line += ", ";
|
||||
line += names[i];
|
||||
columns += 2;
|
||||
}
|
||||
line += name;
|
||||
columns += width;
|
||||
}
|
||||
if (line != "")
|
||||
lines[lines.size] = line;
|
||||
return lines;
|
||||
}
|
||||
|
||||
lb_holder_name(value)
|
||||
{
|
||||
// The service has 200 virtual units at font scale 1.1. Use a conservative
|
||||
// 16-column bound, counting non-ASCII glyphs twice, pending in-game metrics.
|
||||
// A long name gets one bounded entry; no tied holder is dropped.
|
||||
value = lb_display_text(value, 64);
|
||||
if (value == "")
|
||||
value = "Unknown player";
|
||||
if (lb_holder_width(value) <= 16)
|
||||
return value;
|
||||
|
||||
output = "";
|
||||
columns = 0;
|
||||
for (i = 0; i < value.size; i++)
|
||||
{
|
||||
code = level.tws_lb_bytes[getSubStr(value, i, i + 1)];
|
||||
width = 1;
|
||||
if (code >= 128)
|
||||
width = 2;
|
||||
if (columns + width > 13)
|
||||
break;
|
||||
count = lb_glyph_bytes(code);
|
||||
output += getSubStr(value, i, i + count);
|
||||
columns += width;
|
||||
i += count - 1;
|
||||
}
|
||||
return output + "...";
|
||||
}
|
||||
|
||||
lb_holder_width(value)
|
||||
{
|
||||
columns = 0;
|
||||
for (i = 0; i < value.size; i++)
|
||||
{
|
||||
code = level.tws_lb_bytes[getSubStr(value, i, i + 1)];
|
||||
if (code >= 128)
|
||||
columns += 2;
|
||||
else
|
||||
columns++;
|
||||
i += lb_glyph_bytes(code) - 1;
|
||||
}
|
||||
return columns;
|
||||
}
|
||||
|
||||
lb_glyph_bytes(code)
|
||||
{
|
||||
if (code >= 240)
|
||||
return 4;
|
||||
if (code >= 224)
|
||||
return 3;
|
||||
if (code >= 192)
|
||||
return 2;
|
||||
return 1;
|
||||
}
|
||||
|
||||
lb_display_text(value, limit)
|
||||
{
|
||||
output = "";
|
||||
|
||||
Reference in New Issue
Block a user