feedback7 R22-LOADOUT: the server takes each player's 潜能 / 练度 per operator and every owned operator fights at them (default 潜能 6, 精英2 Lv.60); golden: roster 49/49, bonds 46/46, fields 22/22, matches 18/18, diy 134/138 moved (+10 % 练度), standins unchanged

The owner's decision of 2026-10-08: 「调配干员里自己设置吧,默认满潜满加成」. Official: the account's potential (调度手册);
the 自持有 bonus to owned operators of the same name, initial and elite alike (activity_table cultivateEffectList).

- shared/protocol.js: room.loadout { entries, ops? } — `ops` = { [charId]: { potential?: 1–6, cultivate?: 0–3 } }
  (isLoadoutOps, ≤ 256), checkLoadoutOps (strict like checkLoadout: an operator of the 干员调配 roster or the 自选 owned
  pool — cultivationCharIds; defaults dropped, entries stored complete); absent = none set.
- server/lobby.js / net.js: checked with the entries (either refused ⇒ nothing stored), session.ops / seat.ops /
  seats[].ops, match.setLoadout(playerId, loadout, ops) in INFO_CHECK only.
- PlayerState: ops re-checked and frozen (bots none); cultivationFor (shared/potential.js cultivationOf: null for a
  补位 stand-in or a prototype 自选 pick); loadoutFor carries potential / cultivate; battleInput states `potential` /
  `cultivate` for every owned operator (the chess as itself, an owned 自选 pick) — an old seat without ops and every bot
  at 6 / 3; the 自选 summons' hand count follows the owner's potential (望's 棋子 6 below 潜能3); m.private.ops; the
  scouting UnitInfo carries potential (below 6) / cultivate.
- tools/golden.mjs: the direct-spec scenarios field the default player (owned operators at 潜能 6 / 练度 3; stand-ins
  and prototype picks neither), as PlayerState does.
- Golden: every battle with an owned operator moves — ATK / DEF / max HP ×1.1 from the first second (roster-001:
  角峰 ends at 3527 HP instead of 3115; the first differing snapshot window is 0–10 s in every moved battle); the
  matches part at round 1–8 (battle results, then the bots' boards). Unchanged: the 10 stand-in battles (a stand-in has
  no 练度) and diy-135…138 (the four prototype 自选 battles). No engine error.
- Docs: design/match.md §16 (潜能 / 练度), design/network.md (room.loadout ops).
- Tests: test/match/potential.test.js (new, 4): the ops wire / data check, battle input of a human (settings, defaults,
  stand-in none, elite shares the charId), an old seat and a bot at the defaults, spec → battle, scouting units; 自选
  owned pick vs prototype and 望's hand count; Match.setLoadout in INFO_CHECK only, a refusal changes nothing.
  test/lobby-loadout.test.js (+2): strict validation, storage beside the loadout, an older client's message, the real
  match's briefing (m.private.ops) and lock. test/match/runner.test.js / diy-shop.test.js expect the new fields.
This commit is contained in:
sganggs committed 2026-10-08 20:52:30 +08:00
1 parent ec0a2fc669
commit 96d9441ff1
23 files changed
+6889 -6528

No files matched your search

+59 -8
View File
@@ -1,6 +1,6 @@
// server/match/player/basics.js — PlayerState methods: the basics — seat flags and counts (isHumanActive,
// botControlled, deployCap, deployCount, tempEmpty), the prep that resolves a temp piece (tempDue, _putTemp: every
// write into a temp slot), the operator loadout (DESIGN §16: setLoadout, loadoutFor), the not-owned operators fielded
// write into a temp slot), the operator loadout (DESIGN §16: setLoadout, loadoutFor; 0.2.2 潜能 / 练度: cultivationFor), the not-owned operators fielded
// as their stand-ins (0.2.0 补位: setNotOwned, fieldsStandIn, fieldRecord), the deploy map on the field the
// player deploys on (Match.deployFieldOf) and the withdrawal of pieces a terrain / deploy-field change left on tiles
// they may no longer occupy (_evictIllegal), dirty.
@@ -9,9 +9,19 @@
import { PHASE } from '../../../shared/constants.js';
import { msg, dn } from '../../../shared/i18n.js';
import { checkLoadout, checkNotOwned, resolveLoadout } from '../../../shared/protocol.js';
import { checkLoadout, checkLoadoutOps, cultivationCharIds, checkNotOwned, resolveLoadout } from '../../../shared/protocol.js';
import { cultivationOf } from '../../../shared/potential.js';
import { tileKey, boardOrder } from '../board.js';
/** The charIds a player may set a potential / 练度 for, per game-data object (shared/protocol.js cultivationCharIds). */
const OPS_IDS = new WeakMap();
function opsCharIds(raw) {
if (!raw || typeof raw !== 'object') return new Set();
let ids = OPS_IDS.get(raw);
if (!ids) { ids = cultivationCharIds(raw.chess, raw.backups); OPS_IDS.set(raw, ids); }
return ids;
}
export class PlayerBasics {
get isHumanActive() { return !this.isBot && !this.left; }
/** The engine acts for this seat (AI teammate or "AI 托管"; a departed human is eliminated, so nothing is left to do). */
@@ -46,14 +56,35 @@ export class PlayerBasics {
}
/**
* Replace the operator loadout (DESIGN §16) after re-checking it against this match's data. Accepts the checked
* form `{ id: { skill, module|null } }` or raw `room.loadout` entries. Returns false (loadout unchanged) when it does
* not fit the data; bots keep the defaults.
* Replace the operator loadout (DESIGN §16) — and, when `ops` is given, the per-operator 潜能 / 练度 (0.2.2; null = none
* set, undefined = unchanged) — after re-checking them against this match's data. Accepts the checked form
* `{ id: { skill, module|null } }` or raw `room.loadout` entries, and the checked or raw `ops`. Returns false (nothing
* changes) when either does not fit the data; bots keep the defaults.
* @param {any} loadout
* @param {any} [ops]
* @returns {boolean}
*/
setLoadout(loadout) {
setLoadout(loadout, ops = undefined) {
if (this.isBot) return false;
let opsRes = null;
if (ops !== undefined) {
const raw = {};
if (ops && typeof ops === 'object' && !Array.isArray(ops)) {
for (const [id, e] of Object.entries(ops)) {
if (!e || typeof e !== 'object') continue;
const x = {};
if (Number.isInteger(e.potential)) x.potential = e.potential;
if (Number.isInteger(e.cultivate)) x.cultivate = e.cultivate;
if (Object.keys(x).length) raw[id] = x;
}
}
const ids = opsCharIds(this.gd.raw);
opsRes = checkLoadoutOps(raw, (id) => ids.has(id));
if (!opsRes || !opsRes.ok) {
this.m.log?.warn?.(`[match ${this.m.roomCode}] operator settings of ${this.playerId} ignored: ${opsRes && opsRes.detail}`);
return false;
}
}
const entries = {};
if (loadout && typeof loadout === 'object' && !Array.isArray(loadout)) {
for (const [id, e] of Object.entries(loadout)) {
@@ -72,9 +103,27 @@ export class PlayerBasics {
const out = {};
for (const [id, e] of Object.entries(res.loadout)) out[id] = Object.freeze({ skill: e.skill, module: e.module ?? null });
this.loadout = Object.freeze(out);
if (opsRes) {
const o = {};
for (const [id, e] of Object.entries(opsRes.ops)) o[id] = Object.freeze({ potential: e.potential, cultivate: e.cultivate });
this.ops = Object.freeze(o);
}
return true;
}
/**
* The potential / 练度 this player's piece of chess record `chessRecord` fights at (0.2.2; shared/potential.js
* cultivationOf over `this.ops` — defaults 潜能 6, 精英2 Lv.60): the operator the player owns — the chess fielded as
* itself, an owned 自选 pick (its charId) — or null for a 补位 stand-in and a prototype 自选 pick.
* @param {object|null} chessRecord
* @returns {{ potential: number, cultivate: number } | null}
*/
cultivationFor(chessRecord) {
if (!chessRecord || this.fieldsStandIn(chessRecord)) return null;
const rec = chessRecord.isDiy && typeof chessRecord.chessId === 'string' ? this.gd.chess(chessRecord.chessId) || chessRecord : chessRecord;
return cultivationOf(rec, this.ops);
}
/**
* The skill index / module a chess record fights with under this player's loadout (DESIGN §16) — for a chess this
* player fields as its stand-in (0.2.0 补位) the stand-in's backup selection, whatever the loadout says; for a slotted
@@ -83,10 +132,12 @@ export class PlayerBasics {
*/
loadoutFor(chessRecord) {
if (this.fieldsStandIn(chessRecord)) {
return resolveLoadout(null, this.gd.standIn(chessRecord.chessId), (id) => this.gd.standIn(id) || this.gd.chess(id));
return { ...resolveLoadout(null, this.gd.standIn(chessRecord.chessId), (id) => this.gd.standIn(id) || this.gd.chess(id)), potential: null, cultivate: null };
}
const rec = chessRecord && chessRecord.isDiy && typeof chessRecord.chessId === 'string' ? this.gd.chess(chessRecord.chessId) || chessRecord : chessRecord;
return resolveLoadout(this.loadout, rec, (id) => this.gd.chess(id));
// 0.2.2: with the operator's potential / 练度 (null for a prototype 自选 pick) — the 自选 summons' hand count reads it
const cv = this.cultivationFor(chessRecord);
return { ...resolveLoadout(this.loadout, rec, (id) => this.gd.chess(id)), potential: cv ? cv.potential : null, cultivate: cv ? cv.cultivate : null };
}
/**