feedback6 R21E: a refresh runs the 刷新时 特质 of the chess that stood on the board / in the hand when it happened (EffectDispatcher.dispatch takes them before its first handler; each still runs where it stands now, one that left is skipped): the 拉普兰德 贾维 grants on every 6th refresh — or the elite that gift completes — fired in the very refresh that granted her and spent her 「本回合首次主动刷新」; now her first is the next manual refresh; round counters are still not copied on a merge (a merged elite is a new 拉普兰德, §25.13.3); the part of PR #196 0.2.0 lacked, re-implemented; golden: unchanged

This commit is contained in:
sganggs committed 2026-10-07 18:02:48 +08:00
1 parent c225559d4d
commit ce2bd75b49
4 files changed
+68 -5

No files matched your search

+1 -1
View File
@@ -371,7 +371,7 @@ that per garrison with `garrisonHooks(garrison) → hook[]`, e.g. "<进入休整
| `SERVER_GAIN` 获得时 | `onGain` | the gained piece — run **×2 while 投资人 is active, ×3 at ≥ 100 投资人 layers** |
| `SERVER_PREP_START` 进入休整期时 | `onRoundStart` | owned chess: board, and hand unless `bbStr.conditionkey === 'character_target_inboard'` |
| `SERVER_PREP_FIN` 休整期结束时 | `onPrepEnd` | same |
| `SERVER_REFRESH_SHOP` 刷新时 | `onRefresh` | same — manual refreshes only (`ev.trigger` marks a re-run); 拉普兰德's "本回合首次主动刷新" counts per copy (`incPieceCounter`) |
| `SERVER_REFRESH_SHOP` 刷新时 | `onRefresh` | same, as they stood when the refresh happened (taken before its first handler: a chess gained during the dispatch — 贾维's gift, the elite it completes — waits for the next refresh; one that left is skipped) — manual refreshes only (`ev.trigger` marks a re-run); 拉普兰德's "本回合首次主动刷新" counts per copy (`incPieceCounter`) |
| `SERVER_CHESS_SOLD` 售出时 | `onSold` | the sold piece |
| `SERVER_PRICE` 购买价格 | `onPrice` (first) | the chess in the priced slot (`ctx.source.where === 'shop'`); SERVER_CHESS_PRICE `bb.price` is the discount off the tier price (至简 3 − 2 = 1, 红豆 2 − 1 = 1: both texts say 购买价格为1) |
| `IN_BATTLE` | — | battle side (server/sim/content/garrisons.js `install`) |
+29 -3
View File
@@ -18,7 +18,9 @@
// onRoundStart, SERVER_PREP_FIN→onPrepEnd, SERVER_CHESS_SOLD→onSold of the sold piece, SERVER_PRICE→onPrice of the
// slot's chess, SERVER_REFRESH_SHOP→onRefresh); the dispatcher calls `handler[hook] ?? handler.run`. A handler may
// widen that per garrison with `garrisonHooks(garrison) → hook[]` (e.g. "<进入休整期时><休整期结束时>"). Owned-piece
// garrisons fire for board pieces, and for hand pieces unless bbStr.conditionkey is 'character_target_inboard'.
// garrisons fire for board pieces, and for hand pieces unless bbStr.conditionkey is 'character_target_inboard'. A
// refresh (onRefresh) runs those that stood on the board / in the hand when it happened (taken before its first
// handler): a chess gained during its dispatch — 贾维's gift, the elite that gift completes — waits for the next refresh.
// 投资人 (investShip) active ⇒ SERVER_GAIN garrisons run ×2 (×3 at ≥ 100 layers) — owned by the dispatcher.
// ctx.triggerGarrisons(uid, eventType) re-runs another piece's garrisons (铃兰, "触发…的获得时效果", 特质相同).
// Items: onEquip / onArt / onDestroy go to the item's own handler only; every other hook runs for items equipped on
@@ -204,6 +206,10 @@ export class EffectDispatcher {
if (deferItems) ps._deferItemMerge = (ps._deferItemMerge || 0) + 1;
try {
const reg = this.registry;
// onRefresh: the board / hand chess as they stood when the refresh happened, taken before any handler runs — a
// chess gained during this dispatch (贾维's gift on the 6th refresh, the elite its copy completes) was not there
// when it happened: its 刷新时 特质 waits for the next manual refresh (拉普兰德's "本回合首次主动刷新", PR #196)
const refreshed = hook === 'onRefresh' ? refreshPieces(ps) : null;
// 0. onPrice: the priced chess's own 特质 first — 购买价格为N defines the price every other modifier acts on
if (hook === 'onPrice') this._garrisons(ps, hook, ev);
// 1. globals
@@ -221,7 +227,7 @@ export class EffectDispatcher {
if (h) this._call(ps, key, h, hook, { kind: 'bond', key, bondId, bond: ps.bonds[bondId] ?? null }, ev);
}
// 4. garrisons (onPrice: already run as step 0)
if (hook !== 'onPrice') this._garrisons(ps, hook, ev);
if (hook !== 'onPrice') this._garrisons(ps, hook, ev, refreshed);
// 5. equipped items (not for the item-specific hooks): every [holder, item] pair of the owned chess, taken before
// the first item runs; each runs only while still equipped on its still-owned holder — handlers move / destroy
// pieces (header). Taking the pairs per holder as the walk reached it ran an item equipped meanwhile onto a later
@@ -279,7 +285,12 @@ export class EffectDispatcher {
return [GARRISON_HOOK[g.eventType]];
}
_garrisons(ps, hook, ev) {
/**
* Run the owned chess's garrisons of `hook`. `refreshed` (onRefresh): the board / hand chess taken when the dispatch
* began (refreshPieces) — each runs if it is still on the board or in the hand (where it stands now); a chess gained
* meanwhile does not run, nor one that left (sold, consumed by a merge, moved to the 临时整备区).
*/
_garrisons(ps, hook, ev, refreshed = null) {
const gd = ps.gd || this.m.gd;
const run = (piece, where) => {
const rec = gd.chess(piece.id);
@@ -304,6 +315,13 @@ export class EffectDispatcher {
return;
}
if (hook !== 'onRoundStart' && hook !== 'onPrepEnd' && hook !== 'onRefresh') return;
if (refreshed) {
for (const piece of refreshed) {
const loc = ps.find(piece.uid);
if (loc && loc.piece === piece && (loc.area === 'board' || loc.area === 'hand')) run(piece, loc.area);
}
return;
}
for (const { piece } of boardOrder(ps.board)) if (piece.kind === 'chess') run(piece, 'board');
for (const p of ps.hand) if (p && p.kind === 'chess') run(p, 'hand');
}
@@ -385,6 +403,14 @@ function ownedChess(ps) {
return out;
}
/** The board chess (reading order) then the hand chess — the pieces whose 刷新时 特质 a refresh runs (onRefresh). */
function refreshPieces(ps) {
const out = [];
for (const { piece } of boardOrder(ps.board)) if (piece.kind === 'chess') out.push(piece);
for (const p of ps.hand) if (p && p.kind === 'chess') out.push(p);
return out;
}
/** Dispatch step 5: is `it` still equipped on `holder`, and `holder` still an owned chess of `ps`? */
function stillEquipped(ps, holder, it) {
if (!Array.isArray(holder.items) || !holder.items.includes(it)) return false;
+3 -1
View File
@@ -202,7 +202,9 @@ H.SERVER_ADD_REFRESH_CNT_MULTIPLIER_BOND_LAYER = {
// (ev.trigger) is no manual refresh: it neither fires nor counts. A new copy (bought, granted) starts at 0, and so does
// an elite merged this round: a new 拉普兰德, its own first manual refresh this round adds +8 even when its copies
// already fired (GitHub #169; the owner's decision of 2026-10-06 — PlayerState._mergeChess carries no counter over).
// [ASSUMED]: a copy bought after selling one this round is a new copy — "获得该干员后" — and fires on its own first
// A copy gained by a refresh's own effects (贾维's gift on every 6th refresh, or the elite that gift completes) did not
// witness that refresh: the dispatcher runs the chess that stood there when it happened (effectsMeta.js onRefresh), so
// her first is the next manual refresh (PR #196). [ASSUMED]: a copy bought after selling one this round is a new copy — "获得该干员后" — and fires on its own first
// refresh (the server cannot tell it from any other copy; each such +4 costs her price + a refresh − the 1-fund refund,
// and needs her in the shop again).
const REFRESH_CNT_KEY = 'garrison:SERVER_GAIN_BOND_LAYER_BY_REFRESH_CNT:refreshes'; // per-piece counter (module-prefixed)
+35
View File
@@ -184,6 +184,41 @@ test('#1 only manual refreshes count: a re-triggered "刷新时" trait (ctx.trig
m.dispose();
});
// PR #196 (the part 0.2.0 lacks): 贾维 【团伙行动】 grants a 叙拉古 operator on every 6th manual refresh from that refresh's
// own dispatch (band step, before the garrisons). The 拉普兰德 it grants — or the elite its copy completes — is gained
// after the refresh happened: her "本回合首次主动刷新" is the next manual one. The dispatcher walks the board and hand as
// they stood when the refresh happened (EffectDispatcher.dispatch onRefresh snapshot); it walked them live, so she fired
// (and spent her first refresh) in the refresh that granted her. Round counters are not copied on a merge (§25.13.3).
for (const variant of ['a new copy', 'a copy that completes the elite']) {
test(`#1 贾维 refresh gift (${variant}, PR #196): the 拉普兰德 waits for the next manual refresh`, () => {
const s = setup();
const { m, ps } = s;
ps.bandId = 'band_chiave';
ps.shop.level = 2;
// 贾维's draw (ctx.rollChess on the meta RNG) gives 拉普兰德; the shop rolls stay as they are
const roll = m.pool.roll.bind(m.pool);
m.pool.roll = (rng, opts = {}) => (rng === m.rngMeta && typeof opts.filter === 'function' && opts.filter(LAP) ? LAP : roll(rng, opts));
s.activate();
const elite = variant !== 'a new copy';
if (elite) { give(m, ps, LAP, 'hand'); give(m, ps, LAP, 'hand'); }
for (let i = 0; i < 5; i++) s.refresh();
const before = s.L();
assert.equal(before, elite ? 8 : 0, 'the copies already owned fire on the round\'s first refresh');
assert.ok(!ps.allChess().some((p) => p.id === LAP_B) && ps.allChess().filter((p) => p.id === LAP).length === (elite ? 2 : 0));
s.refresh(); // the 6th: 贾维's gift
const lap = ps.allChess().find((p) => p.id === (elite ? LAP_B : LAP));
assert.ok(lap, elite ? 'the gift completed the elite' : 'the gift is owned');
assert.equal(s.L(), before, 'not in the refresh that granted her');
s.refresh();
assert.equal(s.L(), before + (elite ? 8 : 4), 'her first manual refresh is the next one');
s.refresh();
assert.equal(s.L(), before + (elite ? 8 : 4), 'once a round');
assert.equal(m.dispatcher.errors, 0);
checkInvariants(m);
m.dispose();
});
}
// ---------------------------------------------------------------------------------------------------------------------
// #4 突变细胞