Center zombie counters and use ASCII HUD separators
This commit is contained in:
+47
-15
@@ -26,11 +26,12 @@ function install(context, source, names) {
|
||||
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(/maps\\mp\\zombies\\_zm_utility::get_current_zombie_count/g, 'get_enemy_count')
|
||||
.replace(/self thread ([\w.]+)\(([^;]*)\);/g, (_, fn, values) => `queued.push(['${fn}', ${values}]);`)
|
||||
.replace(/self common\.toggle_core_hud\(\)/g, 'common.toggle_core_hud()')
|
||||
.replace(/self ((?:hud_|afk_)\w+)\(/g, '$1(')
|
||||
.replace(/self (\w+)\(/g, 'self.$1(')
|
||||
.replace(/(\w+) (SetText|SetValue|SetTimerUp|SetShader)\(([^;]*)\);/g, '$1.$2($3);')
|
||||
.replace(/(\w+) (SetText|SetTextUnlimited|SetValue|SetTimerUp|SetShader)\(([^;]*)\);/g, '$1.$2($3);')
|
||||
.replace(/level notify\(([^;]*)\);/g, 'level.notify($1);')
|
||||
.replace(/level waittill\(([^;]*)\);/g, 'yield { event: $1 };')
|
||||
.replace(/\.size\b/g, '.length')
|
||||
@@ -51,6 +52,7 @@ function engine(game, source, afk, hidden = false) {
|
||||
function element(args, alpha = 1) {
|
||||
const result = { args, alpha, writes: [],
|
||||
SetText(value) { this.text = value; this.writes.push(['text', value]); },
|
||||
SetTextUnlimited(value) { this.text = value; this.writes.push(['unlimitedText', value]); },
|
||||
SetValue(value) { this.value = value; this.writes.push(['value', value]); },
|
||||
SetShader(...args) { this.shader = args; },
|
||||
SetTimerUp(value) { this.timer = value; this.writes.push(['timer', value]); },
|
||||
@@ -79,7 +81,7 @@ function engine(game, source, afk, hidden = false) {
|
||||
player_active: player => player.active,
|
||||
format_mmss: ms => `${Math.floor(ms / 60000)}:${String(Math.floor(ms / 1000) % 60).padStart(2, '0')}`,
|
||||
};
|
||||
const level = { round_number: 1, hud_rate: .1, afk_grace_seconds: 2,
|
||||
const level = { round_number: 1, hud_rate: .1, afk_grace_seconds: 2, zombie_team: 'axis',
|
||||
hud_overlay_timer_x: 6, hud_overlay_match_y: 8, hud_overlay_round_y: 30,
|
||||
hud_overlay_brand_x: 320, hud_overlay_brand_y: 8,
|
||||
hud_overlay_count_x: 300, hud_overlay_count_y: 440,
|
||||
@@ -92,10 +94,11 @@ function engine(game, source, afk, hidden = false) {
|
||||
hud_c_count: 'white', hud_c_dim: 'dim', hud_c_afk: 'yellow',
|
||||
hud_c_high: 'green', hud_c_mid: 'yellow', hud_c_low: 'red', notify() {} };
|
||||
const context = vm.createContext({ self, level, common, queued, getTime: () => now,
|
||||
get_enemy_count: () => level.fixture_enemies ?? 0,
|
||||
isDefined: value => value !== undefined && value !== null, is_true: value => !!value,
|
||||
int: Math.trunc, max: Math.max });
|
||||
install(context, source, ['hud_connect', 'hud_text', 'hud_start', 'hud_toggle', 'hud_timer_row',
|
||||
'hud_health_display', 'freeze_timers_on_game_over']);
|
||||
'hud_health_display', 'hud_zombie_counter', 'freeze_timers_on_game_over']);
|
||||
install(context, afk, ['afk_spawn', 'afk_enter', 'afk_exit',
|
||||
...(game === 't6' ? ['afk_release', 'afk_lock_controls', 'afk_restore_controls'] : [])]);
|
||||
return { context, self, level, queued, elems, actions, common,
|
||||
@@ -118,7 +121,7 @@ for (const game of ['t5', 't6']) {
|
||||
assert(chat.includes('player thread hud_toggle()'), 'disconnect scope stays in the player worker');
|
||||
assert(!body(source, 'hud_connect')[1].includes('getTime()'));
|
||||
assert(!source.includes('tws_hud_hidden ='), 'spawn/construction must not reset the preference');
|
||||
for (const name of ['round_watcher', 'reset_timer_each_round', 'match_timer_bind'])
|
||||
for (const name of ['round_watcher', 'reset_timer_each_round', 'match_timer_bind', 'hud_zombie_counter'])
|
||||
assert(body(source, name)[1].includes('level endon("hud_game_over")'));
|
||||
const bind = body(source, 'match_timer_bind')[1];
|
||||
if (game === 't5') assert(bind.includes('elem SetTimerUp(0)'));
|
||||
@@ -127,13 +130,13 @@ for (const game of ['t5', 't6']) {
|
||||
assert(afk.includes('level.hud_overlay_afk_y'));
|
||||
assert(!/\.alpha\s*=/.test(afk), 'AFK visibility must use the common mask');
|
||||
});
|
||||
check(`${game}: connection places 11 box-free core elements at the screen edges without seeding clocks`, () => {
|
||||
check(`${game}: connection places 10 box-free core elements at the screen edges without seeding clocks`, () => {
|
||||
const e = make(); e.context.hud_connect();
|
||||
assert.equal(e.level.match_start_time, undefined); assert.equal(e.level.round_start_time, undefined);
|
||||
assert.equal(e.self.tws_hud_match.text, '--:--'); assert.equal(e.self.tws_hud_health.text, '--');
|
||||
assert.equal(e.elems.length, 11);
|
||||
assert.equal(e.self.tws_core_hud_elems.length, 11);
|
||||
e.context.hud_connect(); assert.equal(e.elems.length, 11);
|
||||
assert.equal(e.elems.length, 10);
|
||||
assert.equal(e.self.tws_core_hud_elems.length, 10);
|
||||
e.context.hud_connect(); assert.equal(e.elems.length, 10);
|
||||
assert.equal(e.queued.length, 1); assert.equal(e.queued[0][0], 'common.destroy_on_disconnect');
|
||||
assert.equal(e.queued[0][1].length, e.elems.length);
|
||||
const textAt = (text, x, y, align, scale, colour) => {
|
||||
@@ -142,9 +145,9 @@ for (const game of ['t5', 't6']) {
|
||||
};
|
||||
assert.deepEqual(e.self.tws_hud_match.args.slice(1), [6, 8, 'left', 1.4, 'white']);
|
||||
assert.deepEqual(e.self.tws_hud_round.args.slice(1), [6, 30, 'left', 1.25, 'dim']);
|
||||
textAt('TWS ZOMBIES · discord.tws.lol', 320, 8, 'center', 1.1, 'dim');
|
||||
assert.deepEqual(e.self.tws_hud_count.args.slice(1), [300, 440, 'right', 1.5, 'white']);
|
||||
textAt('ZOMBIES', 306, 446, 'left', 1.05, 'dim');
|
||||
textAt('TWS ZOMBIES | discord.tws.lol', 320, 8, 'center', 1.1, 'dim');
|
||||
textAt('-- ZOMBIES', 320, 440, 'center', 1.25, 'white');
|
||||
assert(!e.elems.some(elem => elem.text === 'ZOMBIES'), 'count and label share one alignment');
|
||||
textAt('HP', 449, 460, 'left', 1, 'white');
|
||||
for (const [key, x, align] of [['health', 414, 'right'], ['slash', 418, 'left'], ['maxhealth', 445, 'right']]) {
|
||||
const elem = e.self[`tws_hud_${key}`];
|
||||
@@ -165,13 +168,42 @@ for (const game of ['t5', 't6']) {
|
||||
assert.equal(e.level.match_start_time, undefined); assert.equal(e.level.round_start_time, undefined);
|
||||
assert.equal(e.queued.length, 1, 'toggle only queues disconnect cleanup');
|
||||
e.context.hud_start(); e.context.hud_start();
|
||||
assert.equal(e.elems.length, 11); assert.equal(e.self.tws_hud_hidden, true);
|
||||
assert.equal(e.elems.length, 10); assert.equal(e.self.tws_hud_hidden, true);
|
||||
assert.deepEqual(e.queued.slice(1).map(item => item[0]), ['hud_timer_row', 'hud_zombie_counter', 'hud_health_display']);
|
||||
e.context.hud_timer_row();
|
||||
assert.equal(e.level.match_start_time, 123456); assert.equal(e.level.round_start_time, 123456);
|
||||
const threads = e.queued.length;
|
||||
e.context.hud_toggle(); e.context.hud_toggle(); e.context.hud_start();
|
||||
assert.equal(e.queued.length, threads); assert.equal(e.elems.length, 11);
|
||||
assert.equal(e.queued.length, threads); assert.equal(e.elems.length, 10);
|
||||
});
|
||||
check(`${game}: complete zombie text stays centered through digit changes and hidden updates without config-string churn`, () => {
|
||||
const e = make();
|
||||
// Center on the configured track, including non-default and fractional midpoints.
|
||||
e.level.hud_overlay_bar_x = 210;
|
||||
e.level.hud_overlay_bar_width = 155;
|
||||
e.context.hud_connect();
|
||||
const count = e.self.tws_hud_count;
|
||||
assert.deepEqual(count.args.slice(1, 4), [287.5, 440, 'center']);
|
||||
const run = e.context.hud_zombie_counter();
|
||||
for (const value of [0, 6, 9, 10, 99, 100, 999, 1000, 6]) {
|
||||
e.level.fixture_enemies = value;
|
||||
run.next();
|
||||
assert.equal(count.text, `${value} ZOMBIES`);
|
||||
assert.deepEqual(count.args.slice(1, 4), [287.5, 440, 'center']);
|
||||
const writes = count.writes.length;
|
||||
run.next(); assert.equal(count.writes.length, writes, 'unchanged counts do not rewrite text');
|
||||
}
|
||||
e.level.zombie_total = 94; run.next();
|
||||
assert.equal(count.text, '100 ZOMBIES', 'remaining spawn count remains included');
|
||||
e.context.hud_toggle();
|
||||
e.level.zombie_total = -4; run.next();
|
||||
assert.equal(count.text, '6 ZOMBIES'); assert.equal(count.alpha, 0);
|
||||
e.context.hud_toggle();
|
||||
assert.equal(count.text, '6 ZOMBIES'); assert.equal(count.alpha, 1);
|
||||
assert.equal(count.writes.filter(([kind]) => kind === 'text').length, 1,
|
||||
'only the fixed placeholder uses a normal config string');
|
||||
assert(count.writes.slice(1).every(([kind]) => kind === 'unlimitedText'));
|
||||
assert.equal(e.elems.length, 10, 'updates and toggles never allocate replacement elements');
|
||||
});
|
||||
check(`${game}: health numbers appear only while damaged, follow the bar colour, and avoid config-string churn`, () => {
|
||||
const e = make(); e.context.hud_connect(); const { self } = e;
|
||||
@@ -216,7 +248,7 @@ for (const game of ['t5', 't6']) {
|
||||
e.context.hud_toggle();
|
||||
assert.equal(e.self.tws_hud_bar.alpha, 1); assert.equal(e.self.tws_hud_health.value, 50);
|
||||
assert.equal(other.self.tws_hud_hidden, undefined); assert.equal(other.self.tws_hud_health.alpha, 0);
|
||||
assert.equal(e.elems.length, 11); assert.equal(e.queued.length, 1);
|
||||
assert.equal(e.elems.length, 10); assert.equal(e.queued.length, 1);
|
||||
e.context.hud_toggle(); e.self.health = 100; run.next(); e.context.hud_toggle();
|
||||
assert.equal(e.self.tws_hud_health.alpha, 0, 'healing while hidden conceals the readout when shown');
|
||||
assert.equal(e.self.tws_hud_hp.alpha, 0); assert.equal(e.self.tws_hud_bar.alpha, 1);
|
||||
@@ -254,7 +286,7 @@ for (const game of ['t5', 't6']) {
|
||||
e.context.hud_toggle(); assert.equal(indicator.alpha, 0);
|
||||
exit.next(); assert.equal(e.self.ignoreme, false);
|
||||
assert(e.actions.some(([name]) => name === 'disableInvulnerability'));
|
||||
assert.equal(e.elems.length, 12);
|
||||
assert.equal(e.elems.length, 11);
|
||||
});
|
||||
if (game === 't6') check('t6: AFK release while hidden respects stock protection ownership', () => {
|
||||
const e = make(); e.context.hud_connect(); const watcher = e.spawnAfk();
|
||||
|
||||
+13
-12
@@ -45,7 +45,7 @@ function translate(source) {
|
||||
.replace(/(self|level) notify\(([^;]+)\);/g, 'emit($1, $2);')
|
||||
.replace(/self (\w+)\(/g, '$1(')
|
||||
.replace(/\bwait ([\d.]+);/g, 'yield { milliseconds: $1 * 1000 };')
|
||||
.replace(/([\w.]+(?:\[[^\]\n]+\])?)\s+(SetText|SetValue|SetShader|Destroy)\(/g, '$1.$2(')
|
||||
.replace(/([\w.]+(?:\[[^\]\n]+\])?)\s+(SetText|SetTextUnlimited|SetValue|SetShader|Destroy)\(/g, '$1.$2(')
|
||||
.replace(/__literal_(\d+)__/g, (_, i) => literals[Number(i)]);
|
||||
result.push(`function${coroutineNames.has(name) ? '*' : ''} ${name}(${match[2]}) { var ${[...locals].join(', ') || 'unused'}; ${body} }`);
|
||||
}
|
||||
@@ -99,6 +99,7 @@ function engine(game, count = 8, withHud = false) {
|
||||
owner,
|
||||
alpha: 1, textCalls: 0, valueCalls: 0, destroyCalls: 0,
|
||||
SetText(text) { assert(!this.destroyCalls); this.text = text; this.textCalls++; },
|
||||
SetTextUnlimited(text) { assert(!this.destroyCalls); this.text = text; this.textCalls++; },
|
||||
SetValue(value) { assert(!this.destroyCalls); this.value = value; this.valueCalls++; },
|
||||
SetShader(shader, width, height) {
|
||||
assert(!this.destroyCalls);
|
||||
@@ -335,7 +336,7 @@ for (const game of ['t5', 't6']) {
|
||||
assertCounter(e, 1, 1);
|
||||
e.record(snapshot(['A', 'B', 'C', 'D']));
|
||||
assert(first.every(elem => elem.destroyCalls === 1), 'pagedness changes release the old view first');
|
||||
assert.equal(e.state.record_title.text, 'Map record · Round 82');
|
||||
assert.equal(e.state.record_title.text, 'Map record | Round 82');
|
||||
assert.equal(e.live.length, 5);
|
||||
const paged = [...e.live];
|
||||
assertCounter(e, 1, 2);
|
||||
@@ -380,7 +381,7 @@ for (const game of ['t5', 't6']) {
|
||||
e.advance(11000);
|
||||
assert.equal(e.textCalls, calls);
|
||||
assert.equal(e.elements.length, count);
|
||||
assert.equal(e.state.record_title.text, 'Map record · Round 82');
|
||||
assert.equal(e.state.record_title.text, 'Map record | Round 82');
|
||||
assert.equal(e.state.commands[3].command, '.join');
|
||||
assert.match(e.c.help_description(e.state.commands[3]), /newcomer/);
|
||||
assert.equal(e.c.help_command_label(e.state.commands[6].command), '.d <amount|all>');
|
||||
@@ -522,9 +523,9 @@ check('T5 service transitions preserve actual core element identities, styles, d
|
||||
const e = engine('t5', 8, true);
|
||||
e.createCore();
|
||||
const core = Array.from(e.self.tws_core_hud_elems);
|
||||
assert.equal(core.length, 12, 'eleven core elements plus one optional AFK indicator');
|
||||
assert.equal(e.live.length, 12);
|
||||
e.self.tws_hud_count.SetValue(6);
|
||||
assert.equal(core.length, 11, 'ten core elements plus one optional AFK indicator');
|
||||
assert.equal(e.live.length, 11);
|
||||
e.self.tws_hud_count.SetTextUnlimited('6 ZOMBIES');
|
||||
e.self.tws_hud_health.SetValue(100);
|
||||
e.self.tws_hud_maxhealth.SetValue(100);
|
||||
e.c.set_core_alpha(e.self, e.self.tws_hud_bar, 1);
|
||||
@@ -536,17 +537,17 @@ check('T5 service transitions preserve actual core element identities, styles, d
|
||||
e.advance(3000);
|
||||
e.record(snapshot(['Jeff', 'Lokester', 'Moniialx']));
|
||||
const controller = e.state, tasks = e.tasks.length;
|
||||
assert.equal(e.live.length, 16);
|
||||
assert.equal(e.live.length, 15);
|
||||
assert.deepEqual(state(), initial);
|
||||
e.advance(12000);
|
||||
assert.equal(e.state, controller);
|
||||
assert.equal(e.state.closed, false, 'idle releases the view, not its controller');
|
||||
assert.equal(e.state.elems.length, 0);
|
||||
assert.equal(e.live.length, 12);
|
||||
assert.equal(e.live.length, 11);
|
||||
assert.deepEqual(state(), initial);
|
||||
for (let i = 0; i < 4; i++) {
|
||||
e.record(snapshot(['One', 'Two', 'Three', 'Four'])); e.advance(15000); e.help();
|
||||
assert.equal(e.live.length, 31, '12 core/AFK plus 19 help elements; this is not an engine pool-limit assertion');
|
||||
assert.equal(e.live.length, 30, '11 core/AFK plus 19 help elements; this is not an engine pool-limit assertion');
|
||||
assert.equal(e.live.filter(elem => elem.archived === false).length, 19);
|
||||
e.c.toggle_core_hud();
|
||||
assert(core.every(elem => elem.alpha === 0));
|
||||
@@ -556,17 +557,17 @@ check('T5 service transitions preserve actual core element identities, styles, d
|
||||
e.advance(12000);
|
||||
assert.equal(e.state.rows[0].text, 'Latest');
|
||||
assert.equal(e.state.record.remaining, 12000);
|
||||
assert.equal(e.live.length, 16);
|
||||
assert.equal(e.live.length, 15);
|
||||
assert(core.every(elem => elem.alpha === 0 && elem.destroyCalls === 0));
|
||||
e.c.toggle_core_hud();
|
||||
assert.deepEqual(state(), initial);
|
||||
e.advance(12000);
|
||||
assert.equal(e.live.length, 12);
|
||||
assert.equal(e.live.length, 11);
|
||||
assert.equal(e.state.elems.length, 0);
|
||||
assert.equal(e.tasks.length, tasks, 'reopening reuses the controller and cleanup watchers');
|
||||
assert.deepEqual(state(), initial);
|
||||
}
|
||||
assert.equal(e.peakLive, 31);
|
||||
assert.equal(e.peakLive, 30);
|
||||
assert.deepEqual(Array.from(e.self.tws_core_hud_elems), core);
|
||||
e.event('disconnect');
|
||||
assert.equal(e.live.length, 0);
|
||||
|
||||
Reference in New Issue
Block a user