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
@@ -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 };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
import { KITTED_CHARS } from '../../sim/content/kits/index.js';
|
||||
import { checkDiyPicks } from '../../../shared/protocol.js';
|
||||
import { diyRecord, diySlot, diyTokenOwner } from '../../../shared/diy.js';
|
||||
import { atPotential } from '../../../shared/potential.js';
|
||||
|
||||
const posIntOr = (v, d) => (Number.isInteger(v) && v > 0 ? v : d);
|
||||
const isObj = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
||||
@@ -102,7 +103,8 @@ export function diyGameData(gd, records) {
|
||||
* placeable, by the variant of the owner form (`bySkill[skillIndex]` sources) — the deploy limit as the count (PRTS
|
||||
* 卫戍协议/帮助 "根据召唤物部署数量上限(非初始持有量)"), the active module's own when its variant has one (`byModule`:
|
||||
* 望's TRP-X "可同时部署的陷阱数量提升", 6 → 7 棋子; SUM-Y stage 2+ 4 drones / summons). The data's deploy limit holds the
|
||||
* token's own talent additions (tools/build-data.mjs tokenTalentDeckBonus, 0.2.0): 麦哲伦 / 令 / 电弧 3, 白铁 2, 夜莺 3 幻影.
|
||||
* token's own talent additions (tools/build-data.mjs tokenTalentDeckBonus, 0.2.0): 麦哲伦 / 令 / 电弧 3, 白铁 2, 夜莺 3 幻影 —
|
||||
* at the owner's potential (`loadout.potential`, PlayerState.loadoutFor; 0.2.2: 望's 棋子 6 below 潜能3, 7 from it).
|
||||
*/
|
||||
placeableTokens: {
|
||||
value: (chessId, loadout = null) => {
|
||||
@@ -113,7 +115,7 @@ export function diyGameData(gd, records) {
|
||||
for (const tid of Array.isArray(rec.tokens) ? rec.tokens : []) {
|
||||
const t = tokenOf(tid);
|
||||
if (!t || t.kind !== 'summon' || t.placeable !== true) continue;
|
||||
const v = isObj(t.variants) ? t.variants[owner] ?? null : null;
|
||||
const v = isObj(t.variants) ? atPotential(t.variants[owner] ?? null, loadout?.potential) : null;
|
||||
if (v) {
|
||||
const alt = loadout && Number.isInteger(loadout.skillIndex) && v.bySkill ? v.bySkill[loadout.skillIndex] : null;
|
||||
const src = Array.isArray(alt?.sources) ? alt.sources : Array.isArray(v.sources) ? v.sources : [];
|
||||
|
||||
@@ -118,6 +118,10 @@ export class PlayerRound {
|
||||
u.skillIndex = lo.skillIndex;
|
||||
u.moduleId = lo.moduleId;
|
||||
}
|
||||
// 0.2.2: the potential / 练度 of an operator the player owns (the chess as itself, an owned 自选 pick — the player's
|
||||
// 干员调配 settings, 潜能 6 / 精英2 Lv.60 by default and for bots; never a stand-in's nor a prototype pick's)
|
||||
const cv = u.standIn ? null : this.cultivationFor(this.gd.chess(piece.id));
|
||||
if (cv) { u.potential = cv.potential; u.cultivate = cv.cultivate; }
|
||||
if (carry && carry.has(piece.uid)) u.carryState = carry.get(piece.uid);
|
||||
units.push(u);
|
||||
} else if (piece.kind === 'token') {
|
||||
|
||||
@@ -90,6 +90,9 @@ export class PlayerViews {
|
||||
nextEnemies: this.m.nextEnemiesFor(this),
|
||||
// DESIGN §16: the effective operator loadout ({ [baseChessId]: { skill, module } }; chess not listed use defaults)
|
||||
loadout: this.loadout,
|
||||
// 0.2.2: the effective per-operator 潜能 / 练度 ({ [charId]: { potential, cultivate } }; operators not listed: 潜能 6,
|
||||
// 精英2 Lv.60) — the client's cards compose the numbers from it (shared/potential.js)
|
||||
ops: this.ops,
|
||||
// 0.2.0 补位: the base chess ids this player fields as their stand-ins in this match (the not-owned list the seat had
|
||||
// at the match start; [] = every operator owned) — the client shows these as their stand-ins (cards, pieces, the
|
||||
// detail card, with a small 「替补」 mark)
|
||||
|
||||
Reference in new issue
Block a user