feedback7: merge fb7-ui-backlog — #229 device tooltips, #238 settings button in lobby / room, #138 准备 folds the shop, #120 in-match spectator roster (cap stays 2), PR #308 capsule counts (+ the shared-field progress fix), PR #333 empty new shop slot on 调度中心 upgrade

- Conflicts: the stats entry (merged fb7-stats) and the settings button both sit in the lobby / room top bars — both kept;
  public/js/ui/settings.js (#321 diagnostics, voiceLang, SettingsButton); docs/PLAYING.md table rows resolved line by
  line against the merge base (#368, #354, #333 edits kept); docs/SIM.md / design/network.md snapshot field lists
  (resolved + ammo / wolves / neg / stand / standCut); design/client.md Screens line (#368's AI switch kept).
- Grok review: MERGE AFTER FIXES — the shared-field capsule finding fixed on the branch (1a880699).
- Golden: roster 23, bonds 2, fields 11, matches 18, diy 11 — #308 totals / snapshots and #333's shop RNG (union OK);
  broad node --test 5473 (5472 pass, 0 fail, 1 skipped); lint 0 errors; typecheck; i18n strict clean.
This commit is contained in:
sganggs committed 2026-10-08 20:56:20 +08:00
commit dc609ea13f
71 files changed
+3016 -806

No files matched your search

+4 -2
View File
@@ -100,7 +100,7 @@ export function followedScout({ field = null, watching = null, alive = true, spe
return fid;
}
/** Teammates' progress for the waiting pill: [{ playerId, name, killed, total, done, isBot }]. */
/** Teammates' progress for the waiting pill: [{ playerId, name, killed, resolved, total, done, isBot }]. */
export function teammateProgress(pub, myId) {
const out = [];
for (const f of fields(pub)) {
@@ -111,7 +111,9 @@ export function teammateProgress(pub, myId) {
const pr = isObj(f.progress) ? f.progress : null;
out.push({
playerId: pid, name: p?.name || t('队友'), isBot: !!p?.isBot,
killed: Number.isFinite(pr?.killed) ? pr.killed : null, total: Number.isFinite(pr?.total) ? pr.total : null,
killed: Number.isFinite(pr?.killed) ? pr.killed : null,
resolved: Number.isFinite(pr?.resolved) ? pr.resolved : (Number.isFinite(pr?.killed) ? pr.killed : null),
total: Number.isFinite(pr?.total) ? pr.total : null,
done: f.live === false || !!pr?.done,
});
}
+13 -1
View File
@@ -120,6 +120,14 @@ export const LOST_RESULT_CODES = Object.freeze(['DISCONNECTED', 'OFFLINE', 'TIME
/** Ticks per frame at a speed (same cap as the server pacing: server/match/fields.js maxTicksPerInterval). */
export const ticksPerFrameCap = (speed) => Math.max(8, Math.ceil((Number(speed) || 2) * 4));
/**
* What the runner compares between two looks at a 联防 battle (noteUniteLeft) before it reads the leakers' enemies still
* standing again: a knock-out, a leak, a spawn, the end. `total` stands for the spawns no longer — a split child or a summon
* is outside the capsule's denominator (DESIGN §14 顶栏胶囊) — so the length of the enemy list does.
* @param {any} b the battle
*/
export const uniteLeftMark = (b) => `${Number(b.killed) || 0}:${Number(b.leakedCount) || 0}:${Number(b.total) || 0}:${Array.isArray(b.enemies) ? b.enemies.length : 0}:${b.finished ? 1 : 0}`;
function deepFreeze(root) {
const stack = [root];
while (stack.length) {
@@ -292,7 +300,7 @@ export function createBattleRunner(deps) {
function noteUniteLeft(e) {
const b = e.battle;
const mark = `${Number(b.killed) || 0}:${Number(b.leakedCount) || 0}:${Number(b.total) || 0}:${b.finished ? 1 : 0}`;
const mark = uniteLeftMark(b);
if (mark === e.leakMark) return;
e.leakMark = mark;
let left = e.left;
@@ -433,6 +441,10 @@ export function createBattleRunner(deps) {
e.lastProgressAt = t;
const p = e.sim.spec.battleProgress(e.battle);
const msg = { battleId: e.battleId, gt: Math.min(1e5, p.gt), killed: Math.min(p.killed, p.total), total: Math.min(1e5, p.total), done: !!p.done };
// the HUD capsule's numerator of this field (shared/protocol.js b.progress `resolved`): the field's own scheduled
// enemies knocked out or leaked. Sent only when the battle reports one (Battle.resolved) — an absent field leaves
// the teammate UI on its `resolved ?? killed` fallback
if (Number.isFinite(p.resolved)) msg.resolved = Math.max(0, Math.min(msg.total, p.resolved));
if (bossLike(e)) {
const pool = e.battle.sharedBoss;
msg.leaks = Math.min(1e6, e.meter.lp);