feedback7 UIBACK: a finished 联防 / boss-pair field publishes the capsule's own resolved, not the sum of the players' clamps — an enemy that enters one helper's half and leaks on the other's is billed to one player's total and to the other's leakedInTotal, so each player's min(total, killedInTotal + leakedInTotal) read 0 while the field read 1/1 and the teammates' m.public progress dropped to 0/1 once the field finished (found reviewing the capsule port of PR #308): validateClientResult keeps the result's field-level resolved (Battle.resolved, clamped to the field's total; junk counts as not reported), _fieldProgress publishes it for client-run and server-run fields alike, the players' own numbers are only the fallback of a result with none, and none at all stays null; SIM.md §1.3 and network.md (顶栏胶囊, result validation) say so; test/match/hud-capsule-resolved.test.js +5 (a 3-human match whose 联防 field runs the real sim with unarmed helpers: capsule 4/4 and m.public 4/4 not 0/4; the boss pair's crossing route; a server-run 联防 field and its timeline; validateClientResult's field-level rule; _fieldProgress's precedence) — all failing before; golden: unchanged (no sim change; compare: roster 49, bonds 46, fields 22, matches 18, standins 10, diy 138 match)

This commit is contained in:
sganggs committed 2026-10-08 20:40:42 +08:00
1 parent f632e0b009
commit 1a88069968
5 files changed
+186 -16

No files matched your search

+15 -3
View File
@@ -751,7 +751,11 @@ const sameMods = (a, b) => {
* flat bound when one is uncapped) and ≤ the room left under BOND_LAYER_CAP (999) from the bond's starting layers, only
* on bonds the player's lineup / band / effects / items name, none when the spec disables gains; coins ≤ the spawns' bounty coins;
* perfect consistent with the counted leaks; unit states only for the player's own units, within range. `resolved` (the
* HUD capsule's numerator) is kept as reported, clamped to the player's `total`; an absent one stays absent.
* HUD capsule's numerator): the result's own — the FIELD's, what the client's capsule showed — is kept as reported,
* clamped to the field's `total`, and is what m.public publishes (never the sum of the players': an enemy that crosses
* the midline of a two-half field is billed to one player's `total` and to the other's leak, so their own numbers
* clamp it to 0); the players' own (each clamped to their `total`) are the fallback of a result with none; absent when
* neither is complete.
* @returns {{ ok: true, result: object } | { ok: false, reason: string }}
*/
export function validateClientResult(spec, raw, { gd = null } = {}) {
@@ -888,12 +892,20 @@ export function validateClientResult(spec, raw, { gd = null } = {}) {
}
if (coinsSum > B.bountyCoins + 1e-6) return bad('coins');
const result = { time: raw.time, reason: raw.reason, perPlayer, killed: 0, total: 0, errors: Math.max(0, Math.trunc(Number(raw.errors) || 0)) };
let ownResolved = 0, ownKnown = pids.length > 0;
for (const pid of pids) {
result.killed += perPlayer[pid].killed;
result.total += perPlayer[pid].total;
result.resolved = (result.resolved ?? 0) + (Number.isInteger(perPlayer[pid].resolved) ? perPlayer[pid].resolved : 0);
if (Number.isInteger(perPlayer[pid].resolved)) ownResolved += perPlayer[pid].resolved; else ownKnown = false;
}
if (pids.some((pid) => !Number.isInteger(perPlayer[pid].resolved))) delete result.resolved;
// The FIELD's capsule numerator (Battle.resolved, what the client's own capsule showed), kept as reported and clamped
// to the field's total — NOT the sum of the players' own numbers: an enemy that spawns on one half and leaks on the
// other (the 联防 lane, the boss pair's crossing routes) is billed to one player's `total` and to the other's
// `leakedInTotal`, so each player's own min(total, …) clamps it away (0 and 0 for an enemy the field resolved). The sum
// is only the fallback of a result with no field-level number; none of either stays absent (the teammate UI then
// falls back to `killed`, never to a fabricated 0).
if (Number.isInteger(raw.resolved) && raw.resolved >= 0) result.resolved = Math.min(result.total, raw.resolved);
else if (ownKnown) result.resolved = Math.min(result.total, ownResolved);
if (spec.kind === 'unite' && Array.isArray(raw.unspawned)) {
const unspawned = [];
for (const u of raw.unspawned) {