feedback7: merge fb7-late — #389 / PR #390 画卷 keeps the items to copy across a merge, #385 / PR #387 spectator bond details count the hand like the own popup, #394 / PR #395 Enter during facing selection follows the focus, #281 the attack range drawn on a tapped operator, #388 引星棘刺 S1 DEF stacks, PR #386 谢拉格 寒风 blows bottom-up (owner)

- Golden union OK (roster-033 / roster-034 moved on both sides: #298 and #388); broad node --test 5598 (5596 pass, 0 fail,
  2 skipped); lint 0 errors.
This commit is contained in:
sganggs committed 2026-10-08 21:54:03 +08:00
commit 2492966c4f
32 files changed
+913 -138

No files matched your search

+11 -5
View File
@@ -225,7 +225,8 @@ export class MatchViews {
return p ? { charId: p.charId, skillIndex: p.skillIndex, uniEquipId: p.uniEquipId } : undefined;
}
/** UnitInfo of a player's board pieces on their (board) tiles — a prep scout's board, the boss partner's (bossMateView). */
/** UnitInfo of a player's board pieces on their (board) tiles — a prep scout's board, the boss partner's (bossMateView).
* `area: 'board'` (a bench unit says 'hand' / 'temp': prepFieldMeta). */
_prepBoardUnits(ps) {
const units = [];
// the player's own view of the data (0.2.0 自选编队: its slotted DIY slots are its operators — player/diy.js)
@@ -241,6 +242,7 @@ export class MatchViews {
const lo = piece.kind === 'chess' && chess ? ps.loadoutFor(chess) : null;
units.push({
id: piece.uid, uid: piece.uid, kind: piece.kind === 'token' ? 'token' : 'op', side: 'ally', ownerId: ps.playerId, defId: piece.id,
area: 'board',
name: rec ? rec.name : piece.id, tier: rec && Number.isInteger(rec.tier) ? rec.tier : 1, golden: !!(rec && rec.isGolden),
spine: assets.spine || (rec && rec.charId) || piece.id, avatar: assets.avatar || (rec && rec.charId) || piece.id,
x: c, y: r, dir: pieceDir(piece), facing: pieceDir(piece) === 'LEFT' ? -1 : 1, maxHp: rec && rec.stats && Number.isFinite(rec.stats.maxHp) ? rec.stats.maxHp : 1,
@@ -292,7 +294,11 @@ export class MatchViews {
// (test/match/spectator.test.js). User playtest #2 item 1 (GitHub #44).
// (a held chess the player fields as its stand-in is the stand-in there too — name, art, max HP — and carries
// `standInFor`, like a board piece: the owner's recall of the official mode, 2026-10-06, the hand shows the stand-in)
const benchUnit = (piece, i, y) => {
// Each unit says where it waits (`area` 'hand' | 'temp'; a board unit 'board'): the bond popup of a watched player
// counts it like the player's own (ui/watchBonds.js ownerBoard — a hand operator is owned and counts for 投资人 远见
// 奇迹, a temp one counts for nothing; GitHub #385, PR #387). The tag survives the boss-half remap (_onBossHalf), the
// row does not.
const benchUnit = (piece, i, y, area) => {
const rec = piece.kind === 'item' ? gd.item(piece.id) : piece.kind === 'token' ? gd.token(piece.id) : gd.chess(piece.id);
const standIn = piece.kind === 'chess' && rec && ps.fieldsStandIn(rec) ? this.gd.standIn(rec.chessId) : null;
const body = standIn || rec;
@@ -300,7 +306,7 @@ export class MatchViews {
const lo = piece.kind === 'chess' && rec ? ps.loadoutFor(rec) : null;
units.push({
id: piece.uid, uid: piece.uid, kind: piece.kind === 'token' ? 'token' : piece.kind === 'item' ? 'item' : 'op',
side: 'ally', ownerId: ps.playerId, defId: piece.id,
side: 'ally', ownerId: ps.playerId, defId: piece.id, area,
name: body ? body.name : piece.id, tier: rec && Number.isInteger(rec.tier) ? rec.tier : 1, golden: !!(rec && rec.isGolden),
spine: assets.spine || (body && body.charId) || piece.id, avatar: assets.avatar || (body && body.charId) || piece.id,
x: i, y, maxHp: body && body.stats && Number.isFinite(body.stats.maxHp) ? body.stats.maxHp : 1,
@@ -312,10 +318,10 @@ export class MatchViews {
});
};
for (let i = 0; i < ps.hand.length; i++) {
if (ps.hand[i]) benchUnit(ps.hand[i], i, GEO.HAND_ROW);
if (ps.hand[i]) benchUnit(ps.hand[i], i, GEO.HAND_ROW, 'hand');
}
for (let i = 0; i < ps.temp.length; i++) {
if (ps.temp[i]) benchUnit(ps.temp[i], GEO.TEMP_C0 + i, GEO.TEMP_ROW);
if (ps.temp[i]) benchUnit(ps.temp[i], GEO.TEMP_C0 + i, GEO.TEMP_ROW, 'temp');
}
// `nextEnemies`: the scouted player's coming enemies — their preview pen shows on the scouting board too (research 09
// §2.2 "Teammates"; render/app.js enterBattle({ prep: true, nextEnemies }))
+10 -4
View File
@@ -119,17 +119,23 @@ export function registerMeta(registry) {
// 画卷 — trap_copy_front_char: copy the operator in range (elite status included) with its equipment. A copied
// normal item that completes a pair with an owned one merges at once and the golden stays in the hand (research 04
// addendum "两个同名道具(无论是否被装备)会自动合并…并自动返回整备区"); the built-in equipped that golden on the copy.
// A copy that completes a three-copy merge (GitHub #389, PR #390): the elite wears nothing — the promotion returns the
// copies' equipment to the hand (PRTS 帮助 "在失去该干员(…合并等)…时自动卸除") — and the copied items stay in the hand
// too (PRTS 画卷 备注 "获得的装备为未装备状态"), a copied normal item merging with the returned original.
wrap(registry, 'chess_item_6_02_m', () => ({
onArt(ctx, ev) {
const target = (ev.targets || []).find((p) => p && p.kind === 'chess');
if (!target) { ev.error = 'BAD_TARGET'; ev.detail = 'no operator in range'; return; }
// snapshot before the gain: a gain that completes a merge consumes the target and empties `target.items`, and an
// item merge detaches the original's copy from `target.items` while we iterate (the built-in's live loop then
// skipped the next item)
const itemIds = (target.items || []).map((it) => it.id);
const copy = ctx.grantChess(target.id, { requirePool: false, source: 'item:chess_item_6_02_m' });
if (!copy) { ev.error = 'HAND_FULL'; return; }
// snapshot first: a merge detaches the original's copy of the item from `target.items` while we iterate
// (the built-in's live loop then skipped the next item)
for (const itemId of (target.items || []).map((it) => it.id)) {
const promoted = copy.id !== target.id;
for (const itemId of itemIds) {
const got = ctx.grantItem(itemId, { source: 'item:chess_item_6_02_m' });
const holder = got ? ctx.piece(copy.uid) : null;
const holder = got && !promoted ? ctx.piece(copy.uid) : null;
if (got && got.id === itemId && holder && holder.kind === 'chess') ctx.equipDirect(got.uid, holder.uid);
}
},
@@ -67,7 +67,10 @@ export default {
if (z.type === 'guard') {
const inZone = (a) => Math.abs(a.tileR - z.r) <= 1 && Math.abs(a.tileC - z.c) <= 1;
const allies = battle.alliesFor(unit).filter(inZone);
for (const a of allies) battle.addBuff(a, { key: 'thorn2:bastion', duration: AURA_DUR, mods: mods({ defFlat: num(bb.def) }) });
// one DEF buff per alchemy unit, so two units on one ally add up (+60 +60; PRTS 度算浪波 备注 「效果均可叠加」,
// GitHub #388 — one shared key let the last unit's buff replace the other's)
z.defKey ??= `thorn2:bastion:${unit.id}:${unit.mem.bastionSeq = (unit.mem.bastionSeq ?? 0) + 1}`;
for (const a of allies) battle.addBuff(a, { key: z.defKey, duration: AURA_DUR, mods: mods({ defFlat: num(bb.def) }) });
if (z.acc >= 1 - 1e-9) {
z.acc -= 1;
const regen = unit.s.atk * num(bb.hp_recovery_per_sec_ratio); // 生命回复速度 (see the header)