From 661f1c074eb1c3e3824bb99a64d6ec05dc48990e Mon Sep 17 00:00:00 2001 From: sganggs Date: Tue, 6 Oct 2026 21:08:12 +0800 Subject: [PATCH] =?UTF-8?q?feedback5=20WY:=20a=20newly=20merged=20elite=20?= =?UTF-8?q?=E6=8B=89=E6=99=AE=E5=85=B0=E5=BE=B7=20is=20a=20new=20=E6=8B=89?= =?UTF-8?q?=E6=99=AE=E5=85=B0=E5=BE=B7=20=E2=80=94=20its=20first=20manual?= =?UTF-8?q?=20refresh=20this=20round=20adds=20+8=20even=20when=20its=20cop?= =?UTF-8?q?ies=20already=20fired=20(GitHub=20#169;=20the=20owner's=20decis?= =?UTF-8?q?ion=20of=202026-10-06)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit garrison_123 「<刷新时>若为本回合首次主动刷新…此干员在整备区时也有效」 counts per piece (§21.1). No source says whether a merge resets the count; since 0.1.1 _mergeChess carried the copies' highest per-piece round count onto the elite ([ASSUMED], conservative), so two white copies that had fired this round made an elite that did not fire again. The owner decided on 2026-10-06 that a newly merged elite counts as a new 拉普兰德. server/match/player/acquire.js _mergeChess no longer carries the per-piece round counters (拉普兰德's refresh count, garrison:SERVER_GAIN_BOND_LAYER_BY_REFRESH_CNT:refreshes, is the only one): the elite starts at 0 like any new piece; an in-place promotion keeps its count. test/match/feedback1-meta.test.js: the test that locked the old inherit is updated on purpose (the elite adds +8 on its next refresh, once), and the case "two white copies refreshed this round, merge, refresh → +8" is new; both fail without the change. Docs: META (counter API, edge cases), PLAYING, the comments (PlayerState, pieces, effectsMeta, garrisons/meta), DESIGN §25.13.3 and the §25.10 table. Golden unchanged (no corpus match merges 拉普兰德 after a refresh in the same round). --- docs/META.md | 14 +++++------ docs/PLAYING.md | 2 +- docs/history/0.2.0.md | 24 +++++++++++++++--- server/match/PlayerState.js | 2 +- server/match/effectsMeta.js | 4 +-- server/match/player/acquire.js | 9 +++---- server/match/player/pieces.js | 7 +++--- server/sim/content/garrisons/meta.js | 16 ++++++------ test/match/feedback1-meta.test.js | 37 +++++++++++++++++++++++----- 9 files changed, 79 insertions(+), 36 deletions(-) diff --git a/docs/META.md b/docs/META.md index c043f17d..d29c06fd 100644 --- a/docs/META.md +++ b/docs/META.md @@ -398,8 +398,8 @@ board() hand() temp() piece(uid) pieceAt(row, col) pieceBonds(uid) garrisonsOf(u shopSlots() effect(id)` (`piece(uid)` adds `area`, `holderUid`, `idx`; "身前一格" of (r, c) is (r, c + 1)), counters `counter(k) setCounter(k, v) incCounter(k, n)` (player scope, persistent; prefix keys with your module) and `pieceCounter(uid, k) incPieceCounter(uid, k, n)` (per piece, current round only: 0 in a new round and for a new piece — -bought, granted, transformed —; a move keeps it; an elite merged this round keeps the highest of its copies' -[ASSUMED]; `PlayerState.pieceRoundCount`; prefix keys with your module too) — 拉普兰德's "本回合首次主动刷新" is the +bought, granted, transformed, or an elite merged this round (GitHub #169, the owner's decision of 2026-10-06) —; a move +keeps it; `PlayerState.pieceRoundCount`; prefix keys with your module too) — 拉普兰德's "本回合首次主动刷新" is the first manual refresh that copy witnesses (player feedback after 0.1.0: "获得该干员后该回合的首次刷新" also stacks; a copy bought after selling one this round is a new copy and fires on its own first refresh [ASSUMED]). @@ -825,11 +825,11 @@ round was over. The official 1 s `broadcastBeginDelay` is not modelled. * Promotions by effects (升华, 博士投影) keep the equipment; merges return it; 突变细胞's transformation returns it (the cell included) before its new operator is gained into the 整备区 — the carrier's tile is left empty (official footage, DESIGN §21.1). -* An elite merged in a round keeps the highest per-piece round counter of its copies (`pieceRoundCount`): an elite made - from 拉普兰德 copies that already fired this round does not fire again before the next round (conservative; the - official server's instance handling is not observable). A 拉普兰德 bought after selling one in the same round is a new - copy and fires on its own first refresh ("获得该干员后"; each such +4 costs 3 + 1 refresh − 1 refund and needs her in - the shop). +* An elite merged in a round is a new piece: its per-piece round counters start at 0 (`pieceRoundCount`), so an elite + 拉普兰德 fires +8 on its own first manual refresh of the round even when its copies already fired (GitHub #169; the + owner's decision of 2026-10-06 — a newly merged elite counts as a new 拉普兰德; until 0.2.0 it kept the highest count of + its copies). A 拉普兰德 bought after selling one in the same round is a new copy and fires on its own first refresh + ("获得该干员后"; each such +4 costs 3 + 1 refresh − 1 refund and needs her in the shop). * Chess granted by effects need a free pool copy unless `requirePool: false` (then they hold 0 copies). * Boss-round `local` pack spawns (boss parts) all spawn; content scripts (bosses.js) decide their behaviour. * The Final Assault ends as a defeat when every field finished with the boss pool above 0 (boss escaped). diff --git a/docs/PLAYING.md b/docs/PLAYING.md index 501fd0fc..ecee9352 100644 --- a/docs/PLAYING.md +++ b/docs/PLAYING.md @@ -63,7 +63,7 @@ - **干员持有(补位)**:大厅、房间和「确认本局信息」里的「干员调配」按钮打开的界面有两个标签页:「干员调配」选择每名干员携带的技能和精锐的模组;「**干员持有**」可以把你没有的干员**下掉**(点一下卡片切换「持有 / 未持有」)。和官方一样只有 55 张「普通」棋子能下掉(界面按阶级列出本赛季商店里的 53 名,每张写着由谁替补、用几技能);74 张「预设」棋子(多是 1–2 阶的常见干员)无论是否持有都由本人上场,不在其中。默认全部持有。下掉的干员和官方一样**显示为替补干员**:商店和晋升奖励的卡片、整备区和临时整备区里的单位、作战区、结算界面的阵容,都是替补干员的头像、名字和模型(例如银灰显示为 Sharp),带一个小小的「替补」标记;详情卡显示替补干员的立绘、名字、技能、天赋和模组,名字下面写着「银灰的替补」。**盟约、特质、阶级、价格、合成仍按原干员**(卡片和详情卡上的盟约、特质就是原干员的),作战时的属性、技能、天赋、模组、攻击范围、近战位 / 远程位都是替补干员的(例如银灰 → Sharp 的 2 技能「亮剑」)。同一名替补干员可能替补好几名干员(Sharp 替补忍冬、银灰、隐德来希等),它们按各自的原干员分别合成:看阶级和盟约区分,点开详情卡能看到替补的是谁。同盟模拟中队友查看你的整备区和作战区、你的盟约成员时,看到的也是替补干员。替补的技能和模组由棋子固定,「干员调配」对它不生效;替补干员没有召唤物(下掉缪尔赛思就没有流形)。这是**局外设置**:下一局开始时生效,对局中修改只保存到下一局;同盟模拟中只影响你自己的棋子,队友的棋子按队友的设置。没有官方的好友借用:想用本人上场,改回「持有」即可。「全部持有」一键恢复,「导出 / 导入」可以把设置带到别的浏览器;大厅的「干员调配」按钮上会显示「替补 N」提醒你有几名干员被下掉。 - **自选编队**:同一个界面的第三个标签页「**自选编队**」有 4 个名额:5 阶两个、6 阶两个。点名额打开选择列表,每个名额可以放一名**你持有的 6★ 干员**(棋子池里没有的那些,标「已持有」:3 个技能任选其一,精锐的模组任选其一或不装备;只在集成战略或生息演算里生效的模组不能选:ISW-A、SO-A / SO-B、RA-A)或一名**原型干员**(标「原型」:Sharp、Mechanist 等 9 名 6★,5 阶另有 6 名 4★ 预备干员;技能和模组与补位时相同,不能更改)。同一名原型干员可以同时放进一个 5 阶和一个 6 阶名额;你持有的干员只能放一个名额;同一阶的两个名额不能放同一人。列表只列出已经做好战斗逻辑的干员(原型干员全部可选,持有的 6★ 随战斗逻辑完成陆续加入)。对局中,自选干员**只出现在你自己的商店里**:调度中心升到 5 级后才会刷出 5 阶名额的干员,6 级后才会刷出 6 阶的;刷新概率和同阶干员一样,每个名额有自己的库存(5 阶 8 张、6 阶 5 张,不与队友共用),价格与同阶干员相同,晋升奖励里也可能出现;效果、奖励或机变卡「获得一名随机干员」时,也可能抽到你自己的自选干员(按那个效果自己的阶级规则,不看调度中心等级)。三张合成精锐,精锐时所选模组生效(5 阶 1 级、6 阶 3 级)。自选干员**没有特质**,盟约按干员的实际阵营分配(推进之王是维多利亚,煌同时是炎和维多利亚,没有对应阵营的是协防干员),和普通棋子一样计入盟约人数;本局禁用了它全部的盟约时,它不会出现在商店里(协防不会被禁用),开局的「确认本局信息」、选策略时的「本局信息」和对局中的「本局信息」都会在「本局禁用干员」下写明是哪一名。商店卡片、整备区、作战区和详情卡上都标着「自选」,详情卡显示所选的技能和模组。这同样是**局外设置**:下一局开始时生效,对局中修改只保存到下一局;AI 队友不使用自选;你开启 AI 托管时,AI 会优先买你的自选干员,并在快能刷出自选名额时提早一回合升级调度中心;信标不会把自选干员送给队友。「导出 / 导入」「全部清空」可用,大厅的「干员调配」按钮上会显示「自选 N」。 - 每回合商店会自动刷新未冻结的格子。 -- **「本回合首次主动刷新」按每名干员各自计算**(拉普兰德):她在本回合遇到的第一次主动刷新生效,在整备区里也算——先刷新、再买下拉普兰德,下一次刷新她仍会叠层;同一回合内用已经生效过的拉普兰德合成的精锐,本回合不再生效。 +- **「本回合首次主动刷新」按每名干员各自计算**(拉普兰德):她在本回合遇到的第一次主动刷新生效,在整备区里也算——先刷新、再买下拉普兰德,下一次刷新她仍会叠层;本回合新合成的精锐算作一名新的拉普兰德——即使合成用的几名本回合已经生效,精锐在本回合的下一次主动刷新仍会 +8。 ## 4. 摆阵与朝向 diff --git a/docs/history/0.2.0.md b/docs/history/0.2.0.md index c9af355c..9d879044 100644 --- a/docs/history/0.2.0.md +++ b/docs/history/0.2.0.md @@ -22,8 +22,8 @@ rules (GitHub #181, #187 / #170, follow-ups) → §25.7; WE2, data / match / UI what stays [ASSUMED] and the questions left to the owner → §25.10. The closing additions: the owner's decisions of 2026-10-06 (望's stock, skill triggers through summon areas, the 突袭 landing, GitHub #148) and the items the sources settled (泥岩's 闭锁; 忍冬, 深靛, 阿) → §25.11; release packaging and the asset manifest (the 自选 voices, the summons -with no Spine model) → §25.12; round 18 — 拟态物质 at the copy cap (GitHub #207), 华法琳's 7 / 14 (GitHub #175, PR #192) -→ §25.13. +with no Spine model) → §25.12; round 18 — 拟态物质 at the copy cap (GitHub #207), 华法琳's 7 / 14 (GitHub #175, PR #192), +a merged elite 拉普兰德 as a new one (GitHub #169) → §25.13. Decisions of the owner: 2026-10-04 — the server stays on plain `node:http` + `ws`, no web framework (§25.1); 2026-10-05 — the 补位 plan (§25.3); the 自选编队 rules, the collab operators out of the pool, ISW-A refused (§25.4); @@ -33,7 +33,7 @@ enemy in range, 纯烬艾雅法拉 S1 at full SP, 伺夜 S1 only with his pack o does not own shows as its stand-in everywhere (the owner's recall of the official mode, §25.3); the enemy damage frame kept (§25.7.2); 望's placed 棋子 count in her stock, skills that act through summons cast on an enemy in a summon's area, a melee 突袭 member lands where it can block (§25.11); 华法琳's granted trait at the data's 7 / 14, replacing the 12 / 24 -lock (§25.13.2). +lock (§25.13.2); a newly merged elite 拉普兰德 counts as a new 拉普兰德 (§25.13.3). ### 25.1 Refactor and tooling: the big files split, golden results, lint, types, import boundaries (refactor plan; owner's decision of 2026-10-04: no web framework) — `server/http/`, `server/match/match/`, `server/match/player/`, `server/sim/battle/`, `content/enemies/`, `kits/ops/`, `public/js/ui/gameLogic/`, `render/fx/`, `render/app/`, `screens/game/`, `tools/golden.mjs`, `tools/check-imports.mjs`, `eslint.config.js`, `jsconfig.json` @@ -662,6 +662,7 @@ its tile, §18.1; §25.9.2). Where a 0.1.4 change met 0.2.0 work: | Skills that act through summons cast on an enemy in a summon's area: 麦哲伦 S1, 令 S3, 电弧 S2 / S3 (§25.11.2) | the owner, 2026-10-06 (the larger-range rule) | | A melee 突袭 member lands on a tile where it can block before one where it cannot (GitHub #148, §25.11.3) | the owner, 2026-10-06 | | 华法琳's granted trait stops at the data's 7 / 14 (PRTS 3/27: lowered from 12 / 24; GitHub #175, PR #192, §25.13.2) | the owner, 2026-10-06 (replaces the 12 / 24 lock) | +| A newly merged elite 拉普兰德 is a new 拉普兰德: +8 on its first manual refresh of the round (GitHub #169, §25.13.3) | the owner, 2026-10-06 (was [ASSUMED]: the copies' count carried) | Settled by the sources in W5V, no longer questions: 泥岩 S3's dormancy is 闭锁 — she lets go and blocks nobody (PRTS, §25.11.4); 阿 S2 / S3 keep the data's DEFAULT, 忍冬 S2's and 深靛 S1's notes are what the kits do (§25.11.5). @@ -876,3 +877,20 @@ ported PR credits its author there and here. `test/content/garrisons_battle.test.js` (updated on purpose: the cap is the data's 7 / 14 — the 7th cast reaches it, the 8th adds nothing), `test/match/feedback1-gaps.test.js` (the client-result bound with 华法琳 + 古米: 60 + 4·3 + 2 × cap passes, one more is refused), `test/ui/playtest3.test.js` (both cards show 至多7层 / 至多14层). + +#### 25.13.3 A newly merged elite 拉普兰德 is a new 拉普兰德: its first manual refresh this round adds +8 (GitHub #169; the owner's decision of 2026-10-06) — `server/match/player/acquire.js _mergeChess`, `sim/content/garrisons/meta.js SERVER_GAIN_BOND_LAYER_BY_REFRESH_CNT` + +- **Seen** (GitHub #169, new comments): two white 拉普兰德 that had already fired this round merged into the elite, and + the elite's next refresh added nothing. The triage confirmed the rest of the report is what the code does: each white + copy fires +4 on the first manual refresh it witnesses, also in the 整备区 (§21.1). +- **Official**: garrison_123 「<刷新时>若为本回合首次主动刷新,使已激活的【叙拉古】层数+4,此干员在整备区时也有效」 (the elite +8; + bb `refresh_cnt` 1); PRTS 下半 3月27日更新#2 changed only the amounts (+3 / +6 → +4 / +8). No source says whether a merge + resets the count; the code carried the copies' highest count onto the elite ([ASSUMED], conservative, since 0.1.1). +- **Decision**: the owner's decision of 2026-10-06 — a newly merged elite counts as a new 拉普兰德. +- **Now**: `_mergeChess` carries no per-piece round counter over (拉普兰德's refresh count is the only one): the elite starts + at 0 like any new piece, so its next manual refresh this round adds +8 and a later one nothing; an in-place promotion + (升华, 博士投影) keeps the piece and its count. A copy re-bought after a sale is still a new copy ([ASSUMED], unchanged). +- Golden: unchanged (no corpus match merges 拉普兰德 after a refresh in the same round). Tests: + `test/match/feedback1-meta.test.js` (updated on purpose: the elite made from copies that already fired adds +8 on its + next refresh, once; new: two white copies refreshed this round, merge, refresh → +8). Docs: docs/META.md (the counter + API, the edge cases), docs/PLAYING.md, the code comments. diff --git a/server/match/PlayerState.js b/server/match/PlayerState.js index 1c67f592..5088a520 100644 --- a/server/match/PlayerState.js +++ b/server/match/PlayerState.js @@ -35,7 +35,7 @@ // outside PREP (a SETTLE merge's elite waits in temp for the next prep). // * Per-piece round counters (pieceRoundCount, piece.meta.round): an operator's own counts of the current round // (拉普兰德: the manual refreshes she witnessed — player feedback after 0.1.0); a new piece starts at 0, an elite -// merged this round keeps the highest of its copies' [ASSUMED]. +// merged this round too (a new 拉普兰德 — GitHub #169, the owner's decision of 2026-10-06). // * Transformations (transformChess, 突变细胞 — PRTS 备注 "生效时,原干员销毁,获得一名高一阶的随机初始干员"): a destroy // followed by a gain. The carrier leaves wherever it stands (a board tile is freed, the deploy count drops), its // equipment — the cell included — returns to the hand first (overflow temp), then the new chess is gained like any diff --git a/server/match/effectsMeta.js b/server/match/effectsMeta.js index 7ccfb73f..df1bdc84 100644 --- a/server/match/effectsMeta.js +++ b/server/match/effectsMeta.js @@ -493,8 +493,8 @@ export function makeCtx(m, ps, source, hook, ev = null) { counter: (k) => (Number.isFinite(ps.counters[k]) ? ps.counters[k] : 0), setCounter: (k, v) => { if (typeof k === 'string' && Number.isFinite(v)) ps.counters[k] = v; return ps.counters[k] ?? 0; }, incCounter: (k, n = 1) => { if (typeof k !== 'string' || !Number.isFinite(n)) return 0; ps.counters[k] = (Number.isFinite(ps.counters[k]) ? ps.counters[k] : 0) + n; return ps.counters[k]; }, - /** Per-piece counter of the current round (0 in a new round / for a new piece; an elite merged this round keeps the - * highest of its copies' — PlayerState.pieceRoundCount). */ + /** Per-piece counter of the current round (0 in a new round / for a new piece, an elite merged this round included — + * PlayerState.pieceRoundCount). */ pieceCounter: (uid, k) => { const l = ps.find(uid); return l ? ps.pieceRoundCount(l.piece, k) : 0; }, incPieceCounter: (uid, k, n = 1) => { const l = ps.find(uid); return l ? ps.bumpPieceRoundCount(l.piece, k, n) : 0; }, diff --git a/server/match/player/acquire.js b/server/match/player/acquire.js index 49887ed1..8f72ec72 100644 --- a/server/match/player/acquire.js +++ b/server/match/player/acquire.js @@ -98,13 +98,10 @@ export class PlayerAcquire { for (const it of l.piece.items || []) items.push(it); l.piece.items = []; } + // a new piece: this round's per-piece counters start at 0, none is carried over from the copies — a newly merged + // elite 拉普兰德 is a new 拉普兰德 and fires +8 on its own first manual refresh this round (GitHub #169; the owner's + // decision of 2026-10-06; pieceRoundCount) const elite = this.newPiece('chess', goldenId, { poolCopies: copies }); - // this round's per-piece counters: the highest of the copies' (an elite made from 拉普兰德 that already saw their - // first refresh this round does not fire again this round — [ASSUMED] conservative, pieceRoundCount) - for (const l of consumed) { - const rc = l.piece.meta && l.piece.meta.round; - if (rc && rc.r === this.m.round) for (const [k, v] of Object.entries(rc.n)) this.bumpPieceRoundCount(elite, k, Math.max(0, v - this.pieceRoundCount(elite, k))); - } const deployed = consumed.filter((l) => l.key && !this.board.has(l.key)).map((l) => ({ key: l.key, dir: pieceDir(l.piece) })); const toTile = (t) => { elite.dir = parseDir(t.dir) || 'RIGHT'; this.board.set(t.key, elite); return 'board'; }; const tile = mergeTile(deployed, (r, c) => this._legal(elite, r, c)); diff --git a/server/match/player/pieces.js b/server/match/player/pieces.js index e852c6a1..b8309af2 100644 --- a/server/match/player/pieces.js +++ b/server/match/player/pieces.js @@ -114,9 +114,10 @@ export class PlayerPieces { /** * Per-piece counter of the current round (`piece.meta.round` = { r, n: { key: count } }): 0 for a key not counted yet - * this round. The counters belong to the operator: a move keeps them, a new piece (bought, granted, transformed) - * starts at 0, and an elite merged this round keeps the highest count of its copies (_mergeChess) — 拉普兰德's - * "本回合首次主动刷新" is the first manual refresh she witnesses (player feedback after 0.1.0, garrisons/meta.js). + * this round. The counters belong to the operator: a move keeps them, a new piece (bought, granted, transformed, or + * an elite merged this round — _mergeChess carries none over: GitHub #169, the owner's decision of 2026-10-06) + * starts at 0 — 拉普兰德's "本回合首次主动刷新" is the first manual refresh she witnesses (player feedback after 0.1.0, + * garrisons/meta.js). */ pieceRoundCount(piece, key) { const rc = piece && piece.meta && piece.meta.round; diff --git a/server/sim/content/garrisons/meta.js b/server/sim/content/garrisons/meta.js index 2ef0c797..6931434e 100644 --- a/server/sim/content/garrisons/meta.js +++ b/server/sim/content/garrisons/meta.js @@ -22,8 +22,9 @@ // of any tier (the text gives no tier cap); a bond without an available chess falls through to the next tied one. // * [ASSUMED] 松果: the "免费特殊招募" is a free pick-one offer of `rewardOffer.count` (3) chess of the pool's tier. // * 拉普兰德 SERVER_GAIN_BOND_LAYER_BY_REFRESH_CNT "若为本回合首次主动刷新": per copy — the first manual refresh this -// operator witnesses in the round (players' report after 0.1.0); [ASSUMED] an elite merged this round keeps its -// copies' count, and a copy bought after selling one this round is a new copy (fires on its own first refresh). +// operator witnesses in the round (players' report after 0.1.0); an elite merged this round is a new 拉普兰德 and +// fires +8 on its own first one (GitHub #169, the owner's decision of 2026-10-06); [ASSUMED] a copy bought after +// selling one this round is a new copy (fires on its own first refresh). // "本回合每刷新过1次" (SERVER_ADD_REFRESH_CNT_MULTIPLIER_BOND_LAYER, 阿罗玛 / 安洁莉娜 / 售出时) // fires on another event and reads the player's refreshes of the round (roundStats), like 本回合每获得过 / 每花费. @@ -198,11 +199,12 @@ H.SERVER_ADD_REFRESH_CNT_MULTIPLIER_BOND_LAYER = { // refreshes this copy witnessed this round (board or hand), so a 拉普兰德 bought after the round's first refresh still // fires on the next one (players' report after 0.1.0: "获得该干员后该回合的首次刷新" also stacks — the official behaviour; // read as each trait instance counting its own SERVER_REFRESH_SHOP triggers against bb.refresh_cnt). A re-triggered trait -// (ev.trigger) is no manual refresh: it neither fires nor counts. A new copy (bought, granted) starts at 0. [ASSUMED]: -// the copies of an elite merged this round pass on their highest count (PlayerState.pieceRoundCount — no second trigger -// that round, conservative); 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). +// (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 +// 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) H.SERVER_GAIN_BOND_LAYER_BY_REFRESH_CNT = { onRefresh(ctx, ev) { diff --git a/test/match/feedback1-meta.test.js b/test/match/feedback1-meta.test.js index 9c1b51fd..c1bd2af2 100644 --- a/test/match/feedback1-meta.test.js +++ b/test/match/feedback1-meta.test.js @@ -2,7 +2,8 @@ // g.move / g.equip, the round loop's SETTLE): // #1 拉普兰德 garrison_123 "<刷新时>若为本回合首次主动刷新,使已激活的【叙拉古】层数+4(+8),此干员在整备区时也有效": the // players' official behaviour — "获得该干员后该回合的首次刷新" also stacks. The refresh count is the 拉普兰德's own -// (each copy counts the manual refreshes it witnessed this round), not the player's. +// (each copy counts the manual refreshes it witnessed this round), not the player's; an elite merged this round is a +// new 拉普兰德 and starts at 0 (GitHub #169, the owner's decision of 2026-10-06). // #4 昆图斯 突变细胞 "战斗结束后,装备者替换为高一阶的随机干员": the cell is not consumed — the original operator is // destroyed (PRTS 备注 "生效时,原干员销毁,获得一名高一阶的随机初始干员(最高六阶)") and its equipment, the cell // included, returns to the hand, to be equipped again ("之后就是一直打针,扎到核心卡…就换人扎"); then the new operator @@ -94,7 +95,7 @@ test('#1 players\' scenario: 普罗旺斯 + 德克萨斯 deployed, refresh, buy m.dispose(); }); -test('#1 each copy counts its own refreshes: a bench copy (整备区时也有效) bought later fires on its own first refresh; an elite keeps "already fired"', () => { +test('#1 each copy counts its own refreshes: a bench copy (整备区时也有效) bought later fires on its own first refresh; an elite merged this round is a new 拉普兰德 (GitHub #169)', () => { const s = setup(); const { m, ps } = s; s.activate(); @@ -107,18 +108,20 @@ test('#1 each copy counts its own refreshes: a bench copy (整备区时也有效 assert.equal(s.L(), 8, 'copy B: its first refresh (A already fired this round)'); s.refresh(); assert.equal(s.L(), 8, 'nothing more this round'); - // the third copy completes the elite: A and B already fired this round, so the elite does not fire again this round - // [ASSUMED: conservative — the elite keeps the highest refresh count of its copies] + // the third copy completes the elite: a new 拉普兰德 (the owner's decision of 2026-10-06; until 0.2.0 it kept its + // copies' count and did not fire again this round) — its own first refresh of the round adds +8 const elite = s.buy(LAP); assert.equal(elite.id, LAP_B, 'merged into the elite'); assert.ok(!ps.find(a.uid) && !ps.find(b.uid), 'copies consumed'); s.refresh(); - assert.equal(s.L(), 8, 'the elite made this round from copies that already fired: no second trigger'); + assert.equal(s.L(), 8 + 8, 'the elite made this round from copies that already fired: its own first refresh +8'); + s.refresh(); + assert.equal(s.L(), 16, 'once'); s.h.toPrep(2); ps.funds = 50; s.activate(); s.refresh(); - assert.equal(s.L(), 8 + 8, 'R2: the elite fires +8 on the round\'s first refresh'); + assert.equal(s.L(), 16 + 8, 'R2: the elite fires +8 on the round\'s first refresh'); checkInvariants(m); m.dispose(); }); @@ -146,6 +149,28 @@ test('#1 an elite merged from copies that had not fired yet this round fires on m.dispose(); }); +test('#1 two white copies refreshed this round, merge, refresh → +8: the newly merged elite is a new 拉普兰德 (GitHub #169; the owner\'s decision of 2026-10-06)', () => { + const s = setup(); + const { m, ps } = s; + s.activate(); + const a = give(m, ps, LAP, 'hand'); + const b = give(m, ps, LAP, 'hand'); + s.refresh(); + assert.equal(s.L(), 8, 'both white copies fire on the refresh they witness: 4 + 4'); + s.refresh(); + assert.equal(s.L(), 8, 'each only once'); + const elite = s.buy(LAP); + assert.equal(elite.id, LAP_B, 'the third copy merges'); + assert.ok(!ps.find(a.uid) && !ps.find(b.uid), 'copies consumed'); + assert.equal(ps.pieceRoundCount(ps.find(elite.uid).piece, `${KEY}:refreshes`), 0, 'the elite carries no refresh count'); + s.refresh(); + assert.equal(s.L(), 16, 'its first manual refresh this round adds +8'); + s.refresh(); + assert.equal(s.L(), 16, 'nothing more this round'); + checkInvariants(m); + m.dispose(); +}); + test('#1 only manual refreshes count: a re-triggered "刷新时" trait (ctx.triggerGarrisons) neither fires nor uses up her first refresh', () => { const s = setup(); const { m, ps } = s;