feedback1 WB: special-refresh offers drawn as what they hold (凯瑟琳's three items as item cards)

Player report #6: 凯瑟琳's 定向投放 offer after a level-up showed three nameless
operator cards (the raw item ids) under the promotion reward's 晋升奖励 header.
The shop bar now draws each offer slot by its kind (item slots as free item
cards) and names the offer: m.private shop.rewardOffer carries source
('merge' | 'special') and label (the strategy's effect name, the item's name
or the 特质's operator — effectsMeta.offerLabel); the reminder pill says
<label>待选择. Same path for 娜仁图亚, 寻呼模块, 信标 and 松果.
This commit is contained in:
sganggs committed 2026-10-03 01:48:29 +08:00
1 parent 5e6fe19a70
commit 49c0ebcf4d
10 files changed
+427 -39

No files matched your search

+13 -8
View File
@@ -96,7 +96,7 @@ export class PlayerState {
this.loadout = Object.freeze({});
if (!this.isBot && seat.loadout) this.setLoadout(seat.loadout);
this.shop = { level: 1, upgradePrice: this.gd.upgradeBase(1) ?? 0, slots: [], frozen: false, freeRefreshes: 0 };
/** reward offers queue (merge rewards, special refreshes): { tier, source, slots: [{ kind, id, price, sold }] } */
/** reward offers queue (merge rewards, special refreshes): { tier, source, label, slots: [{ kind, id, price, sold }] } */
this.offers = [];
/** @type {Array<any>} */
this.hand = new Array(HAND_SIZE).fill(null);
@@ -593,7 +593,7 @@ export class PlayerState {
* up from the tier below (user playtest #6 item 19: the official promotion reward never offers one operator twice —
* the user's first-hand report; the normal shop's slots may repeat). The offer reserves no copies (the pick takes one).
*/
pushRewardOffer(source = 'merge', { tier = null, ids = null } = {}) {
pushRewardOffer(source = 'merge', { tier = null, ids = null, label = null } = {}) {
const ro = this.gd.rewardOffer();
const t = Number.isInteger(tier) ? tier : Math.min(this.shop.level + ro.tierOffset, ro.maxTier);
// an offer never shows one operator twice, whoever built the list (user playtest #6 item 19)
@@ -608,17 +608,20 @@ export class PlayerState {
}
}
if (!list.length) return null;
const offer = { tier: t, source, slots: list.slice(0, MAX_OFFER_SLOTS).map((id) => ({ kind: 'chess', id, price: ro.price, sold: false })) };
const offer = { tier: t, source, label: typeof label === 'string' && label ? label : null, slots: list.slice(0, MAX_OFFER_SLOTS).map((id) => ({ kind: 'chess', id, price: ro.price, sold: false })) };
this.offers.push(offer);
this.dirty();
return offer;
}
/** Queue a free pick-one offer of items (凯瑟琳 定向投放 and similar); shown as shop.rewardOffer with kind 'item'. */
pushItemOffer(ids, { source = 'effect', tier = null } = {}) {
const list = (Array.isArray(ids) ? ids : []).filter((id) => this.gd.item(id)).slice(0, MAX_OFFER_SLOTS);
/**
* Queue a free pick-one offer of items (凯瑟琳 定向投放, 娜仁图亚 见者有份); shown as shop.rewardOffer with slots of kind
* 'item' under its `label` (the effect's name; player report #6 after 0.1.0).
*/
pushItemOffer(ids, { source = 'effect', tier = null, label = null } = {}) {
const list = [...new Set(Array.isArray(ids) ? ids : [])].filter((id) => this.gd.item(id)).slice(0, MAX_OFFER_SLOTS);
if (!list.length) return null;
const offer = { tier: Number.isInteger(tier) ? tier : null, source, slots: list.map((id) => ({ kind: 'item', id, price: 0, sold: false })) };
const offer = { tier: Number.isInteger(tier) ? tier : null, source, label: typeof label === 'string' && label ? label : null, slots: list.map((id) => ({ kind: 'item', id, price: 0, sold: false })) };
this.offers.push(offer);
this.dirty();
return offer;
@@ -1481,7 +1484,9 @@ export class PlayerState {
freeRefreshes: this.shop.freeRefreshes,
frozen: this.shop.frozen,
slots,
rewardOffer: offer ? { tier: offer.tier, slots: offer.slots.map((s) => ({ kind: s.kind === 'item' ? 'item' : 'chess', id: s.id, price: s.price, sold: !!s.sold })) } : null,
// `source` 'merge' = the promotion reward (晋升奖励); any other offer (a strategy, an item, a 特质) carries the
// `label` the bar shows instead (player report #6 after 0.1.0)
rewardOffer: offer ? { tier: offer.tier, source: offer.source === 'merge' ? 'merge' : 'special', label: offer.label || null, slots: offer.slots.map((s) => ({ kind: s.kind === 'item' ? 'item' : 'chess', id: s.id, price: s.price, sold: !!s.sold })) } : null,
},
hand: this.hand.map((p) => (p ? this.pieceView(p) : null)),
temp: this.temp.map((p) => (p ? this.pieceView(p) : null)),
+19 -2
View File
@@ -351,6 +351,22 @@ function ownedChess(ps) {
const finiteInt = (n) => (Number.isFinite(n) ? Math.trunc(n) : 0);
/**
* What a pick-one offer made by an effect is called on the shop bar (m.private shop.rewardOffer.label; player report #6
* after 0.1.0 — 凯瑟琳's three items came under the promotion reward's 晋升奖励 header): a strategy's effect name
* (定向投放, 见者有份), an item's name (寻呼模块, 信标), the operator whose 特质 it is (松果), else null.
* @param {import('./gamedata.js').GameData} gd
* @param {object} source the dispatch source (kind band / item / garrison …)
* @returns {string|null}
*/
export function offerLabel(gd, source) {
if (!source || typeof source !== 'object') return null;
if (source.kind === 'band') { const b = source.band || gd.band(source.bandId); return (b && (b.effectName || b.name)) || null; }
if (source.kind === 'item') { const it = source.item || (source.piece ? gd.item(source.piece.id) : null); return (it && it.name) || null; }
if (source.kind === 'garrison' && source.piece) { const c = gd.chess(source.piece.id); return (c && c.name) || null; }
return null;
}
/**
* @param {import('./Match.js').Match} m
* @param {import('./PlayerState.js').PlayerState} ps
@@ -510,9 +526,10 @@ export function makeCtx(m, ps, source, hook, ev = null) {
ps.recompute();
return true;
},
offerChess: (ids, opts = {}) => !!ps.pushRewardOffer(opts.source || source.key || 'effect', { ids, tier: opts.tier ?? null }),
/** Queue a free pick-one offer of chess (shop.rewardOffer; `label` defaults to offerLabel(source), the bar's header). */
offerChess: (ids, opts = {}) => !!ps.pushRewardOffer(opts.source || source.key || 'effect', { ids, tier: opts.tier ?? null, label: opts.label ?? offerLabel(gd, source) }),
/** Queue a free pick-one offer of items (shop.rewardOffer with slots of kind 'item'). */
offerItems: (ids, opts = {}) => !!ps.pushItemOffer(ids, { source: opts.source || source.key || 'effect', tier: opts.tier ?? null }),
offerItems: (ids, opts = {}) => !!ps.pushItemOffer(ids, { source: opts.source || source.key || 'effect', tier: opts.tier ?? null, label: opts.label ?? offerLabel(gd, source) }),
setShopSlot: (i, slot) => {
if (!Number.isInteger(i) || i < 0 || i >= ps.shop.slots.length) return false;
if (slot == null) { ps.shop.slots[i] = null; ps.dirty(); return true; }