- Stock: fixed trait gifts (缪尔赛思 / 变形者集群 变形同构体, 耶拉 不融冰, 蜜蜡 / 卡涅利安 迅捷作战粮, 诗怀雅 盟约之币) still occupy stock but are granted past the cap; an operator card already shown in the shop or a reward can be taken after the pool runs out and overdraws (GitHub #498, the owner's decision); item buys and item rewards keep the stock check. 洛洛 / 焰尾 sub-pools use the measured weights (PR #497 by @2321Robin; the occupancy idea of PR #496 by @2321Robin re-implemented differently). - Boss pairs are drawn at random once per match and fixed to battle start; the Hidden Core reuses them; a leaver does not split a full pair (the owner's decision). - 阿戈尔 in 联防 no longer re-applies the opening devour HP loss or a false revive; Touch's team medic stays for the match after its holder leaves (owner-approved, official lifetime unverified). - Auto-skill ranges: 司霆惊蛰 S2, 斩业星熊 S3 against air, 予愿安洁莉娜 S3; 自在's post-rebirth invulnerability is also untargetable (#510); Horn / Clementia / catapult / 忍冬 first hit (PR #502 by @Xinglan233); 不融冰 / tear gas trigger on real damage. - Bond layer-up sound in round start / draft / prep / battle (#482): one channel, a new cue replaces the old, no extra cooldown; a prep update that adds layers and activates a bond plays one cue. - Client: multi-swing fix (#506), refresh single-flight (PR #507 by @robll-v1), LP tower keeps the settled value with a −N tick (PR #504 by @robll-v1), export with only 潜能 / 练度 changes (PR #503 by @Xinglan233), spectator rosters list DIY members (PR #505 by @Xinglan233), banned members beside bond tooltips (PR #494 by @YuriRestia), recovery action above the away overlay (PR #511 by @RainoutOvO), the /dev/sim-check.html consistency page (PR #492 by @Cloudnyco), Korean (PR #490 by @D-Roote) and English Victoria wording (PR #499 by @YuriRestia). - golden: 45 of 287 scenarios move against 0.2.4 (roster 17, bonds 3, fields 6, matches 17, standins 2), each attributed in docs/history/0.2.5.md §30.9 / §30.16.
170 lines
10 KiB
JavaScript
170 lines
10 KiB
JavaScript
// Match info — the bonds and banned operators of a match (research 06 §4.1 "1/2 确认本局信息", research 01 A2 / 06
|
|
// addendum D), one implementation for every place that shows them, so they never drift:
|
|
// * the INFO_CHECK briefing's right column (screens/briefing.js);
|
|
// * the strategy draft's 本局信息 dialog (screens/bandDraft.js; GitHub issue #8 item 1 "选策略时没法返回查看禁用的干员和盟约":
|
|
// players choose a strategy partly by the bonds and operators the match leaves them);
|
|
// * the data of the in-game 本局信息 tab (ui/enemyDrawer.js InfoTab, its own compact chips and avatars).
|
|
// matchInfoModel (pure) derives everything from m.public and the static data: the two kinds of greyed bonds
|
|
// (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, 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).
|
|
// Styles: css/screens/briefing.css (.brief-*), css/screens/draft.css (.minfo-dlg).
|
|
|
|
import { html, Button, Icon, MicroLabel, BondDisc, Tooltip, Modal } from './components.js';
|
|
import { UnitThumb } from './gameComponents.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>,
|
|
* 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, 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, 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')
|
|
.sort((a, b) => (a.bondOrder ?? 0) - (b.bondOrder ?? 0) || (a.identifier ?? 0) - (b.identifier ?? 0));
|
|
const tierOf = (id) => chess(id)?.tier ?? 0;
|
|
const banned = (Array.isArray(pub?.bannedChess) ? pub.bannedChess : []).filter((id) => typeof id === 'string' && !!chess(id))
|
|
.sort((a, b) => tierOf(a) - tierOf(b));
|
|
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>`;
|
|
}
|
|
|
|
/**
|
|
* The box beside a bond's tooltip: its banned members (the badge's count), each with avatar, name and bonds (the hovered
|
|
* one highlighted); null when none are banned.
|
|
* @param {any} bond @param {MatchInfoModel} model @param {Set<string>} banned
|
|
*/
|
|
function BondBannedAside(bond, model, banned) {
|
|
const ids = (Array.isArray(bond.visibleMembers) ? bond.visibleMembers : []).filter((id) => banned.has(id));
|
|
if (!ids.length) return null;
|
|
const nameOf = new Map(model.bonds.map((b) => [b.bondId, b.name]));
|
|
return html`<div class="brief-bantip">
|
|
<div class="brief-bantip__h">${t('本局禁用干员')}</div>
|
|
${ids.map((id) => {
|
|
const c = data.lookup('chess', id);
|
|
const bonds = (Array.isArray(c?.bonds) ? c.bonds : []).filter((b) => nameOf.has(b));
|
|
return html`<div key=${id} class="brief-bantip__op" data-chess=${id}>
|
|
<${UnitThumb} kind="chess" id=${id} size="xs" showTier=${false} />
|
|
<div class="brief-bantip__txt"><b>${c?.name || id}</b>
|
|
<span>${bonds.map((b, i) => html`${i ? ' · ' : ''}<span class=${cx(b === bond.bondId && 'is-this')}>${nameOf.get(b)}</span>`)}</span></div>
|
|
</div>`;
|
|
})}
|
|
</div>`;
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
* which (briefingBondTip). A bond with banned members lists them in a second box beside the tooltip (BondBannedAside).
|
|
* @param {{ title: string, micro: string, bonds: any[], model: MatchInfoModel }} props
|
|
*/
|
|
export function MatchBondRow({ title, micro, bonds, model }) {
|
|
const m = data.get('assets');
|
|
const banned = new Set(model.banned);
|
|
return html`<div class="brief-bonds">
|
|
<h3 class="brief-h"><span>${title}</span><${MicroLabel}>${micro}</${MicroLabel}></h3>
|
|
<div class="brief-bonds__row">
|
|
${bonds.map((b) => {
|
|
const state = model.stateOf(b.bondId);
|
|
const off = !!state;
|
|
const bannedN = model.perBond.get(b.bondId) || 0;
|
|
return html`<${Tooltip} key=${b.bondId} text=${briefingBondTip(b.name, state, bannedN)}
|
|
aside=${bannedN > 0 ? BondBannedAside(b, model, banned) : null}>
|
|
<div class=${cx('brief-bond', off && 'is-off', state === 'drawn' && 'is-incomplete', !off && bannedN > 0 && 'is-partial')} data-bond=${b.bondId}>
|
|
<${BondDisc} name=${b.name} icon=${bondIconUrl(m, b.bondId)} active=${!off} disabled=${off} tier=${off ? 0 : (b.thresholds?.length || 1)}
|
|
maxTier=${Math.max(1, b.thresholds?.length || 1)} size="md" />
|
|
${bannedN > 0 ? html`<span class="brief-bond__ban num"><${Icon} name="user" />${bannedN}</span>` : null}
|
|
</div>
|
|
<//>`;
|
|
})}
|
|
</div>
|
|
</div>`;
|
|
}
|
|
|
|
/**
|
|
* The legend under the bond rows ("或本模式禁用" only when the mode switches bonds off).
|
|
* @param {{ model: MatchInfoModel }} props
|
|
*/
|
|
export function MatchLegend({ model }) {
|
|
return html`<p class="brief-legend"><span class="brief-legend__off"></span>${t('灰色:部分盟约所含干员阵容不完整(仍可通过其他盟约的干员或装备激活)')}${model.sets.off.size ? t(',或本模式禁用') : ''} · <span class="brief-legend__ban"><${Icon} name="user" /></span>${t('该盟约中无法出现的干员数')}</p>`;
|
|
}
|
|
|
|
/**
|
|
* 本局禁用干员: the count and the greyed avatars by tier.
|
|
* @param {{ model: MatchInfoModel }} props
|
|
*/
|
|
export function BannedOperators({ model }) {
|
|
const { banned } = model;
|
|
return html`<div class="brief-banned">
|
|
<h3 class="brief-h"><span>${t('本局禁用干员')}</span><${MicroLabel}>BANNED OPERATORS</${MicroLabel}><b class="num brief-banned__n">${banned.length}</b></h3>
|
|
${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">${t('本局没有禁用干员')}</p>`}
|
|
<${DiyBannedLine} model=${model} />
|
|
</div>`;
|
|
}
|
|
|
|
/**
|
|
* The briefing's bonds and banned operators: 核心盟约, 附加盟约, the legend, 本局禁用干员.
|
|
* @param {{ model: MatchInfoModel }} props
|
|
*/
|
|
export function MatchInfo({ model }) {
|
|
return html`<${MatchBondRow} title=${t('核心盟约')} micro="CORE BONDS" bonds=${model.core} model=${model} />
|
|
<${MatchBondRow} title=${t('附加盟约')} micro="ADD-ON BONDS" bonds=${model.addon} model=${model} />
|
|
<${MatchLegend} model=${model} />
|
|
<${BannedOperators} model=${model} />`;
|
|
}
|
|
|
|
/**
|
|
* The read-only match-info dialog (the strategy draft's 本局信息): MatchInfo in a Modal that 关闭, a tap outside or Esc
|
|
* closes. `status` (the caller's line, e.g. the draft's turn and countdown) sits left of 关闭.
|
|
* @param {{ open: boolean, onClose: Function, model: MatchInfoModel|null, status?: any }} props
|
|
*/
|
|
export function MatchInfoDialog({ open, onClose, model, status = null }) {
|
|
return html`<${Modal} open=${open && !!model} onClose=${onClose} title=${t('本局信息')} micro="MATCH INFO // BONDS & BANNED OPERATORS" class="minfo-dlg"
|
|
actions=${html`${status ? html`<div class="minfo-dlg__status" data-testid="match-info-status">${status}</div>` : null}
|
|
<${Button} variant="secondary" icon="close" data-autofocus data-testid="match-info-close" onClick=${onClose}>${t('关闭')}<//>`}>
|
|
${open && model ? html`<${MatchInfo} model=${model} />` : null}
|
|
<//>`;
|
|
}
|