diff --git a/docs/design/client.md b/docs/design/client.md index 426d4864..77546d2c 100644 --- a/docs/design/client.md +++ b/docs/design/client.md @@ -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). diff --git a/docs/history/0.2.4.md b/docs/history/0.2.4.md index 9f5c8917..c8cd6bf1 100644 --- a/docs/history/0.2.4.md +++ b/docs/history/0.2.4.md @@ -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. diff --git a/public/js/render/units.js b/public/js/render/units.js index ae4834b5..dd440bda 100644 --- a/public/js/render/units.js +++ b/public/js/render/units.js @@ -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 diff --git a/test/render/hud-readouts.test.js b/test/render/hud-readouts.test.js index 012935f4..70476b72 100644 --- a/test/render/hud-readouts.test.js +++ b/test/render/hud-readouts.test.js @@ -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] }));