feedback3 WC: the 信标 gift sends the original operator (an elite stays an elite) and still arrives when the sender is eliminated; a failed grant waits for the next round start

This commit is contained in:
sganggs committed 2026-10-04 17:06:43 +08:00
1 parent bdb0765c55
commit ce33795d1b
6 files changed
+204 -19

No files matched your search

+5
View File
@@ -1373,6 +1373,11 @@ export class Match {
}
for (const ps of alive) ps.startRound(r);
for (const ps of alive) this.dispatch(ps, 'onRoundStart', { round: r });
// an eliminated player's pending 信标 gift still goes to its teammate (effects flagged afterElimination; GitHub #86)
for (const ps of this.order) {
if (ps.alive) continue;
try { this.dispatcher.dispatchEliminated(ps, 'onRoundStart', { round: r }); } catch (e) { this.reportError('dispatch onRoundStart (eliminated)', e); }
}
for (const ps of alive) ps.recompute();
this.setDeadline(DELAYS.ROUND_START / 1000, () => this.afterRoundStart(), { silent: this.soloUntimed });
this.markPublic();
+2
View File
@@ -1489,6 +1489,8 @@ export class PlayerState {
this.dirty();
}
// `effects` are kept: a 信标 gift still pending is delivered to the teammate at the next round start (builtin_gift is
// flagged afterElimination — GitHub #86); nothing else of an eliminated player is dispatched.
eliminate(round) {
this.alive = false;
this.ready = false;
+39 -17
View File
@@ -15,7 +15,8 @@
// use_equip_reward_char_chess 拟态物质 3rd copy or a same-bond chess
// use_equip_reward_special_goods_char_chess {refresh_cnt} 寻呼模块 offer N same-bond chess (≤ shop level)
// use_equip_recruit_new_char_and_give_char_to_player_most_bond {refresh_cnt} 信标 destroy target, offer N same-tier
// chess, gift the original next prep
// chess, gift the original (an elite stays
// elite) next prep, sender eliminated or not
// sell_char_count_gain_equip_owner_bond {count} 商业包装方案 every `count` sells → same-bond chess
// char_chess_transformation_equip 突变细胞 after battle: holder destroyed, its equipment
// (the cell included — not consumed) back to
@@ -43,6 +44,19 @@ function rollSameBond(ctx, bonds, maxTier, exclude = null) {
return ctx.rollChess({ maxTier, filter: (id) => { const c = ctx.gd.chess(id); return !!(c && Array.isArray(c.bonds) && c.bonds.some((b) => set.has(b))) && !(exclude && exclude.includes(id)); } });
}
/** 信标: the living teammate (not the caller) with the most members of `bonds` ("相应盟约人数最多"), ties at random; null without one. */
function mostBondMate(ctx, bonds) {
const mates = ctx.teammates();
if (!mates.length) return null;
let best = -1;
let pick = [];
for (const t of mates) {
const score = bonds.reduce((s, b) => s + t.bondCount(b), 0);
if (score > best) { best = score; pick = [t]; } else if (score === best) pick.push(t);
}
return ctx.rng.pick(pick) || null;
}
const ITEM_HANDLERS = {
equip_destory_gain_random_coin: {
onEquip(ctx, ev) {
@@ -135,6 +149,12 @@ const ITEM_HANDLERS = {
if (ids.length) ctx.offerChess(ids, { source: 'item' });
},
},
// 信标 (act2autochess eff_acarm109 / eff_acgarm109 "装备时,目标干员和本装备销毁并进行一次特殊刷新,出现两名与携带者同等阶的
// 干员,免费获取其中一名 / 若在同盟模拟中且存在其他队友,下个休整期向相应盟约人数最多的队友发送1个原干员 / 相应盟约人数相同则
// 随机发送"; 芬 band_fang "原干员在下回合传递给对应盟约人数最多的队友"). The gift is the ORIGINAL operator: an elite carrier
// is sent as that elite (community report #6 after 0.1.2: it arrived as the normal card — the gift carried baseIdOf).
// Destroying the carrier returns its pool copies (an elite's 3) at once; the gift takes them again when it is granted
// (effect:builtin_gift: grantChess → acquireChess, an elite takes up to goldenCopies, as every effect grant).
use_equip_recruit_new_char_and_give_char_to_player_most_bond: {
onEquip(ctx, ev) {
const target = ctx.piece(ev.target.uid);
@@ -142,7 +162,7 @@ const ITEM_HANDLERS = {
const tier = ctx.gd.tierOf(target.id);
const n = Math.max(1, int(paramsOf(ctx, ev.item).refresh_cnt, 2));
const bonds = ctx.pieceBonds(target.uid);
const original = ctx.gd.baseIdOf(target.id);
const original = target.id; // the elite id for an elite (report #6)
ctx.destroyPiece(target.uid);
const ids = [];
// different operators of the target's tier, topped up from the tier below like the promotion reward (item 19)
@@ -153,16 +173,8 @@ const ITEM_HANDLERS = {
}
if (ids.length) ctx.offerChess(ids, { source: 'item', tier });
// co-op: next prep, send the original chess to the teammate with the most members of its bonds
const mates = ctx.teammates();
if (!mates.length) return;
let best = -1;
let pick = [];
for (const t of mates) {
const score = bonds.reduce((s, b) => s + t.bondCount(b), 0);
if (score > best) { best = score; pick = [t]; } else if (score === best) pick.push(t);
}
const to = ctx.rng.pick(pick);
if (to) ctx.addEffect({ id: `gift:${ev.item.uid}`, key: 'effect:builtin_gift', hidden: true, battle: false, params: { toPlayerId: to.playerId, chessId: original } });
const to = mostBondMate(ctx, bonds);
if (to) ctx.addEffect({ id: `gift:${ev.item.uid}`, key: 'effect:builtin_gift', hidden: true, battle: false, params: { toPlayerId: to.playerId, chessId: original, bonds } });
},
},
sell_char_count_gain_equip_owner_bond: {
@@ -230,15 +242,25 @@ const EFFECT_HANDLERS = {
if (n > 0) ctx.addFunds(n, 'doll');
},
},
// 信标's gift: at the next round start (下个休整期) the original operator goes to the teammate picked at equip time. It
// hangs on the SENDER and runs even when the sender was eliminated in between (`afterElimination`: Match.startRound →
// EffectDispatcher.dispatchEliminated) — an eliminated player gets no onRoundStart, so the gift was lost (GitHub #86).
// A receiver eliminated meanwhile is replaced by the living teammate with the most members of the operator's bonds
// (ties at random) [ASSUMED]; with no teammate left alive the gift is dropped. The effect is removed only after the
// operator was granted: a grant that fails (no copy of it left in the shared pool, the receiver's 整备区 and temp
// full) keeps it for the next round start — the official text names no refund [ASSUMED].
builtin_gift: {
afterElimination: true,
onRoundStart(ctx) {
const ref = ctx.source.ref;
ctx.removeEffect(ref.id);
const p = ref.params || {};
const to = ctx.player(p.toPlayerId);
if (!to || !p.chessId) return;
const got = to.grantChess(p.chessId);
if (got) to.giftTicker(ctx.name, p.chessId);
const rec = p.chessId ? ctx.gd.chess(p.chessId) : null;
if (!rec) { ctx.removeEffect(ref.id); return; }
const to = ctx.player(p.toPlayerId) || mostBondMate(ctx, Array.isArray(p.bonds) ? p.bonds : rec.bonds || []);
if (!to) { ctx.removeEffect(ref.id); return; }
if (!to.grantChess(p.chessId)) return;
ctx.removeEffect(ref.id);
to.giftTicker(ctx.name, p.chessId);
},
},
// 整备: the next purchased item becomes advanced (golden)
+22
View File
@@ -33,6 +33,8 @@
// then hand) → equipped items → effects (insertion order); onPrice runs the priced chess's own 特质 first (购买价格为N
// sets the price the discounts and caps of bonds / strategies then act on — user playtest #5). Every call is
// try/catch-guarded; nested dispatch depth is capped (MAX_DEPTH) so content can never loop the server.
// An eliminated player gets no dispatch, except the persistent effects whose handler sets `afterElimination: true`
// (onRoundStart, EffectDispatcher.dispatchEliminated: 信标's gift — GitHub #86).
import { itemKey } from './gamedata.js';
import { boardOrder, parseKey, tileKey } from './board.js';
@@ -239,6 +241,26 @@ export class EffectDispatcher {
return ev;
}
/**
* `hook` for an ELIMINATED player (Match.startRound: onRoundStart): only its persistent effects whose handler sets
* `afterElimination: true` run — 信标's gift still reaches the teammate when its sender is out (builtinMeta.js
* builtin_gift, GitHub #86). Nothing else of an eliminated player is dispatched.
*/
dispatchEliminated(ps, hook, ev = {}) {
if (!ps || ps.alive || this.depth >= MAX_DEPTH) return ev;
this.depth++;
try {
for (const ref of ps.effects.slice()) {
if (!ref || typeof ref.key !== 'string') continue;
const h = this.registry.get(ref.key);
if (h && h.afterElimination === true) this._call(ps, ref.key, h, hook, { kind: 'effect', key: ref.key, ref }, ev);
}
} finally {
this.depth--;
}
return ev;
}
/** Hooks a garrison handler runs on: its eventType's hook, or what `handler.garrisonHooks(garrison)` returns. */
_garrisonHooks(h, g) {
if (typeof h.garrisonHooks === 'function') {