diff --git a/docs/PLAYING.md b/docs/PLAYING.md index 29f02be4..f4aa2d87 100644 --- a/docs/PLAYING.md +++ b/docs/PLAYING.md @@ -110,7 +110,7 @@ - **本局禁用**:「标准模拟」与官方相同,关闭拉特兰、阿戈尔、卡西米尔、灵巧、奥术、奇迹、投资人、突袭、独行、绝技这 10 个盟约,它们不会激活。你有这些盟约的成员时(奇迹、投资人也算整备区里的成员),盟约栏末尾显示**灰色带 ✕ 的盟约图标和「本局禁用」**,干员卡、盟约标签和盟约详情也这样标注。「远见」「奇迹」「投资人」计入整备区,盟约详情的「成员」人数也一样计入整备区里的成员(临时整备区不算)。 - **调和**:激活时,场上已有成员的核心盟约人数各 +1(只加 1 人,不论有几名调和干员)。盟约详情会写「含调和 +1」,成员列表上方列出带来这 1 人的调和干员(如缪尔赛思)。 - **炎佑**:场上 6 名以上「炎」干员时召唤炎佑(9 名时两只)。它追踪全场仇恨最高的敌人并悬停在其上方,技能「祛恶之焰」锁定 1 个目标**持续喷火**(最长 20 秒,每秒对目标周围的敌人造成法术伤害并附带灼燃,喷火时不普攻,目标死亡或被沉默时结束);场上暂时没有敌人时停在原地等待。它持有官方的「孤立」:医疗干员不会治疗它,友方的增益、光环和天赋也不会选中它(敌人照常可以攻击它)。 -- **层数**:盟约通过各种效果叠加层数,层数越高效果越强(例如「炎」攻击力随层数提升、「远见」每 10 层给资金)。**层数整局保留,但只有盟约激活时才生效**,多数情况下也只有激活的盟约能叠层。每个盟约最多 **999 层**(与官方相同,取自官方客户端的常量):到 999 后再获得的层数直接作废,按层数发放的奖励(如「远见」的资金)也随之停止。作战中获得的层数在盟约栏里**实时**显示,作战结束后一直保留到结算(联防期间也不会退回去)。 +- **层数**:盟约通过各种效果叠加层数,层数越高效果越强(例如「炎」攻击力随层数提升、「远见」每 10 层给资金)。**层数整局保留,但只有盟约激活时才生效**,多数情况下也只有激活的盟约能叠层。每个盟约最多 **999 层**(与官方相同,取自官方客户端的常量):到 999 后再获得的层数直接作废,按层数发放的奖励(如「远见」的资金)也随之停止。作战中获得的层数在盟约栏里**实时**显示,作战结束后一直保留到结算(联防期间也不会退回去)。「调和」「协防干员」「独行」「绝技」与官方一样**不显示层数**(盟约栏、详情和结算卡片都不显示),但叠层的特质 / 策略 / 装备照样给它们加层(PRTS 盟约记录:「部分盟约不会显示叠加层数,但是叠加层数的特质/策略/装备等效果仍然对其生效」),这些层数也计入激活层数总和;它们的效果本身与层数无关。 - **加成怎么叠**:盟约、策略、装备、机变卡和特质给的「攻击力 / 防御力 / 生命值 +X%」与技能的「攻击力 +X%」**相加**后再乘到属性上(官方:直接乘算),例如精准 +130%、独行 +60%、一件 +40% 的装备合计 +230%,而不是 ×2.3×1.6×1.4。「伤害提升至 X%」「受到的伤害 +X%」(维多利亚、谢拉格、奥术、脆弱等)这类伤害倍率之间仍然相乘;但同名效果只算一个——同一战场两名玩家都开了「奥术」时,敌人身上只挂一层奥术易伤,取两人中较高的那一个(不再相乘);两名「灵知」的坚冰、两名「莱恩哈特」/「缄默德克萨斯」的法抗削减也一样只算最强的一个。「坚守」的反伤计入被打的那名成员(个人对领袖伤害统计里能看到)。 - 每局会随机**禁用**若干盟约(标准模拟较少),它们的干员不会出现在商店;开局的「确认本局信息」会列出,选择策略时(「查看禁用盟约与干员」)和对局中(左上角 🔍)也能再查看。 - 两件同名装备会合成一件更强的装备。休整期结束时才到手的那一件(维多利亚每 25 层发的维式重锤是这种情况)先留在整备区,已经戴在干员身上的不会为了这场战斗被卸下,合成出的进阶装备在**下一休整期开始**时出现;其他时候拿到的同名装备仍立刻合成。整备区和临时整备区都满时,这件新装备照旧销毁(「整备区已满,获得的装备已销毁」),不会换进装备栏。特定的装备组合可以为干员赋予额外的盟约效果:「变形同构体」与另一件盟约装备(如维式重锤)一同携带时,携带者视为该装备对应盟约的成员。对应关系见变形同构体详情卡的「天赋」一栏(本局禁用的盟约会标出;装在干员身上时,正在生效的那一组会高亮)。 diff --git a/docs/research/02-bonds.md b/docs/research/02-bonds.md index 9a0b0699..ab23af11 100644 --- a/docs/research/02-bonds.md +++ b/docs/research/02-bonds.md @@ -58,7 +58,7 @@ Core (isPower, bondType SEASON): 炎 萨尔贡 维多利亚 谢拉格 拉特兰 ### 2.2 Layers (层数) - Each player has one integer layer counter L per bond (23 counters). All start at 0 when the simulation starts, are never reset between rounds and are never reduced (no layer-loss effect exists in the data) [ASSUMED: never reduced]. -- `noStack: true` hides layer counts for 调和 / 协防干员 / 独行 / 绝技; it does not prohibit gains or clear stored layers. Their effects depend on member thresholds, but internal layers still participate in active-layer totals and unite ordering. [PRTS 下半盟约记录](https://prts.wiki/w/卫戍协议:盟约_下半/PRTS盟约记录): hidden-layer bonds still accept trait / strategy / equipment layer gains (review correction, 2026-10-04). +- `noStack: true` hides layer counts for 调和 / 协防干员 / 独行 / 绝技; it does not prohibit gains or clear stored layers. Their effects depend on member thresholds, but internal layers still participate in active-layer totals and unite ordering. [PRTS 下半盟约记录](https://prts.wiki/w/卫戍协议:盟约_下半/PRTS盟约记录): hidden-layer bonds still accept trait / strategy / equipment layer gains (review correction, 2026-10-04) — its header: 「下述盟约中部分盟约不会显示叠加层数,但是叠加层数的特质/策略/装备等效果仍然对其生效」. The client hides the count on the bond strip and popup (0.1.4, PR #66) and on the result card (0.2.0, `screens/result.js resultBonds`: no number, not ranked by it). - L is kept while the bond is inactive, but bond effects only apply while the bond is active (PRTS 盟约记录). - Wording contract used by every source: "使已激活的【X】层数+N" -> add only if bond X is active at the moment of the trigger; "(无需激活盟约)" -> add even if X is inactive. "自身所属盟约" = every bond of that operator; "自身已激活的盟约" = only its bonds that are active. - No layer cap in the data. Caps are per source: max_add_count_per_battle (layers a single garrison instance may add in one battle), max_layer (per round, e.g. 安洁莉娜 12/round). The official cap is in the client, not the data: `AutoChessBattleConst.MAX_GARRISON_STACK = 999`, `AddBondCount` stores `min(L + n, 999)` per bond (research 11 §1). Since 2026-10-01 (DESIGN §20.12) the remake stops each bond at 999 (`shared/constants.js BOND_LAYER_CAP`, `layerGainRoom`); the community agrees (巴哈姆特 12534 "每把都能999层", "沒999層的盟約情況下"; 12316 "999謝"). diff --git a/public/js/screens/result.js b/public/js/screens/result.js index 04e0b7c4..ec7572bf 100644 --- a/public/js/screens/result.js +++ b/public/js/screens/result.js @@ -36,6 +36,21 @@ const STAT_ROWS = [ ['refreshes', N_('刷新次数')], ['leaks', N_('未击倒')], ['lpLost', N_('损失生命')], ]; +/** + * The bonds a result card lists (at most 6): the active ones and those holding layers, most layers first. A bond whose + * layers the game never shows (bonds.json `noStack`: 调和 协防干员 独行 绝技 — PRTS 卫戍协议:盟约 下半/PRTS盟约记录 「部分盟约不会 + * 显示叠加层数,但是叠加层数的特质/策略/装备等效果仍然对其生效」) counts none for the order and carries no number + * (`hideLayers`), as on the bond strip since 0.1.4 (PR #66) — the card printed them until 0.2.0 (community report + * 「独行这种盟约是不能叠层的」). + * @param {any[]} bonds m.result players[].bonds @param {(bondId: string) => any} bondRec bonds.json record lookup + */ +export function resultBonds(bonds, bondRec = () => null) { + const list = (Array.isArray(bonds) ? bonds : []).filter((b) => b && typeof b.bondId === 'string') + .map((b) => (bondRec(b.bondId)?.noStack ? { ...b, hideLayers: true } : b)); + const shown = (b) => (b.hideLayers ? 0 : b.layers || 0); + return list.filter((b) => b.active || shown(b) > 0).sort((a, b) => shown(b) - shown(a)).slice(0, 6); +} + /** * One unit of a final lineup: a 自选 piece draws its operator; a 补位 piece (`standInFor`) its stand-in, with the small * 「替补」 mark — the title names the chess it fielded for. @@ -57,7 +72,7 @@ function PlayerCard({ p, myId, titles, best, solo = false }) { const stats = STAT_ROWS.filter(([k]) => Number.isFinite(p.stats[k])).slice(0, 7); const band = p.bandId ? gd.band(p.bandId) : null; const lineup = p.lineup.slice(0, 10); - const bonds = p.bonds.filter((b) => b.active || b.layers > 0).sort((a, b) => (b.layers || 0) - (a.layers || 0)).slice(0, 6); + const bonds = resultBonds(p.bonds, (id) => gd.bond(id)); return html`
<${PlayerAvatar} player=${p} self=${p.playerId === myId} /> @@ -71,7 +86,7 @@ function PlayerCard({ p, myId, titles, best, solo = false }) { : html`${p.alive === false ? t('阵容已撤离') : 'NO INFO'}`} ${bonds.length ? html`
${bonds.map((b) => html` - <${BondGlyph} bondId=${b.bondId} />${b.layers ?? 0}`)}
` : null} + <${BondGlyph} bondId=${b.bondId} />${b.hideLayers ? null : html`${b.layers ?? 0}`}`)}` : null}
<${MicroLabel}>ROUNDS${p.roundsPassed} ${p.trophies > 0 || p.reward > 0 ? html`${p.trophies > 0 ? html`<${Icon} name="crown" />+${p.trophies}` : null}${p.reward > 0 ? html`<${Icon} name="shield" />+${p.reward}` : null}` : null} diff --git a/test/ui/feedback5-result-bonds.test.js b/test/ui/feedback5-result-bonds.test.js new file mode 100644 index 00000000..8a2b1748 --- /dev/null +++ b/test/ui/feedback5-result-bonds.test.js @@ -0,0 +1,44 @@ +// Community report of 2026-10-06 「独行这种盟约是不能叠层的(顺便排查下还有没有类似的)」 (item 21). PRTS 卫戍协议:盟约 下半/ +// PRTS盟约记录, its header: 「下述盟约中部分盟约不会显示叠加层数,但是叠加层数的特质/策略/装备等效果仍然对其生效」 — the four +// bonds.json `noStack` bonds (调和 协防干员 独行 绝技) do take layers (助力, 余, 华法琳 …), the game only never shows them. The +// bond strip and its popup hide them since 0.1.4 (PR #66, test/ui/feedback1b-bonds.test.js); the result card still printed +// them (「独行 24」) and ordered its six bonds by them. resultBonds: no number, no rank for a hidden count. +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); +globalThis.fetch = async (url) => { + const name = String(url).split('/').pop(); + try { + const body = readFileSync(path.join(ROOT, 'data', name), 'utf8'); + return { ok: true, status: 200, json: async () => JSON.parse(body) }; + } catch { + return { ok: false, status: 404, json: async () => ({}) }; + } +}; + +const { resultBonds } = await import('../../public/js/screens/result.js'); +const BONDS = JSON.parse(readFileSync(path.join(ROOT, 'data/bonds.json'), 'utf8')); +const rec = (id) => BONDS[id] ?? null; + +test('the four hidden-layer bonds are the data\'s noStack ones', () => { + assert.deepEqual(Object.keys(BONDS).filter((id) => BONDS[id].noStack).sort(), ['emptyShip', 'maniShip', 'soloShip', 'suntShip']); +}); + +test('result card bonds: a noStack bond shows no number and does not rank by its hidden layers', () => { + const out = resultBonds([ + { bondId: 'yanShip', layers: 12, active: true }, + { bondId: 'soloShip', layers: 40, active: true }, + { bondId: 'deputShip', layers: 20, active: true }, + { bondId: 'suntShip', layers: 30, active: false }, + { bondId: 'kjeragShip', layers: 3, active: false }, + ], rec); + assert.deepEqual(out.map((b) => b.bondId), ['deputShip', 'yanShip', 'kjeragShip', 'soloShip'], + 'layered bonds by layers; active 独行 last; an inactive 绝技 with only hidden layers left out'); + assert.equal(out.find((b) => b.bondId === 'soloShip').hideLayers, true); + assert.equal(out.find((b) => b.bondId === 'yanShip').hideLayers, undefined); + assert.equal(out.find((b) => b.bondId === 'yanShip').layers, 12, 'the stacking bonds keep their count'); +});