fix(render): show exact remaining ammunition

Reference: lolink’s numeric ammunition display in https://github.com/Yilegendoflink/garrison-protocol/commit/071ed14aa27d5ebf54e57447c958f2681737403c. This implementation uses the existing PIXI HUD and exact snapshot counts; no fork code is copied.
This commit is contained in:
IceCodeNew committed 2026-10-12 09:55:51 +00:00
1 parent bc50cac01f
commit 02fd42abe5
4 files changed
+57 -1

No files matched your search

+5
View File
@@ -40,6 +40,11 @@ Visuals: oblique perspective board (tilted ~30°, trapezoid tiles), raised highl
## 10. Client UI (screens & components)
Active ammunition skills also show `remaining/capacity` below the segmented skill bar, using the exact `b.snap ammo`
integers, including extra rounds and refills. The label stays readable when cells are too narrow to separate. It hides
with the ammunition row, during preparation and after death; elemental gauges sit below it. This is a display-only
readout, not a change to ammunition consumption or skill duration.
Look & feel: research 06 §11–§12 (near-black green-grey panels, mint `#4ed8af` accents, gold prices, bracket frames, hexagon badges, bond discs, 7-segment countdown). Fonts: Noto Sans SC (Google Fonts with local fallback), Bender/Novecento self-hosted for numerals and Latin micro-labels.
Screens: **Title** (season art backdrop, 开始 → nickname), **Lobby** (独立模拟 / 同盟模拟 → create or join; recent rooms; a 设置 button left of 玩法说明 opens the title screen's settings dialog, GitHub #238), **Room** (4 seats, invite code + copy link, difficulty picker (host), the co-op 「AI 队友最后选择」 switch (host, off by default; read-only for the others), add/remove AI, ready, start; the same 设置 button), **Briefing** (INFO_CHECK: stage, factions, disabled bonds, banned chess, boss silhouette, ready x/4), **Band draft** (grid of 40 bands with icon/name/LP/effect; turn order list; skip; 查看禁用盟约与干员 under the order list opens the briefing's bond rows, legend and banned operators again in a read-only dialog — one component, `ui/matchInfo.js`, §21.28), **Game** (Pixi field + DOM HUD: top bar with round/phase capsule (the kill counter reads 已解决 / the round's own enemies: a leak counts, a split child or summon is in neither number, §14)/LP (live during the own battle, §17.5)/timer/ready, and beside the latency a 观战席 capsule (eye + count, a roster with the host's ✕) while the room has spectators (GitHub #120); bond strip + popups; left team panel with LP/status/emote bubbles, click to watch (a boss round frames the viewer's pair in green, §15); bottom shop bar (level card, 3–5 chess cards — a slot the upgrade just opened is a bare frame until the next refresh, GitHub #332 —, item card, funds, refresh, freeze, collapse; 准备就绪 folds it and cancelling unfolds it, GitHub #138); detail panel on click/hover (header with bonds, then 特质 — the operator's own effect — right under it, 特性, stats (live: the battle's own numbers / the start-of-battle ones in prep, §18.5), skill, module, equipped items, talents: §17.5); enemy preview drawer (next round enemies with counts, factions; its 本局信息 tab lists the briefing's disabled bonds and banned operators, §21.28); 机变 overlay (the official card: framed icon + name, the effect text — data `descRaw` — under it, the taker's round avatar at the top-right; two taps, §18.2, §20.7); merge reward overlay (the promotion reward, or a special offer — 凯瑟琳's 定向投放 … — as item / chess cards under its source's label, with the count of offers queued behind it, §21.2); phase banners; the round's result box at SETTLE (the official 「作战结束」 + 「全员无伤!」 / 「生命值减少 −N」 for the viewer's own LP — after a 联防 the server's charge, `m.public.uniteResult.losses` — with the matching BATTLEOVER sound; none in the boss rounds or for a viewer not in the round; GitHub #235, PR #112); ticker (by broadcast priority, highest first; a leader-damage line plays only in its round and a player's newer one replaces the older, §21.10); toasts; emote wheel; settings (volume, quality, show damage numbers)), **Result** (victory/defeat, rounds, per-player lineup, stats, titles, back to room).
+7
View File
@@ -274,3 +274,10 @@ enemy: picking emitted a unit click with no definition, instead of the tile clic
engine-canvas regression retains the card/100 HP assertions and also covers another field refresh, live HP loss and
closing the card when the crate is destroyed. Device metadata supplied by the new entry still wins; changing fields
does not carry old devices across. Gameplay and allied targeting rules are unchanged.
### 29.15 Ammunition readout
Active ammunition skills show remaining rounds and magazine capacity below the segmented bar (§10), including
Laterano's extra rounds and refills. The renderer reads the existing exact snapshot counts rather than estimating
them from SP. Missing ammunition snapshots, preparation and death hide the number. Dense magazines retain their
numeric readout when the renderer omits cell separators. Combat rules and golden results are unchanged.
+19 -1
View File
@@ -760,6 +760,7 @@ export class UnitView {
this.blockIcon.visible = false;
this.ctx.layers.groundFx.addChild(this.blockIcon);
this.countText = null;
this.ammoText = null;
this.itemPips = [];
}
@@ -1413,7 +1414,8 @@ export class UnitView {
}
// 狼影 pips under the bars (b.snap `wolves`), then the element gauge row under them (b.snap `elem`); redeploy ring above a
// knocked-down operator (b.snap `down`)
const rowsTop = cy + bh / 2 + (showSp ? spH + 1.5 : 0);
let rowsTop = cy + bh / 2 + (showSp ? spH + 1.5 : 0);
rowsTop += this._updateAmmoText(showSp && !!ammo, x0 + bw / 2, rowsTop, s);
const wolfH = this._updateWolfPips(showBars && !this.isEnemy && !!this.wolves, x0, rowsTop, s);
const coinH = this._updateCoins(showBars && !this.isEnemy && !!this.coins, x0, rowsTop + wolfH, s);
this._updateElementBar(showBars && !!this.el, x0, bw, rowsTop + wolfH + coinH + 1, spH, s, t);
@@ -1441,6 +1443,22 @@ export class UnitView {
}
}
/** Exact snapshot counts, including extra rounds and refills; readable even when the cells are too narrow. */
_updateAmmoText(show, x, top, s) {
let text = this.ammoText;
if (!show) { if (text) text.visible = false; return 0; }
if (!text) {
text = this.ammoText = new this.P.Text('', { fontFamily: 'Bender, Oxanium, sans-serif', fontSize: 16, fontWeight: '700', fill: COLORS.ammo, stroke: '#0b0f0e', strokeThickness: 3 });
text.anchor.set(0.5, 0);
this.hud.addChild(text);
}
text.text = `${this.ammo[0]}/${this.ammo[1]}`;
text.scale.set(clamp(s * 0.2, 10, 16) / 16);
text.position.set(x, top + 3);
text.visible = true;
return text.height + 6;
}
/**
* The thin dark separators between the ammo bar's cells (see _updateHud): one at every round boundary of the `n`-round
* magazine across the bar `x0` … `x0 + bw` at height `y`, `h` tall; none while a cell would be under AMMO_BAR.minCell px
+26
View File
@@ -96,6 +96,32 @@ describe('interp: ammo / wolves / neg', () => {
});
describe('UnitView: the ammo skill\'s segmented bar', () => {
test('user sees exact ammunition through spending, refills and skill end, even without cell separators', () => {
// Given a magazine too dense to count its cells, When snapshots change, Then the readout stays exact.
const v = view();
try {
for (const [left, max] of [[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);
assert.equal(v.ammoCuts.filter((c) => c.visible).length, 0);
}
// When the skill ends or an older peer omits the row, Then no stale number remains.
v.sync(sample({ sp: 6, spMax: 24 }));
frames(v);
assert.equal(v.ammoText.visible, false);
v.sync(sample({ flags: UF.SKILL, ammo: [5, 14] }));
v.prep = true;
frames(v);
assert.equal(v.ammoText.visible, false);
v.prep = false;
v.die();
frames(v);
assert.equal(v.ammoText.visible, false);
} finally { v.destroy(); }
});
test('replaces the SP bar while the magazine runs: a yellow cell per round, the fill is rounds left / magazine, a thin cut at every boundary', () => {
const v = view();
v.sync(sample({ flags: UF.SKILL, sp: 22.3, spMax: 24, ammo: [13, 14] }));