0.2.6-dev: port PR #552 by IceCodeNew — an optional 「弹药数量」 setting (default off: the official HUD has no such number, the maintainer's decision of 2026-10-13) shows an ammo skill's rounds left / magazine under its bar, from the exact b.snap ammo counts; sanitised with the other settings, applied by the field view live, all four language packs, the diagnostics line; the simplified board has no ammo bar; DESIGN §31.9; golden: unchanged
This commit is contained in:
1 parent
231da0efbc
commit
461a7747e1
16 files changed
+244
-17
No files matched your search
@@ -3,6 +3,7 @@
|
||||
// [[id, left, max]] and `neg` [[id, fill]] beside the unit tuples and samples them from the older snapshot; render/units.js
|
||||
// draws the ammo skill's segmented yellow bar in place of the SP bar, 伺夜's 狼影 as a row of diamonds and the 我执 pool
|
||||
// as the red bar over the drained HP bar; an ammo skill has no standing aura (headless fake PIXI, test/render/fakepixi.js).
|
||||
// 设置「弹药数量」 (PR #552 by @IceCodeNew, off by default) adds the magazine's count under the ammo bar.
|
||||
// The server side is test/sim/hud-readouts.test.js and test/content/op_hsgma2.test.js.
|
||||
// Run: node --test test/render/hud-readouts.test.js
|
||||
|
||||
@@ -166,6 +167,92 @@ describe('UnitView: the ammo skill\'s segmented bar', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// 设置「弹药数量」 (PR #552 by @IceCodeNew, DESIGN §31.9): the official HUD has no number under the ammo bar, so the count is an
|
||||
// option, off by default (the maintainer's decision of 2026-10-13); the settings side is test/ui/ammo-count.test.js
|
||||
describe('UnitView: the optional ammo count under the bar (设置「弹药数量」)', () => {
|
||||
const counting = (on = true) => view({}, { settings: { damageNumbers: true, quality: 'high', ammoCount: on } });
|
||||
const coinY = (v) => v.coinHud.root.position.y;
|
||||
|
||||
test('off (the default): nothing is drawn under the magazine, and the rows below stay where they were', () => {
|
||||
const v = view();
|
||||
const w = counting(false);
|
||||
try {
|
||||
for (const u of [v, w]) { u.sync(sample({ flags: UF.SKILL, ammo: [13, 14], coins: [3, 10] })); frames(u); }
|
||||
assert.equal(v.ammoText, null, 'never built without the setting');
|
||||
assert.equal(w.ammoText, null, 'nor with it off');
|
||||
assert.equal(v.hud.children.some((c) => c instanceof fake.P.Text && /\d+\/14/.test(c.text)), false);
|
||||
const plain = view();
|
||||
plain.sync(sample({ coins: [3, 10] }));
|
||||
frames(plain);
|
||||
assert.equal(coinY(v), coinY(w));
|
||||
assert.ok(coinY(v) > coinY(plain), 'the magazine row (thicker than the SP bar) is all that sits above the purse');
|
||||
plain.destroy();
|
||||
} finally { v.destroy(); w.destroy(); }
|
||||
});
|
||||
|
||||
test('on: the exact count through spending, refills and capacity changes, also where the cells are too narrow for cuts', () => {
|
||||
const v = counting();
|
||||
try {
|
||||
for (const [left, max] of [[13, 14], [12, 14], [137, 240], [136, 240], [260, 260], [0, 260]]) {
|
||||
v.sync(sample({ flags: UF.SKILL, ammo: [left, max] }));
|
||||
frames(v);
|
||||
assert.equal(v.ammoText?.text, `${left}/${max}`);
|
||||
assert.equal(v.ammoText.visible, true);
|
||||
if (max >= 240) assert.equal(v.ammoCuts.filter((c) => c.visible).length, 0, 'a dense magazine draws no cuts, the count still reads');
|
||||
}
|
||||
const bw = barWidth(v);
|
||||
assert.ok(Math.abs(v.ammoText.position.x - (v.spFill.position.x + bw / 2)) < 1e-9, 'centred under the bar');
|
||||
assert.ok(v.ammoText.position.y > v.spFill.position.y, 'below the magazine');
|
||||
assert.equal(v.ammoText.anchor.x, 0.5);
|
||||
assert.equal(v.ammoText.style.fill, COLORS.ammo, 'in the bar\'s yellow');
|
||||
} finally { v.destroy(); }
|
||||
});
|
||||
|
||||
test('on: hidden when the skill ends, without the snapshot row, in the prep view, after death and for enemies; the rows below make room', () => {
|
||||
const v = counting();
|
||||
try {
|
||||
v.sync(sample({ flags: UF.SKILL, ammo: [5, 14], coins: [3, 10] }));
|
||||
frames(v);
|
||||
const withCount = coinY(v);
|
||||
v.sync(sample({ sp: 6, spMax: 24, coins: [3, 10] })); // the magazine is spent: the SP bar again
|
||||
frames(v);
|
||||
assert.equal(v.ammoText.visible, false, 'no stale number after the skill');
|
||||
assert.ok(withCount > coinY(v), 'the purse row moved down by the count\'s height while it showed');
|
||||
v.sync(sample({ flags: UF.SKILL, ammo: [5, 14] }));
|
||||
v.prep = true;
|
||||
frames(v);
|
||||
assert.equal(v.ammoText.visible, false, 'the prep view draws no bars');
|
||||
v.prep = false;
|
||||
frames(v);
|
||||
assert.equal(v.ammoText.visible, true);
|
||||
v.die();
|
||||
frames(v);
|
||||
assert.equal(v.ammoText.visible, false, 'a dead unit has no bars');
|
||||
} finally { v.destroy(); }
|
||||
const e = view({ id: 2, side: 'enemy', kind: 'enemy', defId: 'enemy_x' }, { settings: { damageNumbers: true, quality: 'high', ammoCount: true } });
|
||||
try {
|
||||
e.sync(sample({ hp: 500, flags: UF.SKILL, ammo: [3, 4] }));
|
||||
frames(e);
|
||||
assert.equal(e.ammoText, null, 'enemies have no magazine, so no count');
|
||||
} finally { e.destroy(); }
|
||||
});
|
||||
|
||||
test('turning the setting off takes the count away at once (the field view shares its settings object with the units)', () => {
|
||||
const v = counting();
|
||||
try {
|
||||
v.sync(sample({ flags: UF.SKILL, ammo: [9, 14] }));
|
||||
frames(v);
|
||||
assert.equal(v.ammoText.visible, true);
|
||||
v.ctx.settings.ammoCount = false;
|
||||
frames(v);
|
||||
assert.equal(v.ammoText.visible, false);
|
||||
v.ctx.settings.ammoCount = true;
|
||||
frames(v);
|
||||
assert.equal(v.ammoText.visible, true);
|
||||
} finally { v.destroy(); }
|
||||
});
|
||||
});
|
||||
|
||||
describe('UnitView: 伺夜\'s 狼影 pips', () => {
|
||||
const pack = (extra = {}) => view({ kind: 'token', defId: 'token_10028_vigil_wolf', ...extra });
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
// test/ui/ammo-count.test.js — 设置「弹药数量」 (PR #552 by @IceCodeNew, ported in 0.2.6, DESIGN §31.9): the remaining ammo
|
||||
// count under an ammo skill's bar is not part of the official HUD, so it is an option, off by default (the maintainer's
|
||||
// decision of 2026-10-13, like 「官方伤害数字」). Settings normalisation, the dialog's toggle and its hint in every language
|
||||
// pack, the field view's live setting, the diagnostics line. What the renderer draws: test/render/hud-readouts.test.js.
|
||||
// Run: node --test test/ui/ammo-count.test.js
|
||||
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { sanitizeSettings, DEFAULT_SETTINGS } from '../../public/js/ui/gameLogic.js';
|
||||
import { buildDiagnostics } from '../../public/js/diag.js';
|
||||
|
||||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
||||
const read = (p) => readFileSync(path.join(ROOT, p), 'utf8');
|
||||
const LABEL = '弹药数量';
|
||||
const HINT = '弹药类技能开启时,在弹药条下方显示剩余弹药 / 总弹药数(如 13/14)。官方界面没有这个数字。';
|
||||
|
||||
test('ammoCount is stored with the other settings: off by default and for a profile saved before it, a boolean kept, junk off', () => {
|
||||
assert.equal(DEFAULT_SETTINGS.ammoCount, false);
|
||||
assert.equal(sanitizeSettings(null).ammoCount, false);
|
||||
assert.equal(sanitizeSettings({ bgm: 0.4, officialDamageNumbers: true }).ammoCount, false, 'a profile saved before 0.2.6');
|
||||
assert.equal(sanitizeSettings({ ammoCount: true }).ammoCount, true);
|
||||
assert.equal(sanitizeSettings({ ammoCount: false }).ammoCount, false);
|
||||
for (const bad of ['true', 1, 0, null, {}, []]) assert.equal(sanitizeSettings({ ammoCount: bad }).ammoCount, false, JSON.stringify(bad));
|
||||
// independent of the damage-number switches
|
||||
assert.deepEqual(sanitizeSettings({ ammoCount: true, damageNumbers: false, officialDamageNumbers: true }), { ...sanitizeSettings({ damageNumbers: false, officialDamageNumbers: true }), ammoCount: true });
|
||||
});
|
||||
|
||||
test('the settings dialog has the toggle and a hint that the official interface has no such number, in every language pack', () => {
|
||||
const src = read('public/js/ui/settings.js');
|
||||
assert.match(src, /<\$\{Toggle\} label=\$\{t\('弹药数量'\)\} micro="AMMO COUNT" value=\$\{s\.ammoCount\} onChange=\$\{\(v\) => updateSettings\(\{ ammoCount: v \}\)\} \/>/);
|
||||
assert.ok(src.includes(`t('${HINT}')`), 'the hint under it');
|
||||
assert.ok(src.indexOf("t('官方伤害数字')") < src.indexOf(`t('${LABEL}')`), 'beside the damage-number options');
|
||||
for (const lang of ['en', 'ja', 'ko', 'zh-TW']) {
|
||||
const pack = JSON.parse(read(`public/i18n/${lang}.json`));
|
||||
for (const id of [LABEL, HINT]) assert.ok(typeof pack[id] === 'string' && pack[id].length > 0, `${lang}: ${id}`);
|
||||
assert.match(pack[HINT], /13\/14/, `${lang}: the example count`);
|
||||
}
|
||||
});
|
||||
|
||||
test('the field view takes the setting at creation and live (setSettings); the units read it from their shared settings', () => {
|
||||
const app = read('public/js/render/app.js');
|
||||
assert.match(app, /const settings = \{ damageNumbers: true, officialDamageNumbers: false, ammoCount: false, quality: 'high', \.\.\.\(opts\.settings \|\| \{\}\) \};/);
|
||||
assert.match(app, /if \(typeof s\.ammoCount === 'boolean'\) settings\.ammoCount = s\.ammoCount;/);
|
||||
const units = read('public/js/render/units.js');
|
||||
assert.match(units, /this\._updateAmmoCount\(showSp && !!ammo && this\.ctx\.settings\?\.ammoCount === true, x0 \+ bw \/ 2, rowsTop, s\)/);
|
||||
// the simplified board draws no ammo bar (its SP bar is the tuple's SP), so it has nothing to count under
|
||||
assert.doesNotMatch(read('public/js/ui/fallbackField.js'), /ammo/);
|
||||
});
|
||||
|
||||
test('the diagnostics name the option when it is on', () => {
|
||||
const state = { me: {}, room: null, match: { public: null, battle: null }, connection: { status: 'online' } };
|
||||
const line = (settings) => buildDiagnostics({ state, env: {}, settings }).split('\n').find((l) => l.startsWith('settings:'));
|
||||
assert.equal(line({ quality: 'high', damageNumbers: true, ammoCount: true }), 'settings: quality high | damage numbers | ammo count');
|
||||
assert.equal(line({ quality: 'high', damageNumbers: true, ammoCount: false }), 'settings: quality high | damage numbers');
|
||||
});
|
||||
@@ -722,7 +722,7 @@ describe('keyboard & settings', () => {
|
||||
test('sanitizeSettings', () => {
|
||||
assert.deepEqual(sanitizeSettings(null), { ...DEFAULT_SETTINGS });
|
||||
assert.deepEqual(sanitizeSettings({ bgm: 3, sfx: -1, voice: 2, muted: 'yes', damageNumbers: false, quality: 'ultra' }),
|
||||
{ bgm: 1, sfx: 0, voice: 1, voiceLang: 'cn', voiceOverrides: {}, muted: false, damageNumbers: false, officialDamageNumbers: false, quality: 'high', textSize: 'sm', keys: { ...DEFAULT_SETTINGS.keys } },
|
||||
{ bgm: 1, sfx: 0, voice: 1, voiceLang: 'cn', voiceOverrides: {}, muted: false, damageNumbers: false, officialDamageNumbers: false, ammoCount: false, quality: 'high', textSize: 'sm', keys: { ...DEFAULT_SETTINGS.keys } },
|
||||
'a saved profile without `keys` (before 0.2.0) gets the default key map (test/ui/feedback5-hotkeys.test.js)');
|
||||
assert.equal(sanitizeSettings({ bgm: 0.5 }).voice, DEFAULT_SETTINGS.voice, 'a saved profile without `voice` gets the default');
|
||||
// 语音语言 (0.2.2): 中文 by default — a profile saved before it, or any other value, plays the Chinese dub
|
||||
|
||||
Reference in new issue
Block a user