From becb165f7a63137f87759219889da61e255f5b36 Mon Sep 17 00:00:00 2001 From: sganggs Date: Sat, 3 Oct 2026 07:18:50 +0800 Subject: [PATCH] =?UTF-8?q?feedback1f=20WS:=20=E4=BC=91=E9=9C=B2=E4=B8=9D?= =?UTF-8?q?=20/=20=E7=8E=9B=E6=81=A9=E7=BA=B3=20purchase=20counters=20pinn?= =?UTF-8?q?ed=20to=20shop=20purchases=20(F2=20not=20reproduced)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Player report F2 after 0.1.0: a 谢拉格 taken from the promotion reward was said to use up 休露丝's first-purchase discount. Through real intents the promotion reward, 寻呼模块's pick and the 拟态物质 / 紧急调度券 / 简易通讯机 grants never touch it: only g.buy dispatches onBuy, where every purchase counter lives. Regression tests and the META onBuy row say so. --- docs/META.md | 2 +- server/sim/content/bands/meta.js | 3 +- test/match/feedback1f-sciurus.test.js | 108 ++++++++++++++++++++++++++ 3 files changed, 111 insertions(+), 2 deletions(-) create mode 100644 test/match/feedback1f-sciurus.test.js diff --git a/docs/META.md b/docs/META.md index 62ae4972..7d8bdc37 100644 --- a/docs/META.md +++ b/docs/META.md @@ -242,7 +242,7 @@ Every handler method is `(ctx, ev)`; `ev` is shared by all handlers of one dispa | `onSold` | a chess was sold (after refund of copies) | `{ piece, gain }` — **write `ev.gain`** to change the funds paid | | `onRefresh` | manual refresh | `{ slots, free, price }` (slots mutable, or use `ctx.setShopSlot`) | | `onPrice` | every price query of a shop slot (views + buy) — must be **pure** | `{ slot, kind, id, price }` — write `ev.price` / `ctx.setPrice` / `ctx.modifyPrice` | -| `onBuy` | after a purchase | `{ piece, slot, price, kind }` (`piece` = owned result, elite after a merge) | +| `onBuy` | after a shop purchase (`g.buy` only) | `{ piece, slot, price, kind }` (`piece` = owned result, elite after a merge). The "购买" counters — 休露丝 首名<谢拉格> 1 资金, 玛恩纳 每购买一名<卡西米尔>, 升华 / 整备 "接下来购买的…" — hook it, so a free pick (promotion reward, 寻呼模块 / 信标 / 凯瑟琳 offers: `g.reward`) or a grant (紧急调度券, 简易通讯机, 拟态物质, strategies) never counts as a purchase (player report F2 after 0.1.0; PRTS 帮助 describes the promotion reward as a 0-cost temporary shop — whether the official 雪域礼赠 counts that pick is unknown, [ASSUMED] no) | | `onMerge` | chess or item merge | `{ kind, piece, baseId|itemId, consumed:[uid], area? }` — a chess merge's `area` is where its elite went: 'board' (a consumed copy's tile) \| 'hand' \| 'temp' | | `onLevelUp` | shop level up | `{ level, price }` | | `onSpend` | a payment's action is complete (buy / refresh / levelUp / reward / effect) | `{ amount, reason, total }` (`total` = funds spent this match) | diff --git a/server/sim/content/bands/meta.js b/server/sim/content/bands/meta.js index ae1371fa..ba6cd2ce 100644 --- a/server/sim/content/bands/meta.js +++ b/server/sim/content/bands/meta.js @@ -14,7 +14,8 @@ // band_coin_cost_gain_random_char_by_shop_level 绮良 every coin_cnt funds spent → count random chess ≤ shop level // up_shop_next_refresh_must_present_bond_char 佩佩 level-up to a level of lvlist → +1 free refresh (price 0) that prefers // gain_bond_char_per_round {round,preround,bond} 哈洛德 rounds round, round+preround, …: a chess (≤ shop level, else any tier) -// first_buy_in_round_char_price_change {price,bond} 休露丝 first chess of the round costs `price` +// first_buy_in_round_char_price_change {price,bond} 休露丝 first chess of the round costs `price` (bought in the +// shop: onBuy — a free pick / grant never uses it up) // band_cost_coin_reach_cnt_gain_chess_from_pool 潘格尼尼 once coin_cnt funds spent in total: 1 chess of pool (elite 拉特兰 ≥ T4) // round_start_bond_check_gain_layer 余 round start of `round`: exactly factioncount active bonds → +count1, else each active +count2 // up_shop_add_special_goods {count,choice,pool} 凯瑟琳 every level-up: pick 1 of `count` items of pool (free) diff --git a/test/match/feedback1f-sciurus.test.js b/test/match/feedback1f-sciurus.test.js new file mode 100644 index 00000000..3af2a0ee --- /dev/null +++ b/test/match/feedback1f-sciurus.test.js @@ -0,0 +1,108 @@ +// Player report F2 after 0.1.0 (2026-10-03): with 休露丝 【雪域礼赠】 ("每回合购买的首名<谢拉格>干员消耗资金为1", +// band_sciurus first_buy_in_round_char_price_change {price 1, bond kjeragShip}) a <谢拉格> taken for free — the promotion +// reward's 3-choose-1 after a merge, an item's pick-one offer or grant — was said to use up the round's discount. Checked +// through real intents (g.buy / g.reward / g.equip on a real Match with the real registry): only a shop purchase (g.buy → +// onBuy) counts; every free acquisition leaves the discount for the next <谢拉格> bought. Same rule for 玛恩纳 【业务指标】 +// ("每购买一名<卡西米尔>干员,下回合开始时资金+1"). Pinned here (the report did not reproduce on 0.1.0). +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { makeMatch, give, giveItem, DATA } from './harness.js'; + +const OK = { ok: true }; +const KJ = (id) => !!(DATA.chess[id] && DATA.chess[id].bonds.includes('kjeragShip')); +const slot = (id) => ({ kind: 'chess', id, basePrice: DATA.chess[id].tier === 1 ? 2 : DATA.chess[id].tier <= 4 ? 3 : 4, frozen: false, sold: false }); +const NOT_KJ_T1 = Object.values(DATA.chess).find((c) => c.visible && !c.isGolden && c.tier === 1 && !c.bonds.includes('kjeragShip') && !c.bonds.includes('kazimierzShip')).chessId; + +/** Solo match at R1 prep with an empty board, 50 funds, shop level 3 and `band`; the shop = `shop` chess ids. */ +function setup(band, shop) { + const h = makeMatch({ mode: 'solo', humans: 1, seed: 3 }).start(); + h.toPrep(1); + const m = h.m; + const ps = h.ps('p_0'); + for (const p of [...ps.board.values(), ...ps.hand.filter(Boolean)]) if (p.kind === 'chess') ps.returnCopies(p); + ps.board.clear(); ps.hand.fill(null); ps.offers.length = 0; + ps.funds = 50; ps.counters = {}; ps.pendingFunds = 0; ps.shop.level = 3; ps.bandId = band; + ps.shop.slots = shop.map(slot); + ps.recompute(); + const viewPrice = (i) => { m.flush(true); return h.lastTo('p_0', 'm.private').shop.slots[i].price; }; + return { h, m, ps, viewPrice }; +} + +/** After a free acquisition: the shop's <谢拉格> at `i` still costs 1 (server and m.private), a purchase pays 1, the next costs full. */ +function discountIntact({ m, ps, viewPrice }, i, j, what) { + assert.equal(ps.priceOf(ps.shop.slots[i]), 1, `${what}: the shop <谢拉格> still costs 1`); + assert.equal(viewPrice(i), 1, `${what}: m.private shows 1`); + const f0 = ps.funds; + assert.deepEqual(m.handle('p_0', { t: 'g.buy', slot: i }), OK); + assert.equal(f0 - ps.funds, 1, `${what}: the first <谢拉格> bought pays 1`); + assert.equal(ps.priceOf(ps.shop.slots[j]), ps.shop.slots[j].basePrice, `${what}: the second <谢拉格> of the round costs full`); +} + +test('F2 休露丝: a <谢拉格> taken from the promotion reward after a merge keeps the round\'s discount', () => { + const s = setup('band_sciurus', ['chess_char_3_11_a', 'chess_char_3_14_a', NOT_KJ_T1]); + give(s.m, s.ps, NOT_KJ_T1); give(s.m, s.ps, NOT_KJ_T1); + assert.deepEqual(s.m.handle('p_0', { t: 'g.buy', slot: 2 }), OK, 'the third copy merges'); + const offer = s.ps.offers[0]; + assert.ok(offer && offer.source === 'merge', 'promotion reward'); + if (!offer.slots.some((x) => KJ(x.id))) offer.slots[0].id = 'chess_char_3_20_a'; + const idx = offer.slots.findIndex((x) => KJ(x.id)); + assert.deepEqual(s.m.handle('p_0', { t: 'g.reward', idx }), OK); + assert.ok(s.ps.hand.some((p) => p && p.kind === 'chess' && KJ(p.id)), 'the <谢拉格> joined the hand'); + discountIntact(s, 0, 1, 'promotion reward'); +}); + +test('F2 休露丝: 寻呼模块\'s pick-one and the free grants of items (拟态物质, 紧急调度券, 简易通讯机) keep the discount', () => { + // 寻呼模块 on 银灰 (only <谢拉格>): three <谢拉格> ≤ shop level offered, one taken for free + { + const s = setup('band_sciurus', ['chess_char_3_11_a', 'chess_char_3_14_a']); + const t = give(s.m, s.ps, 'chess_char_4_22_a'); + const it = giveItem(s.m, s.ps, 'chess_item_4_01_e_a'); + assert.deepEqual(s.m.handle('p_0', { t: 'g.equip', itemUid: it.uid, targetUid: t.uid }), OK); + const offer = s.ps.offers[0]; + assert.ok(offer && offer.slots.length && offer.slots.every((x) => KJ(x.id)), `寻呼模块 offers <谢拉格> (${offer && offer.slots.map((x) => x.id)})`); + assert.deepEqual(s.m.handle('p_0', { t: 'g.reward', idx: 0 }), OK); + discountIntact(s, 0, 1, '寻呼模块'); + } + // 拟态物质 on one of two 角峰: the third 角峰 (a merge) and its promotion reward + { + const s = setup('band_sciurus', ['chess_char_3_11_a', 'chess_char_3_14_a']); + give(s.m, s.ps, 'chess_char_1_02_a'); + const t = give(s.m, s.ps, 'chess_char_1_02_a'); + const it = giveItem(s.m, s.ps, 'chess_item_5_05_e_a'); + assert.deepEqual(s.m.handle('p_0', { t: 'g.equip', itemUid: it.uid, targetUid: t.uid }), OK); + assert.ok([...s.ps.hand, ...s.ps.board.values()].some((p) => p && p.id === 'chess_char_1_02_b'), 'elite 角峰'); + const offer = s.ps.offers[0]; + if (!offer.slots.some((x) => KJ(x.id))) offer.slots[0].id = 'chess_char_3_20_a'; + assert.deepEqual(s.m.handle('p_0', { t: 'g.reward', idx: offer.slots.findIndex((x) => KJ(x.id)) }), OK); + discountIntact(s, 0, 1, '拟态物质'); + } + // 紧急调度券: a shop operator taken for free (here one of three <谢拉格>) + { + const s = setup('band_sciurus', ['chess_char_3_11_a', 'chess_char_3_14_a', 'chess_char_3_20_a']); + const t = give(s.m, s.ps, NOT_KJ_T1); + const it = giveItem(s.m, s.ps, 'chess_item_2_02_e_a'); + assert.deepEqual(s.m.handle('p_0', { t: 'g.equip', itemUid: it.uid, targetUid: t.uid }), OK); + const left = s.ps.shop.slots.map((x, i) => (x && !x.sold ? i : -1)).filter((i) => i >= 0); + assert.equal(left.length, 2, 'one shop operator taken'); + discountIntact(s, left[0], left[1], '紧急调度券'); + } + // 简易通讯机 on 银灰: a random <谢拉格> granted + { + const s = setup('band_sciurus', ['chess_char_3_11_a', 'chess_char_3_14_a']); + const t = give(s.m, s.ps, 'chess_char_4_22_a'); + const it = giveItem(s.m, s.ps, 'chess_item_2_06_e_a'); + assert.deepEqual(s.m.handle('p_0', { t: 'g.equip', itemUid: it.uid, targetUid: t.uid }), OK); + assert.equal(s.ps.hand.filter((p) => p && p.kind === 'chess' && KJ(p.id)).length, 2, 'a <谢拉格> granted'); + discountIntact(s, 0, 1, '简易通讯机'); + } +}); + +test('F2 玛恩纳 业务指标: only a shop purchase of a <卡西米尔> pays the next round\'s +1 (a free reward pick does not)', () => { + const kz = Object.values(DATA.chess).filter((c) => c.visible && !c.isGolden && c.tier <= 3 && c.bonds.includes('kazimierzShip')).map((c) => c.chessId); + const s = setup('band_mlynar', [kz[0], kz[1], NOT_KJ_T1]); + s.ps.pushRewardOffer('merge', { ids: [kz[2] ?? kz[0]] }); + assert.deepEqual(s.m.handle('p_0', { t: 'g.reward', idx: 0 }), OK); + assert.equal(s.ps.pendingFunds, 0, 'a free <卡西米尔> pays nothing'); + assert.deepEqual(s.m.handle('p_0', { t: 'g.buy', slot: 0 }), OK); + assert.equal(s.ps.pendingFunds, 1, 'a bought <卡西米尔>: +1 next round'); +});