feedback5 WK: 自选编队 in the browser — the 干员调配 overlay's third tab 自选编队 (5阶 ×2, 6阶 ×2 slots; the slot picker lists the kitted picks of the tier with avatar, name, class, derived bonds and a 原型 / 已持有 tag; an owned pick chooses its skill and module — 集成战略 modules left out —, a prototype shows its locked skill; sp.pref.diy, export / import, 全部清空, synced as room.diy through the shared engine, welcome.diyKitted kept), the operator on shop / reward cards, own pieces (「自选」 tag, its model on bench and board), the detail card (pick's skill and module), placement and range previews, bond popups (own and a teammate's 自选 member), the result screen; new UI strings through t() with English; the e2e fast server's SP_START_LEVEL hook

Bonds of 自选 pieces checked everywhere membership is decided (the owner's reminder of 2026-10-05): the match's counts through the player's data view (煌 in 炎 and 维多利亚, one member per slot, 绝技 elites), the sim's bond effects through def.bonds (叙拉古 on deployment, 协防 ×1.2 / elite ×1.4), the client's strip, popups, cards and detail card. Golden unchanged.
Tests: test/ui/diy-ui.test.js (15), test/match/diy-bonds.test.js (4), test/i18n.test.js pins screens/diy.js; test/ui/diy.e2e.test.js (SP_E2E=1, run once: pass).
This commit is contained in:
sganggs committed 2026-10-06 00:13:31 +08:00
1 parent e480b1cb26
commit 0def7babfd
31 files changed
+1637 -78

No files matched your search

+9 -3
View File
@@ -276,6 +276,7 @@ export async function createFieldView(host, options = {}) {
let penList = null;
let ownPen = null; // the own m.private.nextEnemies (fallback composition of a scouted teammate's pen)
let standInList = []; // the own m.private.standIns (0.2.0 补位): board pieces of these chess draw the stand-in
let diyPicks = {}; // the own m.private.diy (0.2.0 自选编队): pieces of these DIY slots draw the operator
let camBeforePen = null; // { kind, opts } the camera the pen returns to
let leader = null; // { key, view, stand, area } the round leader standing on the boss field in the prep (setLeader)
let leaderHidden = true; // shown only by the boss-field prep camera (leaderShown)
@@ -288,7 +289,7 @@ export async function createFieldView(host, options = {}) {
timeScale: () => (mode === 'battle' ? interp.rate : 1),
// (a chess fighting as its 补位 stand-in — `standInFor` — reads the stand-in's record: its attack interval)
lookupDef: (info) => (info.side === 'enemy' ? data.enemy(info.defId)
: (info.standInFor && data.standIn(info.defId)) || data.chess(info.defId) || data.token(info.defId)),
: (info.standInFor && data.standIn(info.defId)) || (info.diy && data.diy(info.defId, info.diy)) || data.chess(info.defId) || data.token(info.defId)),
crowded: () => views.size > 90,
renderer: app.renderer,
frameNo: () => frameNo,
@@ -304,7 +305,7 @@ export async function createFieldView(host, options = {}) {
timeScale: () => ctx.timeScale(),
loadLevel: () => loadLevel,
fieldRect: () => (mode === 'battle' && battleMeta ? battleMeta.rect : null),
subProfOf: (defId, info = null) => ((info && info.standInFor && data.standIn(defId)) || data.chess(defId))?.subProfessionId || null,
subProfOf: (defId, info = null) => ((info && info.standInFor && data.standIn(defId)) || (info && info.diy && data.diy(defId, info.diy)) || data.chess(defId))?.subProfessionId || null,
view: (id) => views.get(id) || null,
screenSize: size,
fieldTop: () => {
@@ -633,12 +634,16 @@ export async function createFieldView(host, options = {}) {
// 0.2.0 补位: a board piece of a chess the player does not own (m.private.standIns) is deployed as its stand-in —
// the stand-in's model; on the bench it keeps the chess's own (the hand shows the original operator)
const si = area === 'board' && chess && standInList.includes(chess.baseId || chess.chessId) ? data.standIn(piece.id) : null;
const rec = si || chess;
// 0.2.0 自选编队: a DIY slot the player filled is its operator, on the board and on the bench alike
const pick = chess && chess.isDiy ? diyPicks[chess.baseId || chess.chessId] : null;
const dr = pick ? data.diy(piece.id, pick) : null;
const rec = si || dr || chess;
return {
kind: 'op', side: 'ally', defId: piece.id,
spine: rec?.assets?.spine || rec?.charId || null, avatar: rec?.assets?.avatar || rec?.charId || null,
tier: chess?.tier || piece.tier || 1, golden: !!(piece.golden || chess?.isGolden), dir,
...(si ? { standInFor: si.standInFor } : null),
...(dr ? { diy: { charId: pick.charId, skillIndex: pick.skillIndex ?? null, uniEquipId: pick.uniEquipId ?? null } } : null),
};
}
@@ -687,6 +692,7 @@ export async function createFieldView(host, options = {}) {
};
const src = ps && typeof ps === 'object' ? ps : {};
standInList = Array.isArray(src.standIns) ? src.standIns.filter((x) => typeof x === 'string') : [];
diyPicks = src.diy && typeof src.diy === 'object' ? src.diy : {};
ownPen = Array.isArray(src.nextEnemies) ? src.nextEnemies : null;
setPenList(ownPen);
addList(src.hand, 'hand');
+18 -1
View File
@@ -2,6 +2,7 @@
import { createAssets, assets as defaultAssets } from '../../assets.js';
import { standInRecord } from '../../../../shared/standIn.js';
import { diyRecordOf } from '../../../../shared/diy.js';
function withTimeout(p, ms) {
return Promise.race([p, new Promise((resolve) => setTimeout(resolve, ms))]);
@@ -40,10 +41,26 @@ function makeData(src) {
standIns.set(c, rec);
return rec;
};
// 0.2.0 自选编队: the composed 自选 record of a DIY slot filled with a pick (shared/diy.js over chess.json + backups.json)
const diys = new Map();
const chessMap = () => {
try { return (src && typeof src.get === 'function' ? src.get('chess') : src && src.chess) || null; } catch { return null; }
};
const diy = (id, pick) => {
const c = look('chess', id);
const b = backups();
if (!c || !c.isDiy || !b || !pick || typeof pick.charId !== 'string') return null;
const key = `${id}|${pick.charId}|${pick.skillIndex ?? ''}|${pick.uniEquipId ?? ''}`;
if (diys.has(key)) return diys.get(key);
let rec;
try { rec = diyRecordOf(c, pick, { chess: chessMap(), backups: b }); } catch { rec = null; }
diys.set(key, rec);
return rec;
};
return {
chess: (id) => look('chess', id), token: (id) => look('tokens', id), item: (id) => look('items', id),
enemy: (id) => look('enemies', id), stage: (id) => look('stages', id), bond: (id) => look('bonds', id),
standIn,
standIn, diy,
};
}
+5
View File
@@ -45,5 +45,10 @@ export function renderInfo(u) {
// 0.2.0 补位: the replaced operator's charId of a chess fighting as its stand-in (UnitInfo.standInFor; spine / avatar /
// name are already the stand-in's) — the detail card and the view's data lookups (attack interval, splash FX) follow it
standInFor: typeof u.standInFor === 'string' && u.standInFor ? u.standInFor : undefined,
// 0.2.0 自选编队: the pick of a DIY slot's unit (UnitInfo.diy { charId, skillIndex, uniEquipId }; spine / avatar / name
// are already the operator's) — the detail card composes the operator from it, the data lookups follow it
diy: u.diy && typeof u.diy === 'object' && typeof u.diy.charId === 'string'
? { charId: u.diy.charId, skillIndex: Number.isInteger(u.diy.skillIndex) ? u.diy.skillIndex : null, uniEquipId: typeof u.diy.uniEquipId === 'string' ? u.diy.uniEquipId : null }
: undefined,
};
}