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:
1 parent
ec0a2fc669
commit
96d9441ff1
23 files changed
+6889
-6528
No files matched your search
+58
-8
@@ -4,7 +4,7 @@
|
||||
import { DIFFICULTIES, NAME_MAX_LEN, ROOM_CODE_LEN, MAX_SEATS, EMOTES, GEO } from './constants.js';
|
||||
import { isDroppableChess } from './standIn.js';
|
||||
import { diySlotIds, validateDiyPicks } from './diy.js';
|
||||
import { cultivatedStats } from './potential.js';
|
||||
import { cultivatedStats, isPotential, isCultivate, POTENTIAL_DEFAULT, CULTIVATE_DEFAULT } from './potential.js';
|
||||
|
||||
// ---- tiny validators -------------------------------------------------------
|
||||
const isInt = (v, lo = -Infinity, hi = Infinity) => Number.isInteger(v) && v >= lo && v <= hi;
|
||||
@@ -65,12 +65,15 @@ export function isBattleResult(v) {
|
||||
// ---- operator loadout (DESIGN §16): room.loadout { entries } -------------------------------------------------
|
||||
|
||||
/**
|
||||
* `room.loadout { entries }`: `entries` = `{ [baseChessId]: { skill?: skillIndex, module?: uniEquipId | 'none' } }`
|
||||
* (the per-browser loadout of the 干员调配 screen). Structural limits below; the semantic check against the game data
|
||||
* (known visible chess, legal skill index for the normal AND the elite status, legal module of the elite) is
|
||||
* `checkLoadout` — used by the server (lobby, match) and by the client to sanitise a stored loadout before sending.
|
||||
* `room.loadout { entries, ops? }`: `entries` = `{ [baseChessId]: { skill?: skillIndex, module?: uniEquipId | 'none' } }`
|
||||
* (the per-browser loadout of the 干员调配 screen); `ops` (0.2.2) = `{ [charId]: { potential?: 1–6, cultivate?: 0–3 } }`,
|
||||
* the player's per-operator 潜能 and 练度 (shared/potential.js; absent / missing = 潜能 6, 精英2 Lv.60 — the owner's
|
||||
* decision of 2026-10-08). Structural limits below; the semantic checks against the game data — known visible chess,
|
||||
* legal skill index for the normal AND the elite status, legal module of the elite (`checkLoadout`); an operator of the
|
||||
* 干员调配 roster or the 自选 owned pool (`checkLoadoutOps`) — are used by the server (lobby, match) and by the client to
|
||||
* sanitise a stored loadout before sending.
|
||||
*/
|
||||
export const LOADOUT_LIMITS = Object.freeze({ entries: 160, skillIndex: 9 });
|
||||
export const LOADOUT_LIMITS = Object.freeze({ entries: 160, skillIndex: 9, ops: 256 });
|
||||
/** The "no module" choice of an elite (模组: 不装备). */
|
||||
export const MODULE_NONE = 'none';
|
||||
const isLoadoutEntry = (e) => isPlain(e) && Object.keys(e).length > 0 && Object.keys(e).every((k) => k === 'skill' || k === 'module')
|
||||
@@ -174,6 +177,52 @@ export function resolveLoadout(loadout, chess, getChess) {
|
||||
return { skillIndex, moduleId };
|
||||
}
|
||||
|
||||
/** One `room.loadout.ops` entry: `{ potential?: 1–6, cultivate?: 0–3 }`, at least one of them. */
|
||||
const isOpsEntry = (e) => isPlain(e) && Object.keys(e).length > 0 && Object.keys(e).every((k) => k === 'potential' || k === 'cultivate')
|
||||
&& optional(isPotential)(e.potential) && optional(isCultivate)(e.cultivate);
|
||||
/** Structural check of `room.loadout.ops` (0.2.2): a map of ≤ 256 charIds → `{ potential?, cultivate? }`. */
|
||||
export const isLoadoutOps = (v) => isMap(v, LOADOUT_LIMITS.ops, isId, isOpsEntry);
|
||||
|
||||
/**
|
||||
* The operators a player sets a potential / 练度 for (0.2.2): the charIds of the 干员调配 roster — the visible normal chess
|
||||
* (checkLoadout's targets; a hidden chess shares its visible twin's charId: 锡人, 耶拉 …) — and the owned 6★ 自选 picks
|
||||
* (data/backups.json `diy.ownedPool`). A PRESET (特许) operator the player does not own is set by hand (潜能 1, 未精英化:
|
||||
* the official 「未持有的特许按1潜」 and +0 %); a 补位 stand-in or a prototype pick takes neither.
|
||||
* @param {Record<string, any>|null|undefined} chess data/chess.json
|
||||
* @param {any} [backups] data/backups.json
|
||||
* @returns {Set<string>}
|
||||
*/
|
||||
export function cultivationCharIds(chess, backups = null) {
|
||||
const out = new Set();
|
||||
for (const c of Object.values(chess && typeof chess === 'object' ? chess : {})) {
|
||||
if (c && !c.isGolden && !c.isDiy && c.visible !== false && !c.isHidden && (!c.baseId || c.baseId === c.chessId) && isId(c.charId)) out.add(c.charId);
|
||||
}
|
||||
for (const id of Array.isArray(backups?.diy?.ownedPool) ? backups.diy.ownedPool : []) if (isId(id)) out.add(id);
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Semantic check + normalisation of `room.loadout.ops` (0.2.2) — strict like checkLoadout: an operator `isOperator` does
|
||||
* not know (cultivationCharIds) rejects the whole message. Entries equal to the defaults (潜能 6, 练度 3) are dropped, the
|
||||
* rest stored complete `{ potential, cultivate }`. `undefined` / `null` = no settings (`{}`).
|
||||
* @param {any} ops
|
||||
* @param {(charId: string) => boolean} isOperator
|
||||
* @returns {{ ok: true, ops: Record<string, { potential: number, cultivate: number }> } | { error: 'BAD_MSG'|'BAD_TARGET', detail: string }}
|
||||
*/
|
||||
export function checkLoadoutOps(ops, isOperator) {
|
||||
if (ops == null) return { ok: true, ops: {} };
|
||||
if (!isLoadoutOps(ops)) return { error: 'BAD_MSG', detail: 'bad operator settings' };
|
||||
const out = {};
|
||||
for (const id of Object.keys(ops)) {
|
||||
if (typeof isOperator !== 'function' || !isOperator(id)) return { error: 'BAD_TARGET', detail: `unknown operator ${id}` };
|
||||
const potential = ops[id].potential ?? POTENTIAL_DEFAULT;
|
||||
const cultivate = ops[id].cultivate ?? CULTIVATE_DEFAULT;
|
||||
if (potential === POTENTIAL_DEFAULT && cultivate === CULTIVATE_DEFAULT) continue;
|
||||
out[id] = { potential, cultivate };
|
||||
}
|
||||
return { ok: true, ops: out };
|
||||
}
|
||||
|
||||
// ---- operator ownership (干员持有, 0.2.0 补位): room.ownership { notOwned } -------------------------------------------
|
||||
|
||||
/**
|
||||
@@ -334,8 +383,9 @@ export const C2S = {
|
||||
// host confirmed — a seat that changed hands meanwhile is refused
|
||||
'room.kick': { seat: (v) => isInt(v, 0, MAX_SEATS - 1), playerId: isId },
|
||||
'room.start': {},
|
||||
// operator loadout (DESIGN §16): stored per session/seat; accepted until the match leaves INFO_CHECK
|
||||
'room.loadout': { entries: isLoadoutEntries },
|
||||
// operator loadout (DESIGN §16): stored per session/seat; accepted until the match leaves INFO_CHECK — `ops` (0.2.2):
|
||||
// the per-operator 潜能 / 练度 (absent = none set: every operator at 潜能 6, 精英2 Lv.60)
|
||||
'room.loadout': { entries: isLoadoutEntries, ops: isLoadoutOps, $optional: ['ops'] },
|
||||
// operator ownership (干员持有, 0.2.0 补位): stored per session / seat; a match takes the list its seat had when it
|
||||
// started (an out-of-match setting — during a match it is stored for the next one: ROOM_STARTED)
|
||||
'room.ownership': { notOwned: isNotOwnedList },
|
||||
|
||||
Reference in new issue
Block a user