feedback5 WE2: the briefing and the 本局信息 views name a 自选 piece the match leaves out of the shop (#11)

m.private.diyBanned (every bond of the operator off this match) is shown under 本局禁用干员
in the INFO_CHECK briefing, the strategy draft's 本局信息 dialog and the in-game 本局信息
tab: ui/gameLogic/diy.js diyBannedPieces, ui/matchInfo.js matchInfoModel diyBanned +
DiyBannedLine (t() with English in public/i18n/en.json). Client only; golden unchanged.
This commit is contained in:
sganggs committed 2026-10-06 15:48:32 +08:00
1 parent 7b83028139
commit d31de31e31
13 files changed
+146 -17

No files matched your search

+4
View File
@@ -89,6 +89,10 @@
.brief-banned__n { margin-left: auto; font-size: .22rem; color: var(--red-premium); }
.brief-banned__grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(.6rem, 1fr)); gap: .08rem; }
.brief-banned__grid .uthumb { --ut: .6rem; }
/* 0.2.0: the player's own 自选 pieces out of the shop this match (ui/matchInfo.js DiyBannedLine; m.private.diyBanned) */
.brief-banned__diy { margin: .1rem 0 0; font-size: .14rem; line-height: 1.5; color: var(--text-dim, #b8c2bf); }
.diybanned__tag { display: inline-block; margin-right: .06rem; padding: .01rem .06rem; font-size: .12rem; font-weight: 700;
letter-spacing: .08em; background: var(--mint-500); color: var(--text-on-mint); vertical-align: .01rem; }
.brief__foot { position: relative; z-index: 2; flex: none; display: flex; align-items: center; justify-content: flex-end; gap: .4rem; padding: .18rem .5rem .3rem; }
.brief-ready { display: flex; align-items: center; gap: .16rem; }
+1
View File
@@ -130,4 +130,5 @@
.minfo-dlg .brief-banned__n { font-size: max(.22rem, 13px); }
.minfo-dlg .brief-banned__grid { grid-template-columns: repeat(auto-fill, minmax(max(.6rem, 34px), 1fr)); gap: max(.08rem, 3px); }
.minfo-dlg .brief-banned__grid .uthumb { --ut: max(.6rem, 34px); }
.minfo-dlg .brief-banned__diy { font-size: max(.14rem, 12px); }
.minfo-dlg .t-dim { font-size: max(.16rem, 10px); }
+4
View File
@@ -370,6 +370,10 @@ button.bpop__harmony:hover { border-style: solid; color: #fff; }
.ibond__ban .icon { width: .12rem; height: .12rem; }
.ibanned { display: flex; flex-wrap: wrap; gap: .06rem; }
.ibanned__one { padding: 0; background: none; border: 0; }
/* 0.2.0: the player's own 自选 pieces out of the shop this match (ui/matchInfo.js DiyBannedLine) */
.idiybanned { margin: .08rem 0 0; font-size: .13rem; line-height: 1.5; }
.idiybanned .diybanned__tag { display: inline-block; margin-right: .05rem; padding: 0 .05rem; font-size: .11rem; font-weight: 700;
background: var(--mint-500); color: var(--text-on-mint); }
/* ---- 机变 overlay ---- */
.spov { position: absolute; inset: 0 0 0 2.8rem; z-index: 20; }
+1
View File
@@ -878,6 +878,7 @@
"阶": "Tier",
"自选编队:{name}(只在你的商店出现)": "Custom Squad: {name} (only in your shop)",
"自选": "Custom",
"{names}的盟约本局全部禁用,不会出现在你的商店": "Every Alliance of {names} is disabled this match: not in your shop",
"自选编队:所选技能与模组,没有特质,盟约按所属阵营分配": "Custom Squad: the chosen skill and module, no Attribute, Alliances by its factions",
"自选编队:{name}": "Custom Squad: {name}",
"替补": "Stand-in",
+2 -1
View File
@@ -26,6 +26,7 @@ import { MatchInfoDialog, matchInfoModel } from '../ui/matchInfo.js';
import { actions, act } from '../ui/gameActions.js';
import { normalizeDraft, sortedPlayers } from '../ui/gameLogic.js';
import { useStore } from '../store.js';
import { data } from '../data.js';
import { audio } from '../audio.js';
import { modeOffBonds, bandOffBonds, bandOffLine } from '../ui/gameLogic.js';
@@ -242,7 +243,7 @@ export function BandDraftScreen() {
const turnSecs = clock ? secondsLeft(clock.deadline) : null;
const turnLen = Number(pub?.draft?.turnSeconds) > 0 ? Math.round(pub.draft.turnSeconds) : null;
// 本局信息: the briefing's blocks (built only while the dialog is open) and the draft's state under it
const info = infoOpen ? matchInfoModel(pub, { bonds: gd.list('bonds'), chess: gd.chess, mode }) : null;
const info = infoOpen ? matchInfoModel(pub, { bonds: gd.list('bonds'), chess: gd.chess, mode, priv, diyData: { chess: data.get('chess'), backups: data.get('backups') } }) : null;
const infoStatus = infoOpen ? draftInfoStatus({ myPick, pickName: myPick ? gd.band(myPick)?.name : null, myTurn, turnName, secs: turnSecs,
waiting: !solo && !draft.done }) : null;
+6 -2
View File
@@ -2,7 +2,8 @@
// abilities), stage (+ its pool: 战场固定 / 战场随机(共N张)), 特训敌人 factions (icon, name, description), the difficulty
// tag, the ready count x/N with person pips, the 准备就绪 button (g.infoReady) and the countdown. The right column — 核心盟约
// / 附加盟约 rows of bond discs (disabled bonds greyed with the banned-member badge), the legend and the 本局禁用干员
// avatars — is ui/matchInfo.js MatchInfo, the same blocks the strategy draft's 本局信息 dialog shows (screens/bandDraft.js).
// avatars, with the player's own 自选 pieces out of the shop (m.private.diyBanned) — is ui/matchInfo.js MatchInfo, the same
// blocks the strategy draft's 本局信息 dialog shows (screens/bandDraft.js).
import { useState } from '../../vendor/hooks.module.js';
import { html, Button, Icon, MicroLabel } from '../ui/components.js';
@@ -14,12 +15,14 @@ import { actions } from '../ui/gameActions.js';
import { factionTypes, sortedPlayers, phaseTotalSeconds } from '../ui/gameLogic.js';
import { enemyIconUrl, factionIconUrl } from '../ui/assetUrls.js';
import { useStore } from '../store.js';
import { data } from '../data.js';
const cx = (...p) => p.flat().filter(Boolean).join(' ');
/** INFO_CHECK screen. */
export function BriefingScreen() {
const pub = useStore((s) => s.match.public);
const priv = useStore((s) => s.match.private);
const myId = useStore((s) => s.me.playerId);
const solo = useStore((s) => s.room?.mode === 'solo') || String(pub?.modeId || '').includes('single');
const gd = useGameData();
@@ -31,7 +34,8 @@ export function BriefingScreen() {
const me = players.find((p) => p.playerId === myId);
const readyN = players.filter((p) => p.ready).length;
const mode = gd.config?.modes?.[pub.modeId];
const info = matchInfoModel(pub, { bonds: gd.list('bonds'), chess: gd.chess, mode });
// (+ the own 自选 pieces this match leaves out of the shop: m.private.diyBanned)
const info = matchInfoModel(pub, { bonds: gd.list('bonds'), chess: gd.chess, mode, priv, diyData: { chess: data.get('chess'), backups: data.get('backups') } });
const boss = pub.bossId ? gd.boss(pub.bossId) : null;
const bossEnemy = boss ? gd.enemy(boss.enemyKey) : null;
const stage = pub.stageId ? gd.stage(pub.stageId) : null;
+6 -2
View File
@@ -10,7 +10,7 @@
import { html, Icon, Tabs, MicroLabel } from './components.js';
import { Img, UnitThumb, BondGlyph, BandIcon, RichText, GIcon } from './gameComponents.js';
import { groupEnemies, factionTypes, briefingBondTip } from './gameLogic.js';
import { matchInfoModel } from './matchInfo.js';
import { matchInfoModel, DiyBannedLine } from './matchInfo.js';
import { factionIconUrl } from './assetUrls.js';
import { data } from '../data.js';
@@ -60,9 +60,12 @@ function EnemiesTab({ pub, priv, onEnemy }) {
}
function InfoTab({ pub, priv, onChess, bandId = null, bandOwner = null }) {
const { bonds, banned, perBond, stateOf } = matchInfoModel(pub, {
const model = matchInfoModel(pub, {
bonds: data.list('bonds'), chess: (id) => data.lookup('chess', id), mode: data.get('config')?.modes?.[pub?.modeId],
// the own 自选 pieces this match leaves out of the shop (m.private.diyBanned) — not while scouting a teammate
priv: bandOwner ? null : priv, diyData: { chess: data.get('chess'), backups: data.get('backups') },
});
const { bonds, banned, perBond, stateOf } = model;
const disabled = new Set(bonds.filter((b) => stateOf(b.bondId)).map((b) => b.bondId));
// while scouting a teammate's prep board the drawer shows THEIR 策略 in place of one's own (user playtest #2 item 2)
const band = (bandId || priv?.bandId) ? data.lookup('bands', bandId || priv.bandId) : null;
@@ -85,6 +88,7 @@ function InfoTab({ pub, priv, onChess, bandId = null, bandOwner = null }) {
</div>
${banned.length ? html`<div class="ibanned">${banned.map((id) => html`<button key=${id} type="button" class="ibanned__one" onClick=${() => onChess(id)}>
<${UnitThumb} kind="chess" id=${id} size="sm" dim=${true} /></button>`)}</div>` : null}
<${DiyBannedLine} model=${model} class="idiybanned" />
</div>`;
}
+1 -1
View File
@@ -42,5 +42,5 @@ export { DEFAULT_SETTINGS, sanitizeSettings } from './gameLogic/settings.js';
export { normalizeResult } from './gameLogic/result.js';
export { chessLoadout, unitLoadout } from './gameLogic/loadout.js';
export { standInIds, fieldsStandIn, standInOf, ownStandIn, cardStandIn, memberStandIn, standInGetter, standInLoadout, deployedRecord, deployedModuleId, standInLabel, standInForText, standInTip } from './gameLogic/standIn.js';
export { diyPicks, ownDiyPick, diyRecordFor, ownDiyRecord, cardDiy, diyGetter, pickGetter, isDiyRecord } from './gameLogic/diy.js';
export { diyPicks, ownDiyPick, diyRecordFor, ownDiyRecord, cardDiy, diyGetter, pickGetter, isDiyRecord, diyBannedPieces } from './gameLogic/diy.js';
export { panelSide, panelSlots, PANEL_RIGHT_GAP, PANEL_RIGHT_BOTTOM, PANEL_RIGHT_BOTTOM_SHOP, BPOP, bondPopupPlace } from './gameLogic/panel.js';
+20
View File
@@ -92,3 +92,23 @@ export function pickGetter(getChess, pick, data) {
/** Whether a record is a composed 自选 record. */
export const isDiyRecord = (rec) => isObj(rec) && typeof rec.diyFor === 'string';
/**
* The player's slotted 自选 pieces this match leaves out of the shop because every bond of the operator is switched off
* (m.private.diyBanned — the server's initDiyStock: no stock), in slot order: [{ slotId, charId, name }] (the operator's
* name from its composed record; the charId when the data lacks it). [] without any.
* @param {any} priv m.private @param {(id: string) => any} getChess @param {{ chess?: any, backups?: any }} data
*/
export function diyBannedPieces(priv, getChess, data) {
const ids = Array.isArray(priv?.diyBanned) ? priv.diyBanned.filter((id) => typeof id === 'string') : [];
const picks = diyPicks(priv);
const out = [];
for (const slotId of ids) {
const pick = picks[slotId];
if (!isObj(pick) || typeof pick.charId !== 'string') continue;
const chess = getChess(slotId);
const rec = chess ? diyRecordFor(chess, pick, data) : null;
out.push({ slotId, charId: pick.charId, name: rec?.name || pick.charId });
}
return out;
}
+26 -7
View File
@@ -8,8 +8,10 @@
// (gameLogic disabledBondSets over config.modes[modeId].inactiveBondIds — the drawn set D "部分盟约所含干员阵容不完整",
// still activatable through other bonds' operators or items, and the mode's static inactive bonds "本局禁用", e.g.
// 标准模拟's 10), every bond in the briefing order (bondOrder, then identifier) split into 核心 / 附加, the banned
// operators (m.public bannedChess, known chess only) sorted by tier, and the banned-member count per bond (gameLogic
// bannedPerBond). The blocks are hookless (unit-tested by calling them): the 核心盟约 / 附加盟约 rows of bond discs —
// operators (m.public bannedChess, known chess only) sorted by tier, the banned-member count per bond (gameLogic
// bannedPerBond) and — from the viewer's own m.private — its slotted 自选 pieces left out of the shop because all their
// bonds are off (gameLogic diyBannedPieces; 0.2.0). The blocks are hookless (unit-tested by calling them): the 核心盟约 /
// 附加盟约 rows of bond discs —
// greyed with the ✕, the red banned-member badge, the briefingBondTip tooltip — the legend and the 本局禁用干员 grid.
// MatchInfoDialog shows the same blocks read-only in a components.js Modal (关闭, a tap outside or Esc close it) with a
// status line from its caller (the draft's turn and countdown, so the running clock stays in view).
@@ -17,27 +19,31 @@
import { html, Button, Icon, MicroLabel, BondDisc, Tooltip, Modal } from './components.js';
import { UnitThumb } from './gameComponents.js';
import { bannedPerBond, disabledBondSets, briefingBondTip } from './gameLogic.js';
import { bannedPerBond, disabledBondSets, briefingBondTip, diyBannedPieces } from './gameLogic.js';
import { bondIconUrl } from './assetUrls.js';
import { data } from '../data.js';
import { t, dn } from '../../../shared/i18n.js';
const cx = (...p) => p.flat().filter(Boolean).join(' ');
/**
* @typedef {{ sets: { drawn: Set<string>, off: Set<string> }, stateOf: (bondId: string) => 'off'|'drawn'|null,
* bonds: any[], core: any[], addon: any[], banned: string[], perBond: Map<string, number> }} MatchInfoModel
* bonds: any[], core: any[], addon: any[], banned: string[], perBond: Map<string, number>,
* diyBanned: Array<{ slotId: string, charId: string, name: string }> }} MatchInfoModel
*/
/**
* Everything the match-info blocks show.
* @param {any} pub m.public (drawnDisabledBonds / disabledBonds, bannedChess)
* @param {{ bonds?: any[], chess?: (id: string) => any, mode?: any }} [src]
* bonds: bonds.json records (data.list('bonds')); chess: the chess lookup; mode: config.json modes[pub.modeId]
* @param {{ bonds?: any[], chess?: (id: string) => any, mode?: any, priv?: any, diyData?: any }} [src]
* bonds: bonds.json records (data.list('bonds')); chess: the chess lookup; mode: config.json modes[pub.modeId]; priv: the
* viewer's m.private (its 自选 pieces out of the shop, diyBanned) with diyData (`{ chess, backups }`: data.get('chess') /
* data.get('backups')) to name them
* @returns {MatchInfoModel}
* stateOf: 'off' = the mode never activates the bond (本局禁用), 'drawn' = in the drawn set D (阵容不完整), null = normal;
* banned: the banned chess ids the data knows, by tier (ties keep the server's order); perBond: bondId → banned members
*/
export function matchInfoModel(pub, { bonds = [], chess = () => null, mode = null } = {}) {
export function matchInfoModel(pub, { bonds = [], chess = () => null, mode = null, priv = null, diyData = null } = {}) {
const sets = disabledBondSets(pub, mode?.inactiveBondIds);
const stateOf = (id) => (sets.off.has(id) ? 'off' : sets.drawn.has(id) ? 'drawn' : null);
const list = (Array.isArray(bonds) ? bonds : []).filter((b) => !!b && typeof b === 'object' && typeof b.bondId === 'string')
@@ -48,9 +54,21 @@ export function matchInfoModel(pub, { bonds = [], chess = () => null, mode = nul
return {
sets, stateOf, bonds: list, core: list.filter((b) => b.isCore), addon: list.filter((b) => !b.isCore),
banned, perBond: bannedPerBond(list, banned),
diyBanned: priv && diyData ? diyBannedPieces(priv, chess, diyData) : [],
};
}
/**
* The viewer's 自选 pieces out of the shop this match (MatchInfoModel `diyBanned`): one line under 本局禁用干员, or nothing.
* @param {{ model: MatchInfoModel, class?: string }} props
*/
export function DiyBannedLine({ model, class: cls = 'brief-banned__diy' }) {
const list = model?.diyBanned || [];
if (!list.length) return null;
return html`<p class=${cls} data-testid="diy-banned"><span class="diybanned__tag">${t('自选')}</span>
${t('{names}的盟约本局全部禁用,不会出现在你的商店', { names: list.map((x) => dn(x.name)) })}</p>`;
}
/**
* One row of bond discs (核心盟约 / 附加盟约): a greyed disc (✕) for a bond in the drawn set D or one the mode never
* activates, the red badge with its banned members, a red ring on an enabled bond that lost members; the tooltip says
@@ -97,6 +115,7 @@ export function BannedOperators({ model }) {
${banned.length ? html`<div class="brief-banned__grid">
${banned.map((id) => html`<${UnitThumb} key=${id} kind="chess" id=${id} size="sm" dim=${true} />`)}
</div>` : html`<p class="t-dim">本局没有禁用干员</p>`}
<${DiyBannedLine} model=${model} />
</div>`;
}