From 5e4fe8ae6042b72ecb98d709d08c49f1e80801b4 Mon Sep 17 00:00:00 2001 From: sganggs Date: Sun, 4 Oct 2026 20:41:46 +0800 Subject: [PATCH] feedback3 WJ: toast when a bond grants a chess, and flash the new card MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 歌蕾蒂娅:获得X, and the same line for 余 and every other silent grantChess. A new hand or temp card uses the existing deploy flash. --- public/js/render/app.js | 3 ++- server/match/effectsMeta.js | 15 +++++++++++++++ test/content/garrisons_meta.test.js | 28 ++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/public/js/render/app.js b/public/js/render/app.js index 1ffc781d..2686cb4f 100644 --- a/public/js/render/app.js +++ b/public/js/render/app.js @@ -12,7 +12,7 @@ // (left_prepare / *_boss_prepare) clear of the folded shop's band (public // issue #5: folding the shop did not grow the board) // view.setPrep(privateState, { editable, canPlace }) hand/temp/board pieces; editable enables drag & drop; a -// new board piece flashes (fx.deploy); a merge's elite — on the tile of +// new board piece flashes (fx.deploy), and so does a card that just arrived in the hand or temp; a merge's elite — on the tile of // the deployed copy it replaced, or on its bench slot — gets the // promotion cue instead (render/promote.js, fx.promote) // view.enterBattle(fieldMeta) m.field { fieldId, kind, rect, stageId, units: [UnitInfo] } — each @@ -975,6 +975,7 @@ export async function createFieldView(host, options = {}) { promotions.push({ uid: e.uid, id: e.piece.id, area: e.area, row: e.row ?? null, col: e.col ?? null, idx: e.idx ?? null, copies: promoFrom.get(e.uid).length }); if (promotions.length > 20) promotions.shift(); } else if (e.area === 'board' && prevBoard.size && !prevBoard.has(e.uid)) { v.onDeploy?.(); fx.deploy(v); } + else if (before.length && (e.area === 'hand' || e.area === 'temp') && !before.some((g) => g.uid === e.uid)) fx.deploy(v); } else { const prevHome = v._home; const moved = !prevHome || prevHome.x !== w.x || prevHome.y !== w.y || prevHome.z !== w.z; diff --git a/server/match/effectsMeta.js b/server/match/effectsMeta.js index 98fcf5a4..b906163c 100644 --- a/server/match/effectsMeta.js +++ b/server/match/effectsMeta.js @@ -406,6 +406,11 @@ export function offerLabel(gd, source) { return null; } +/** Who a grantChess toast names: the same speaker as an offer label, or the choice card's name. */ +function grantSpeaker(gd, source) { + return offerLabel(gd, source) || (source && source.kind === 'choice' && source.card && source.card.name) || ''; +} + /** * @param {import('./Match.js').Match} m * @param {import('./PlayerState.js').PlayerState} ps @@ -507,6 +512,16 @@ export function makeCtx(m, ps, source, hook, ev = null) { // "some effects fail when the cap is hit" (research 06 §7): by default a chess of the pool needs a free copy if (opts.requirePool !== false && m.pool.has(base) && m.pool.left(base) < 1) return null; const p = ps.acquireChess(id, { source: opts.source || source.key || 'effect', toTemp: !!opts.toTemp, fromPool: opts.fromPool !== false }); + // 「歌蕾蒂娅:获得斯卡蒂」 — every silent grantChess (a 特质, 余 SERVER_MOST_BOND, a band, an item, a choice). + // opts.toast === false skips it. A caller that already says the same thing should pass that. + if (p && opts.toast !== false) { + const got = gd.chess(p.id); + const name = got && got.name; + if (name) { + const who = grantSpeaker(gd, source); + m.toast(ps, 'info', who ? `${who}:获得${name}` : `获得${name}`); + } + } return p ? view(p) : null; }, grantItem: (itemId, opts = {}) => { diff --git a/test/content/garrisons_meta.test.js b/test/content/garrisons_meta.test.js index e1821f71..793977da 100644 --- a/test/content/garrisons_meta.test.js +++ b/test/content/garrisons_meta.test.js @@ -696,6 +696,34 @@ test('pools: 凯瑟琳 127 odd rounds, 佩佩 94, 洛洛 91, 焰尾 149, 歌蕾 } }); +test('歌蕾蒂娅 / 余: granting a chess toasts 「名字:获得X」', () => { + const glady = setup(11); + const row = plain().slice(0, 2); + give(glady.m, glady.ps, 'chess_char_4_12_a', 'board', [10, 4]); + give(glady.m, glady.ps, row[0], 'board', [10, 6]); + give(glady.m, glady.ps, row[1], 'board', [10, 8]); + const n0 = glady.h.allTo('p_0', 'm.toast').length; + glady.roundStart(); + const lines = glady.h.allTo('p_0', 'm.toast').slice(n0).map((t) => t.text); + const line = lines.find((t) => t.startsWith('歌蕾蒂娅:获得')); + assert.ok(line, lines.join(' | ')); + const gained = line.slice('歌蕾蒂娅:获得'.length); + assert.ok(handChess(glady.ps).some((id) => CH(id).name === gained), `${line} not in ${handChess(glady.ps)}`); + const yu = setup(12); + give(yu.m, yu.ps, 'chess_char_6_03_a', 'board', [10, 4]); + yu.ps.bondCountBonus.egirShip = 9; + yu.ps.recompute(); + const fillers = plain((c) => !c.bonds.includes('egirShip')).slice(0, 2); + give(yu.m, yu.ps, fillers[0], 'board', [10, 6]); + give(yu.m, yu.ps, fillers[1], 'board', [10, 8]); + const n1 = yu.h.allTo('p_0', 'm.toast').length; + yu.roundStart(); + const yuLine = yu.h.allTo('p_0', 'm.toast').slice(n1).map((t) => t.text).find((t) => t.startsWith('余:获得')); + assert.ok(yuLine, yu.h.allTo('p_0', 'm.toast').slice(n1).map((t) => t.text).join(' | ')); + const yuName = yuLine.slice('余:获得'.length); + assert.ok(handChess(yu.ps).some((id) => CH(id).name === yuName), yuLine); +}); + test('余 37: a chess of the bond with the most members (normal: 3 in the row; 精锐: always)', () => { for (const { gid, g, owners } of idsOf('SERVER_PREP_START', 'SERVER_MOST_BOND')) { for (const owner of owners) {