feedback1 WB: bounty drafts and the 机密商店 from 66 official screenshots (22 matches)
Bounty drafts: every 悬赏决策 event is a fixed card list and the draft shows 6 different cards of it, drawn with weight 1 + the official drafts of its group the card showed in. - R3: 9 of the 10 fixed sets seen (two new: 20_1 + 20_5, and one with 17_6); 3 x I + 2 x II + 1 x III in 22 of 22. The unseen 10th set is built by a rule whose III is 10_6 or 20_6, the only two-battle cards no draft showed. - R9: 6 groups of 9 cards, one per bossInitial event. The 鼠王 group came in 14 of 22 matches (every grey-board match among them), so groups are picked by the matches they came in. Named bosses no longer always come together. - R11: lists of 7 cards, the draft leaves out one (matches 1 / 6 / 21 / 22 leave out four different cards of one list); 7 of 15 lists seen, the rest and the 4 partial lists' 7th cards built by rule. Families 14 / 4 / 4. 机密商店 (the user: "机密商店按官方改成可以重复吧"): choices.json shopDraft, six slots drawn with replacement - VI, VI, V, 盟约之币 and twice V / IV / III / 盟约之币, items weighted by the official cards they showed on. A repeated item now comes in about 61 % of shops (official 3 of 4). Two identical cards stay two cards (picks by index): E2E, overlay and a headless-Chrome test (game-mock ?variant=shop, the official match 8 shop).
This commit is contained in:
1 parent
7c13e38422
commit
d1f8a49829
12 files changed
+799
-261
No files matched your search
+134
-64
@@ -5,15 +5,17 @@
|
||||
// count is `cards` (co-op 6, solo 3):
|
||||
// bounty 悬赏决策 six distinct cards.bounty entries (solo: 3 of them) built like the official draft of the round
|
||||
// (`bountyDraftCards`; player feedback after 0.1.0, report #2 — late bounty enemies in the early
|
||||
// drafts; 33 official screenshots, tools/build-data.mjs BOUNTY_INITIAL_SETS): schedule `bountyDraft`
|
||||
// names the kind and choices.json `bountyDrafts[kind]` its structure —
|
||||
// initial (R3, 险境 R6 [ASSUMED]): one of the 10 official sets of "接下来两场作战" cards — a seen
|
||||
// set, or for an unseen slot one card from each of 6 of the series 10–15 / 20 with the
|
||||
// tiers I I I II II III;
|
||||
// boss (R9): a seen group of named bosses + the group's cheap boss bounties / 源石虫·特训 to 6;
|
||||
// hunter (R11): one of the 15 official events — a seen group (a 特异III giant + its tier-II
|
||||
// "下场战斗" cards, always together) or for an unseen slot a group of that shape
|
||||
// [ASSUMED] — filled to 6 with tier-I cards, at most one card per faction series;
|
||||
// drafts; 66 official screenshots of 22 matches, tools/build-data.mjs BOUNTY_INITIAL_SETS): schedule
|
||||
// `bountyDraft` names the kind and choices.json `bountyDrafts[kind]` its card lists — the event is a
|
||||
// fixed list and the draft shows 6 different cards of it, each drawn with its weight (1 + the
|
||||
// official drafts of the group it showed in):
|
||||
// initial (R3, 险境 R6 [ASSUMED]): one of the 10 official sets of six "接下来两场作战" cards (all
|
||||
// six) — 9 seen, the 10th built by `rule` (I I I II II III) [ASSUMED];
|
||||
// boss (R9): one of the 6 groups of 9 boss bounties / 源石虫·特训, picked by the matches it came in
|
||||
// [ASSUMED];
|
||||
// hunter (R11): one of the 15 events, a list of 7 "下场战斗" cards — 7 seen (their `open` cards and
|
||||
// the 8 unseen lists built by `rule`: one 特异III giant + tier I / II cards, one per faction
|
||||
// series) [ASSUMED];
|
||||
// only `draft` cards of that `draftPool` (never 战术特训 — 法术教鞭 only —, the 鸭爵 set, the 7
|
||||
// multi-round cards or the pre-series cards: no official draft shows them); positions shuffled; the
|
||||
// mode's inactive enemy list does not apply (PRTS 卫戍协议:盟约 11/18 note "以上调整仅针对战术特训敌人,
|
||||
@@ -22,8 +24,13 @@
|
||||
// MULTI_ROUND_BOUNTY_BATTLES battles and says so (`bountyBattles` / `bountyText`: the user's call
|
||||
// after playtest #6 — "我不记得有过多轮悬赏")
|
||||
// supply 道具补给 random normal EQUIP shop items with tier in supplyTiers [lo, hi] (duplicates allowed)
|
||||
// shop 机密商店 random normal EQUIP shop items of any tier I–VI (duplicates allowed); FREE — the official card
|
||||
// text is "无需消耗资金,获得装备补给" (research 01 A4/04 addendum), so the price is 0
|
||||
// shop 机密商店 choices.json `shopDraft` (the 4 official 机密商店 at R11; the user: "机密商店按官方改成可以重复吧"):
|
||||
// six slots, each drawn on its own — with replacement, so the same item can be offered twice
|
||||
// (official: 盟约之币 ×2, 变形同构体 ×2): VI, VI, V, 盟约之币 and twice V / IV / III / 盟约之币, an
|
||||
// item within its tier by `itemWeights` (solo: 3 of the 6 slots [ASSUMED]); without `shopDraft`
|
||||
// random normal EQUIP shop items of any tier. FREE — the official card text is "无需消耗资金,获得装备
|
||||
// 补给" (research 01 A4/04 addendum), so the price is 0. Two identical cards are two cards: picks,
|
||||
// `taken` and the client go by the card's `idx`
|
||||
// tactic 战术决策 distinct cards.tactic entries; terrain cards only for the match stage; a 驰援 card
|
||||
// (single_special_choice_gain_bond_chess) or a 盟誓 card (global_special_choice_bond_addlayer)
|
||||
// only while at least one of its bonds is live in this match (opts.bondAvailable = Match.bondLive:
|
||||
@@ -185,76 +192,107 @@ export function bountyCard(gd, c) {
|
||||
};
|
||||
}
|
||||
|
||||
/** Up to `k` cards of `list` not in `taken`, at most one per series of `onePer` (a Set), in a shuffled order. */
|
||||
function drawDistinct(rng, list, k, taken, onePer = null) {
|
||||
/** Up to `k` cards of `list` not in `taken`, in a shuffled order. */
|
||||
function drawDistinct(rng, list, k, taken) {
|
||||
const out = [];
|
||||
const used = new Set([...taken].map((c) => c.series));
|
||||
for (const c of rng.shuffle(list.slice())) {
|
||||
if (out.length >= k) break;
|
||||
if (taken.has(c)) continue;
|
||||
if (onePer && onePer.has(c.series) && used.has(c.series)) continue;
|
||||
out.push(c);
|
||||
taken.add(c);
|
||||
used.add(c.series);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** `k` different entries of `list` drawn one by one with weights `weights` (missing / bad weights count 1). */
|
||||
function drawWeighted(rng, list, weights, k) {
|
||||
const pool = list.map((c, i) => [c, Math.max(0, Number(weights && weights[i]) || 1)]);
|
||||
const out = [];
|
||||
while (out.length < k && pool.length) {
|
||||
const c = weightedPick(rng, pool);
|
||||
out.push(c);
|
||||
pool.splice(pool.findIndex(([x]) => x === c), 1);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* The cards of a 悬赏决策 of kind `kind` after its official structure (choices.json `bountyDrafts[kind]`, see the module
|
||||
* The cards of an unseen R3 set (`spec.rule`, [ASSUMED]): one card per tier of `tiers` from the two-battle cards of
|
||||
* `series`, at most `perSeries` of one series; a tier the `prefer` cards have is filled first, by one of them.
|
||||
*/
|
||||
function initialRuleSet(rng, rule, all) {
|
||||
const out = [];
|
||||
const perSeries = Number.isInteger(rule.perSeries) ? rule.perSeries : 1;
|
||||
const ofSeries = (s) => out.filter((c) => c.series === s).length;
|
||||
const prefer = new Set(Array.isArray(rule.prefer) ? rule.prefer : []);
|
||||
const series = new Set(Array.isArray(rule.series) ? rule.series : []);
|
||||
// the tiers a preferred card has go first, so that it still fits its series
|
||||
const prefTiers = new Set(all.filter((c) => prefer.has(c.effectId)).map((c) => c.tier));
|
||||
const tiers = rng.shuffle((Array.isArray(rule.tiers) ? rule.tiers : []).slice());
|
||||
for (const tier of [...tiers.filter((t) => prefTiers.has(t)), ...tiers.filter((t) => !prefTiers.has(t))]) {
|
||||
const ok = (c) => !out.includes(c) && c.tier === tier && series.has(c.series) && ofSeries(c.series) < perSeries;
|
||||
const pref = all.filter((c) => prefer.has(c.effectId) && ok(c));
|
||||
const c = rng.pick(pref.length ? pref : all.filter(ok));
|
||||
if (c) out.push(c);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill an R11 list up to `rule.size` (an unseen event: from nothing; a seen group: its `open` cards) [ASSUMED]: a list
|
||||
* without a 特异III giant gets one, the rest are tier I / II cards, at most one per `onePerSeries` series and
|
||||
* `maxSeries16` of series 16.
|
||||
*/
|
||||
function hunterFill(rng, rule, list, byId) {
|
||||
const out = list.slice();
|
||||
const size = Number.isInteger(rule.size) ? rule.size : 7;
|
||||
const giants = (Array.isArray(rule.giants) ? rule.giants : []).map((id) => byId.get(id)).filter(Boolean);
|
||||
const onePer = new Set(Array.isArray(rule.onePerSeries) ? rule.onePerSeries : []);
|
||||
const max16 = Number.isInteger(rule.maxSeries16) ? rule.maxSeries16 : 2;
|
||||
if (out.length < size && !out.some((c) => giants.includes(c))) { const g = rng.pick(giants.filter((c) => !out.includes(c))); if (g) out.push(g); }
|
||||
const fits = (c) => !out.includes(c) && (onePer.has(c.series) ? !out.some((x) => x.series === c.series) : c.series !== 16 || out.filter((x) => x.series === 16 && !giants.includes(x)).length < max16);
|
||||
for (const c of rng.shuffle((Array.isArray(rule.cards) ? rule.cards : []).map((id) => byId.get(id)).filter(Boolean))) {
|
||||
if (out.length >= size) break;
|
||||
if (fits(c)) out.push(c);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* The cards of a 悬赏决策 of kind `kind` after its official card lists (choices.json `bountyDrafts[kind]`, see the module
|
||||
* header), over the eligible cards `byId` (effectId → cards.bounty entry). Ids the eligible list lacks are skipped.
|
||||
* pick 'slot': one of `slots` events, uniform — a seen group, or for an unseen event a list built by `rule`;
|
||||
* pick 'seen': a group by the number of official matches it came in.
|
||||
* The draft is `count` different cards of the list drawn by the group's `weights` (a rule-built card weighs 1).
|
||||
*/
|
||||
function structuredBounty(rng, kind, spec, byId) {
|
||||
const get = (ids) => (Array.isArray(ids) ? ids : []).map((id) => byId.get(id)).filter(Boolean);
|
||||
const taken = new Set();
|
||||
const count = Number.isInteger(spec.count) ? spec.count : 6;
|
||||
if (kind === 'initial') {
|
||||
const sets = Array.isArray(spec.sets) ? spec.sets : [];
|
||||
const slot = rng.int(Math.max(sets.length, Number.isInteger(spec.slots) ? spec.slots : 0));
|
||||
if (slot < sets.length) return get(sets[slot].cards).slice(0, count);
|
||||
// an unseen official set: one card from each of `count` series, with the seen sets' tier mix
|
||||
const mx = spec.mixed || {};
|
||||
const series = rng.shuffle((mx.series || []).slice()).slice(0, mx.count || count);
|
||||
const tiers = rng.shuffle((mx.tiers || []).slice());
|
||||
const all = [...byId.values()];
|
||||
const out = [];
|
||||
series.forEach((s, i) => {
|
||||
const ofSeries = all.filter((c) => c.series === s);
|
||||
const want = ofSeries.filter((c) => c.tier === tiers[i]);
|
||||
const c = rng.pick(want.length ? want : ofSeries);
|
||||
if (c) out.push(c);
|
||||
});
|
||||
return out;
|
||||
}
|
||||
if (kind === 'boss') {
|
||||
const t = rng.pick(Array.isArray(spec.templates) ? spec.templates : []);
|
||||
if (!t) return [];
|
||||
const featured = get(t.featured);
|
||||
featured.forEach((c) => taken.add(c));
|
||||
return [...featured, ...drawDistinct(rng, get(t.fill), count - featured.length, taken)].slice(0, count);
|
||||
}
|
||||
if (kind === 'hunter') {
|
||||
// one of the official events: a seen group, or for an unseen slot a group of the same shape; then tier-I fill to 6
|
||||
const onePer = new Set(Array.isArray(spec.onePerSeries) ? spec.onePerSeries : []);
|
||||
const templates = Array.isArray(spec.templates) ? spec.templates : [];
|
||||
const slots = Math.max(templates.length, Number.isInteger(spec.slots) ? spec.slots : 0);
|
||||
const groups = Array.isArray(spec.groups) ? spec.groups : [];
|
||||
let group = null;
|
||||
if (spec.pick === 'seen') {
|
||||
group = weightedPick(rng, groups.map((g) => [g, (Array.isArray(g.seen) ? g.seen.filter((x) => Number.isInteger(x)).length : 0) || 1]));
|
||||
} else {
|
||||
const slots = Math.max(groups.length, Number.isInteger(spec.slots) ? spec.slots : 0);
|
||||
const slot = slots > 0 ? rng.int(slots) : 0;
|
||||
let group;
|
||||
let fill;
|
||||
if (slot < templates.length) {
|
||||
group = get(templates[slot].featured);
|
||||
group.forEach((c) => taken.add(c));
|
||||
fill = get(templates[slot].fill);
|
||||
} else {
|
||||
const r = spec.rule || {};
|
||||
const sizes = Array.isArray(r.groupSizes) && r.groupSizes.length ? r.groupSizes : [1];
|
||||
const giant = drawDistinct(rng, get(r.giants), 1, taken);
|
||||
group = [...giant, ...drawDistinct(rng, get(r.groupCards), rng.pick(sizes), taken, onePer)];
|
||||
fill = get(r.fill);
|
||||
}
|
||||
return [...group, ...drawDistinct(rng, fill, count - group.length, taken, onePer)];
|
||||
group = slot < groups.length ? groups[slot] : null;
|
||||
}
|
||||
return [];
|
||||
const rule = spec.rule || {};
|
||||
let list;
|
||||
let weights;
|
||||
if (group) {
|
||||
const ids = Array.isArray(group.cards) ? group.cards : [];
|
||||
list = [];
|
||||
weights = [];
|
||||
ids.forEach((id, i) => { const c = byId.get(id); if (c) { list.push(c); weights.push(Array.isArray(group.weights) ? group.weights[i] : 1); } });
|
||||
if (kind === 'hunter' && Number(group.open) > 0) list = hunterFill(rng, { ...rule, size: list.length + Number(group.open) }, list, byId);
|
||||
} else if (kind === 'initial') {
|
||||
list = initialRuleSet(rng, rule, [...byId.values()]);
|
||||
} else if (kind === 'hunter') {
|
||||
list = hunterFill(rng, rule, [], byId);
|
||||
} else {
|
||||
list = [];
|
||||
}
|
||||
return drawWeighted(rng, list, weights, count);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -274,9 +312,41 @@ function bountyDraftCards(gd, rng, n, sch, round) {
|
||||
return rng.shuffle(out).slice(0, n).map((c) => bountyCard(gd, c));
|
||||
}
|
||||
|
||||
/**
|
||||
* The 机密商店 cards (choices.json `shopDraft`, module header): every slot drawn on its own — a tier (or `coin`, the
|
||||
* 盟约之币) by the slot's weights, then an item of that tier by `itemWeights` (1 when unlisted; an empty tier falls back to
|
||||
* the nearest lower one, then any) — so one item can fill two slots. Positions shuffled; `n` < the slots (solo) keeps
|
||||
* `n` of them. Null without a usable `shopDraft`.
|
||||
*/
|
||||
export function shopDraftCards(gd, rng, n) {
|
||||
const spec = gd.choices.shopDraft;
|
||||
const slots = spec && Array.isArray(spec.slots) ? spec.slots.filter((x) => x && typeof x === 'object') : [];
|
||||
if (!slots.length) return null;
|
||||
const w = spec.itemWeights && typeof spec.itemWeights === 'object' ? spec.itemWeights : {};
|
||||
const coin = typeof spec.coin === 'string' && gd.item(spec.coin) ? spec.coin : null;
|
||||
const ofTier = (t) => {
|
||||
for (let k = t; k >= 1; k--) if ((gd.shopItemsByTier[k] || []).length) return gd.shopItemsByTier[k];
|
||||
return eligibleItems(gd, 1, 6);
|
||||
};
|
||||
const out = [];
|
||||
for (const slot of slots) {
|
||||
const kinds = Object.entries(slot).filter(([k]) => k === 'coin' ? !!coin : Number.isInteger(Number(k)));
|
||||
const kind = weightedPick(rng, kinds);
|
||||
if (kind == null) continue;
|
||||
const list = kind === 'coin' ? [coin] : ofTier(Number(kind));
|
||||
const id = weightedPick(rng, list.map((x) => [x, Object.hasOwn(w, x) ? w[x] : 1]));
|
||||
if (id) out.push(itemCard(gd, id));
|
||||
}
|
||||
return rng.shuffle(out).slice(0, Math.max(0, n));
|
||||
}
|
||||
|
||||
function buildCards(gd, rng, family, n, sch, { stageId = null, bondAvailable = null, round = 1 } = {}) {
|
||||
const cardsData = gd.choices.cards || {};
|
||||
if (family === 'bounty') return bountyDraftCards(gd, rng, n, sch, round);
|
||||
if (family === 'shop') {
|
||||
const cards = shopDraftCards(gd, rng, n);
|
||||
if (cards) return cards;
|
||||
}
|
||||
if (family === 'supply' || family === 'shop') {
|
||||
let lo = 1;
|
||||
let hi = 6;
|
||||
|
||||
Reference in new issue
Block a user