feedback5: merge WE2 — fidelity (data / match / UI): summon hand counts from the tokens' hidden talents (麦哲伦 / 令 3, 白铁 2), 望's 棋子 on any tile, the 一会儿见! marker no hand card, TRY_SEARCH_ALLY_SKILL → SKILL_RANGE + allies, 斩业星熊's normal attack phys, IS / RA-only modules refused (SO-A / SO-B / RA-A), 黑键 S2 at full SP with a free tile, 重构体 / 战术锚点 placement, 塑心's cap per trait, random grants draw 自选 stock, diyBanned shown, AI 托管 buys 自选, 谬因 S2 / 薇薇安娜 S3 area triggers, #148 regression test, tier*.js / diyRecord shims removed; golden: coop2-NORMAL-70-diy, diy-045 / 078 / 114 / 115 / 116 moved (WE2's own), the W4B moves kept
This commit is contained in:
commit
723ee72d38
79 files changed
+1389
-405
No files matched your search
@@ -2,7 +2,8 @@
|
||||
// 干员调配 overlay (screens/diy.js), shared with the sync (ui/loadoutSync.js), the match UI (ui/gameLogic/diy.js) and
|
||||
// the tests. Four DIY slots (data/backups.json `diy.slots`: two at tier 5, two at tier 6, sold from 调度中心 level 5 / 6);
|
||||
// each takes an operator: a 6★ the player owns outside the chess pool (`diy.ownedPool`) — any of its three skills and
|
||||
// any module of its elite form but a 集成战略 one (shared/diy.js isDiyModule) — or a prototype (原型干员) with its locked
|
||||
// any module of its elite form but one of another game mode (集成战略 / 生息演算: shared/diy.js isDiyModule) — or a
|
||||
// prototype (原型干员) with its locked
|
||||
// skill / module; only operators with a kit are offered (`kitted`: the server's `welcome.diyKitted`). A prototype may
|
||||
// fill a tier-5 and a tier-6 slot, an owned operator one slot; the picks of a tier differ (shared/diy.js
|
||||
// validateDiyPicks).
|
||||
|
||||
@@ -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>`;
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ export { sortedPlayers } from './gameLogic/shared.js';
|
||||
export { ownFieldId, homeFieldId, cycleField, watchTarget, switcherLabel, fieldLabel, activeBubbles } from './gameLogic/watch.js';
|
||||
export { sortBonds, bondTier, nextThreshold, grantedBonds, pieceBondIds, morphPairings, HARMONY_BOND, harmonyMembers, bondMembers, memberHeadCount, bannedPerBond, disabledBondSets, briefingBondTip, modeOffBonds, bandOffBonds, bandOffLine } from './gameLogic/bonds.js';
|
||||
export { priceTone, mergeProgress, mergeTarget, handFull, completesMerge, offerHeader, shopBlockReason, readyFundsPrompt } from './gameLogic/shop.js';
|
||||
export { deploySets, stageOverrides, deployMap, effectiveStage, indexPieces, placementContext, piecePosition, tileAllows, summonRange, canPlace, itemAttaches, equipMerges, boardTargets, dropIntent, dropFailureReason } from './gameLogic/placement.js';
|
||||
export { deploySets, stageOverrides, deployMap, effectiveStage, indexPieces, placementContext, piecePosition, tileAllows, summonRange, summonExcluded, canPlace, itemAttaches, equipMerges, boardTargets, dropIntent, dropFailureReason } from './gameLogic/placement.js';
|
||||
export { terrainInfo } from './gameLogic/terrain.js';
|
||||
export { normalizeDraft, normalizeSp } from './gameLogic/draft.js';
|
||||
export { groupEnemies, PEN, penZoneTiles, penPlacement, previewEnemyKey, factionTypes } from './gameLogic/enemies.js';
|
||||
@@ -43,5 +43,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';
|
||||
@@ -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;
|
||||
}
|
||||
@@ -227,8 +227,13 @@ export function piecePosition(ctx, piece) {
|
||||
if (meleeOnHighGround(rec)) return 'ALL';
|
||||
return rec?.position === 'MELEE' ? 'MELEE' : 'RANGED';
|
||||
}
|
||||
// tokens: MELEE → ground only; RANGED / ALL → any deployable tile
|
||||
if (piece.kind === 'token') return ctx.getToken(piece.id)?.position === 'MELEE' ? 'MELEE' : 'RANGED';
|
||||
// tokens: MELEE → ground only; RANGED / ALL → any deployable tile; `rangedTilesOnly` (战术锚点 "仅可以部署在…远程位")
|
||||
// → 'HIGH', the ranged tiles only (server/match/board.js positionClass 'high')
|
||||
if (piece.kind === 'token') {
|
||||
const t = ctx.getToken(piece.id);
|
||||
if (t?.rangedTilesOnly === true) return 'HIGH';
|
||||
return t?.position === 'MELEE' ? 'MELEE' : 'RANGED';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -237,6 +242,7 @@ export function tileAllows(ctx, piece, row, col) {
|
||||
const pos = piecePosition(ctx, piece);
|
||||
if (!pos) return false;
|
||||
const k = tileKey(row, col);
|
||||
if (pos === 'HIGH') return ctx.deploy.ranged.has(k) && !ctx.deploy.melee.has(k);
|
||||
return pos === 'MELEE' ? ctx.deploy.melee.has(k) : ctx.deploy.ranged.has(k);
|
||||
}
|
||||
|
||||
@@ -251,6 +257,23 @@ export function tileAllows(ctx, piece, row, col) {
|
||||
*/
|
||||
export function summonRange(ctx, piece, owner = null) {
|
||||
if (!isObj(piece) || piece.kind !== 'token' || ctx?.getToken?.(piece.id)?.ownerRange !== true) return null;
|
||||
return ownerRangeOf(ctx, piece, owner);
|
||||
}
|
||||
|
||||
/**
|
||||
* Board tiles ('r,c') an outside-bound summon may NOT stand on — its owner's attack range (tokens.json
|
||||
* `ownerRangeOutside`: 凯尔希·思衡托's 战术锚点 "仅可以部署在凯尔希·思衡托攻击范围外的远程位") — or null. Mirror of
|
||||
* server/match/player/placement.js summonExcluded.
|
||||
* @param {ReturnType<typeof placementContext>} ctx
|
||||
* @returns {Set<string> | null}
|
||||
*/
|
||||
export function summonExcluded(ctx, piece, owner = null) {
|
||||
if (!isObj(piece) || piece.kind !== 'token' || ctx?.getToken?.(piece.id)?.ownerRangeOutside !== true) return null;
|
||||
return ownerRangeOf(ctx, piece, owner);
|
||||
}
|
||||
|
||||
/** The attack-range tiles of a summon piece's owner on the board (summonRange / summonExcluded), or null. */
|
||||
function ownerRangeOf(ctx, piece, owner = null) {
|
||||
let at = owner;
|
||||
if (!at) for (const e of ctx.boardAt.values()) if (e.piece.uid === piece.ownerUid && e.piece.kind === 'chess') { at = e; break; }
|
||||
const rec = at && ctx.getChess(at.piece.id);
|
||||
@@ -260,11 +283,14 @@ export function summonRange(ctx, piece, owner = null) {
|
||||
return new Set(rangeTiles(grid || rec.rangeGrid, at.row, at.col, pieceDir(at.piece)).map(([r, c]) => tileKey(r, c)));
|
||||
}
|
||||
|
||||
/** tileAllows plus the owner-range rule of a range-bound summon (summonRange). */
|
||||
/** tileAllows plus the owner-range rules of a range-bound summon (summonRange, summonExcluded). */
|
||||
function unitAllowed(ctx, piece, row, col, owner = null) {
|
||||
if (!tileAllows(ctx, piece, row, col)) return false;
|
||||
const k = tileKey(row, col);
|
||||
const range = summonRange(ctx, piece, owner);
|
||||
return !range || range.has(tileKey(row, col));
|
||||
if (range && !range.has(k)) return false;
|
||||
const out = summonExcluded(ctx, piece, owner);
|
||||
return !out || !out.has(k);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -329,13 +355,16 @@ export function canPlace(ctx, uid, target) {
|
||||
if (src.area === 'board' && src.row === row && src.col === col) return { ok: true, action: 'orient' };
|
||||
if (!tileAllows(ctx, piece, row, col)) {
|
||||
const deployable = ctx.deploy.ranged.has(tileKey(row, col));
|
||||
return no('BAD_TILE', deployable && piecePosition(ctx, piece) === 'MELEE' ? '近战单位只能部署在地面' : '无法部署在该位置');
|
||||
const pos = piecePosition(ctx, piece);
|
||||
return no('BAD_TILE', deployable && pos === 'MELEE' ? '近战单位只能部署在地面' : deployable && pos === 'HIGH' ? '只能部署在远程位' : '无法部署在该位置');
|
||||
}
|
||||
// a range-bound summon (战术点): inside its owner's attack range — seen from the summon's old tile when it is
|
||||
// dropped onto its own owner (the two swap)
|
||||
// dropped onto its own owner (the two swap); an outside-bound one (战术锚点) outside it
|
||||
const ownerSwap = src.area === 'board' && occ && occ.piece.uid === piece.ownerUid ? { row: src.row, col: src.col, piece: occ.piece } : null;
|
||||
const range = summonRange(ctx, piece, ownerSwap);
|
||||
if (range && !range.has(tileKey(row, col))) return no('BAD_TILE', '只能部署在召唤者攻击范围内');
|
||||
const out = summonExcluded(ctx, piece, ownerSwap);
|
||||
if (out && out.has(tileKey(row, col))) return no('BAD_TILE', '只能部署在召唤者攻击范围外');
|
||||
if (src.area === 'board') {
|
||||
// board → board: move or swap (the occupant must be legal on the source tile — the mover's own summon excepted:
|
||||
// a moved operator's summons go back to the hand anyway)
|
||||
|
||||
@@ -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>`;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user