Files
sganggs 269d7b0bcf feedback5 WY: 华法琳's granted trait stops at the data's 7 / 14 layers per battle, not 12 / 24 (GitHub #175; from PR #192 by @kukiC; the owner's decision of 2026-10-06)
PRTS 卫戍协议:盟约 下半, 3月27日更新#2调整 (revision 439189): 「[Ⅳ阶]华法琳:赋予的特质的叠层上限从 初始12/精锐24
降低至 初始7/精锐14」 — a nerf; the current 参战干员 record says 至多7层 / 14层 and the client blackboard is 7 / 14.
Research 02 Addendum 1 had read the line backwards, so the remake overrode garrison_95's cap to 12 / 24.

Ported from PR #192 by @kukiC: GRANTED_CAP_OVERRIDE is removed from server/sim/content/garrisons/battle.js and from
the client-result layer bound (server/match/fields.js layerAllowanceOf); a granted trait keeps its data cap like a
native one. Tests from the PR: garrisons_battle (updated on purpose: the 7th cast reaches 7 / 14, the 8th adds
nothing), feedback1-gaps (the bound with 华法琳 + 古米), playtest3 (both cards show 7 / 14); the first two fail without
the fix. Docs: research 00 / 01 / 02 corrected (02-bonds.json's note; data/bonds.json regenerated offline — only
miraShip.spec.layerGain changed), history 0.1.1's pointer, DESIGN §25.13.2 and the §25.10 table.
Golden unchanged (no corpus battle casts 8 times in front of 华法琳).
2026-10-06 21:03:56 +08:00

294 lines
20 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Gaps found while triaging GitHub issues #1 / #8 (DESIGN §21.26) — the server side.
// 2. 调和's +1: computeBonds marks a core bond's state whose count holds 调和's +1 (`harmony: 1`); the views send it
// (m.private bonds, m.public players[].bonds — a teammate's strip and popup), the battle input does not. The popup
// that reads it: test/ui/feedback1-gaps.test.js.
// 3. Bots never pick a strategy built around a bond the mode switches off: GameData.bandBondIds reads the tie from the
// band's own data (its <bond> names, its blackboards' bond ids and bond pools); botPickBand gives such a band weight 0
// — in 标准 潘格尼尼 (<拉特兰>, pool_char_later), 克莱门莎 (<阿戈尔>), 玛恩纳 (<卡西米尔>). Before: 5 of 200 solo picks and
// 23 of 800 co-op picks (seeds 1–200) were one of them; the other modes keep exactly the earlier picks.
import { test, describe } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { PHASE } from '../../shared/constants.js';
import { GameData } from '../../server/match/gamedata.js';
import { computeBonds, bondList, bondSnapshot, bondsWithGains, HARMONY_BOND } from '../../server/match/bondsMeta.js';
import { botPickBand } from '../../server/match/bot.js';
import { tileKey } from '../../server/match/board.js';
import { validateClientResult } from '../../server/match/fields.js';
import { bandBondIds as bandBondIdsOf } from '../../shared/bandBonds.js';
import { DATA, makeMatch, give, legalTileFor, checkInvariants } from './harness.js';
import { designText } from '../helpers/designDocs.js';
const MLYSS = 'chess_char_6_11_a'; // 缪尔赛思 (调和)
const members = (bond, n) => Object.values(DATA.chess).filter((c) => c.visible && !c.isGolden && c.bonds.includes(bond) && !c.bonds.includes(HARMONY_BOND)).map((c) => c.chessId).sort().slice(0, n);
let uid = 1;
const piece = (id) => ({ uid: uid++, kind: 'chess', id, items: [] });
const state = (board, hand = []) => {
const b = new Map();
board.forEach((p, i) => b.set(`${9 + (i % 4)},${2 + Math.floor(i / 4)}`, p));
const h = new Array(10).fill(null);
hand.forEach((p, i) => { h[i] = p; });
return { board: b, hand: h, layers: {} };
};
describe('§21.26 2 — 调和\'s +1 in the bond states and views', () => {
const gd = new GameData(DATA, 'mode_multi_normal');
test('computeBonds: `harmony: 1` exactly on the core bonds whose count holds the +1 — never an add-on bond, a core bond without a member, or 调和 itself', () => {
const yan = members('yanShip', 2);
const preci = members('preciShip', 1);
const s = computeBonds(gd, state([piece(MLYSS), ...yan.map(piece), ...preci.map(piece)]));
assert.equal(s[HARMONY_BOND].active, true);
assert.deepEqual(s.yanShip, { count: 3, active: true, tier: 1, layers: 0, harmony: 1 });
for (const [id, b] of Object.entries(s)) {
if (id === 'yanShip') continue;
assert.ok(!('harmony' in b), `${id}: no harmony (core with members: ${DATA.bonds[id].isCore && b.count > 0})`);
}
// the same board without 调和: no mark anywhere, the count is the real one
const bare = computeBonds(gd, state([...yan.map(piece), ...preci.map(piece)]));
assert.deepEqual(bare.yanShip, { count: 2, active: false, tier: 0, layers: 0 });
assert.ok(Object.values(bare).every((b) => !('harmony' in b)));
// 调和 on the bench does nothing (BOARD)
const bench = computeBonds(gd, state(yan.map(piece), [piece(MLYSS)]));
assert.equal(bench.yanShip.count, 2);
assert.ok(!('harmony' in bench.yanShip));
});
test('bondList carries it (m.private and m.public), bondsWithGains keeps it, bondSnapshot (the battle input) leaves it out', () => {
const s = computeBonds(gd, state([piece(MLYSS), ...members('yanShip', 2).map(piece)]));
const full = bondList(gd, s, { full: true }).find((b) => b.bondId === 'yanShip');
assert.deepEqual(full, { bondId: 'yanShip', count: 3, active: true, tier: 1, layers: 0, harmony: 1, thresholds: [3, 6, 9], countsHand: false });
const pub = bondList(gd, s).find((b) => b.bondId === 'yanShip');
assert.deepEqual(pub, { bondId: 'yanShip', count: 3, active: true, tier: 1, layers: 0, harmony: 1 });
assert.ok(bondList(gd, s).filter((b) => b.bondId !== 'yanShip').every((b) => !('harmony' in b)));
assert.equal(bondsWithGains(s, { yanShip: 4 }).yanShip.harmony, 1, 'the battle\'s gains keep the mark');
assert.deepEqual(bondSnapshot(s).yanShip, { count: 3, active: true, tier: 1, layers: 0 }, 'the battle reads count / active / tier / layers only');
});
test('a real co-op match: the deploying player\'s m.private and their m.public row (a teammate\'s strip) say the count holds the +1', () => {
const h = makeMatch({ mode: 'coop', humans: 2, seed: 2612, fake: true }).start();
h.toPrep(1);
const m = h.m;
const ps = h.ps('p_1');
ps.board.clear();
const used = new Set();
for (const id of [MLYSS, ...members('yanShip', 2)]) {
const t = legalTileFor(m, ps, id, used);
used.add(tileKey(t[0], t[1]));
give(m, ps, id, 'board', t);
}
ps.recompute();
const own = ps.privateView().bonds.find((b) => b.bondId === 'yanShip');
assert.equal(own.count, 3);
assert.equal(own.harmony, 1);
const row = m.publicView().players.find((p) => p.playerId === 'p_1').bonds.find((b) => b.bondId === 'yanShip');
assert.deepEqual(row, { bondId: 'yanShip', count: 3, active: true, tier: 1, layers: 0, harmony: 1 }, 'what p_0 reads when watching p_1');
assert.ok(m.publicView().players.find((p) => p.playerId === 'p_0').bonds.every((b) => !('harmony' in b)), 'p_0 has no 调和');
// 缪尔赛思 back to the bench: the mark goes with the +1
const k = [...ps.board].find(([, p]) => p.id === MLYSS)[0];
assert.deepEqual(m.handle('p_1', { t: 'g.move', uid: ps.board.get(k).uid, to: { area: 'hand', idx: ps.hand.findIndex((x) => x == null) } }), { ok: true });
const after = ps.privateView().bonds.find((b) => b.bondId === 'yanShip');
assert.equal(after.count, 2);
assert.ok(!('harmony' in after));
checkInvariants(m);
m.dispose();
});
});
describe('§21.26 3 — strategies tied to a bond the mode switches off', () => {
const funny = new GameData(DATA, 'mode_multi_funny');
test('GameData.bandBondIds reads the tie from the band\'s data: its <bond> names, bond ids and bond pools in its blackboards', () => {
const tied = Object.fromEntries(funny.bandIds().map((id) => [DATA.bands[id].name, funny.bandBondIds(id)]).filter(([, b]) => b.length));
assert.deepEqual(tied, {
杜遥夜: ['yanShip'], 佩佩: ['sargonShip'], 哈洛德: ['victoriaShip'], 休露丝: ['kjeragShip'], 潘格尼尼: ['lateranoShip'],
克莱门莎: ['egirShip'], 玛恩纳: ['kazimierzShip'], 贾维: ['siracusaShip'], 娜仁图亚: ['sargonShip'],
});
// every tied band carries the official note "在<X>部分干员缺席时体验可能不完整" naming that bond
for (const id of funny.bandIds()) {
const b = DATA.bands[id];
const note = /在<([^<>]+)>部分干员缺席时体验可能不完整/.exec(b.desc);
if (funny.bandBondIds(id).length) assert.ok(note && funny.bandBondIds(id).some((x) => DATA.bonds[x].name === note[1]), b.name);
}
// the three 标准 switches off; a bracketed item (<寻呼模块>, <画卷>) is no bond; unknown bands: []
assert.deepEqual(funny.bandIds().filter((id) => funny.bandBondIds(id).some((b) => funny.modeInactiveBonds.has(b))), ['band_paganini', 'band_clementia', 'band_mlynar']);
assert.deepEqual(funny.bandBondIds('band_jesica'), []);
assert.deepEqual(funny.bandBondIds('band_dusk'), []);
assert.deepEqual(funny.bandBondIds('nope'), []);
});
test('the derivation (shared/bandBonds.js, run by the data build) catches a tie through a pool or a blackboard bond id alone; GameData reads the field', () => {
const ctx = { bonds: DATA.bonds, pools: DATA.choices.pools };
const pool = { ...DATA.bands.band_paganini, desc: '【定制铳械】累计花费55资金后,获得1名精锐干员' };
const bb = { ...DATA.bands.band_clementia, desc: '【崇高牺牲】' };
assert.deepEqual(bandBondIdsOf(pool, ctx), ['lateranoShip'], 'choices.json pools.pool_char_later.bond');
assert.deepEqual(bandBondIdsOf(bb, ctx), ['egirShip'], 'bbStr bond_id');
assert.deepEqual(bandBondIdsOf({ desc: '在<阿戈尔>部分干员缺席时体验可能不完整;<寻呼模块>', buffs: [{ bb: { bond: 'kazimierzShip,nope' } }] }, ctx), ['egirShip', 'kazimierzShip'], 'the note, comma lists; an item in <…> is no bond');
assert.deepEqual(bandBondIdsOf(DATA.bands.band_bldsk, ctx), []);
assert.deepEqual(bandBondIdsOf(null, ctx), []);
// the build wrote exactly this for every band (data/bands.json is not stale against the helper)
for (const [id, b] of Object.entries(DATA.bands)) assert.deepEqual(b.bondIds, bandBondIdsOf(b, ctx), id);
// GameData reads the field (known bonds, data order) — never re-derives
const gd = new GameData({ ...DATA, bands: { band_x: { ...DATA.bands.band_bldsk, bandId: 'band_x', bondIds: ['kazimierzShip', 'nope', 'egirShip'] }, band_old: { ...DATA.bands.band_paganini, bondIds: undefined } } }, 'mode_single_funny');
assert.deepEqual(gd.bandBondIds('band_x'), ['egirShip', 'kazimierzShip']);
assert.deepEqual(gd.bandBondIds('band_old'), [], 'data without the field: no tie');
});
/** Bands the bots took over seeds 1–200: solo (one AI's pick) and co-op (the real draft, 4 AI seats). */
function picks(difficulty) {
const solo = [];
const coop = [];
for (let seed = 1; seed <= 200; seed++) {
const s = makeMatch({ mode: 'solo', difficulty, seats: [{ seat: 0, playerId: 'ai_0', name: 'AI', isBot: true, connected: true }], seed, fake: true });
solo.push(botPickBand(s.m, s.m.order[0]));
s.m.dispose();
const c = makeMatch({ mode: 'coop', difficulty, humans: 0, bots: 4, seed, fake: true }).start();
c.run(() => c.m.phase !== PHASE.INFO_CHECK && c.m.phase !== PHASE.BAND_DRAFT, { maxSteps: 1e5 });
assert.equal(c.m.phase, PHASE.BATTLE_CHECK, `seed ${seed}: the draft ended`);
for (const ps of c.m.order) coop.push(ps.bandId);
assert.equal(new Set(c.m.order.map((ps) => ps.bandId)).size, 4, 'no strategy twice');
c.m.dispose();
}
return { solo, coop };
}
const TIED = new Set(['band_paganini', 'band_clementia', 'band_mlynar']);
test('标准: over seeds 1–200 no bot picks 潘格尼尼 / 克莱门莎 / 玛恩纳 — solo (200) or in the co-op draft (800); every pick is offered', () => {
const { solo, coop } = picks('FUNNY');
assert.equal(solo.filter((id) => TIED.has(id)).length, 0, 'solo');
assert.equal(coop.filter((id) => TIED.has(id)).length, 0, 'co-op');
const offered = new Set(funny.bandIds());
assert.ok([...solo, ...coop].every((id) => offered.has(id)));
assert.ok(new Set(solo).size >= 15 && new Set(coop).size >= 25, 'the other strategies are still spread out');
});
test('绝境: the same seeds still give those strategies (nothing switched off), picks unchanged from the LP-only weighting', () => {
const { solo, coop } = picks('HARD');
assert.ok(solo.filter((id) => TIED.has(id)).length > 0, `solo ${solo.filter((id) => TIED.has(id)).length}/200`);
assert.ok(coop.filter((id) => TIED.has(id)).length > 0, `co-op ${coop.filter((id) => TIED.has(id)).length}/800`);
// the pre-0.1.1 pick (starting LP only), replayed on the same rng: identical where no bond is off
const before = (m) => {
const ids = m.gd.bandIds();
const lateFunds = (id) => /暂存/.test(String(m.gd.band(id)?.desc || ''));
const pairs = ids.map((id) => [id, Math.max(1, (m.gd.startLp(id) - 18) ** 2) * (m.isSolo && lateFunds(id) ? 0.02 : 1)]);
let r = m.rngBots() * pairs.reduce((a, [, w]) => a + w, 0);
for (const [id, w] of pairs) { r -= w; if (r < 0) return id; }
return pairs[pairs.length - 1][0];
};
for (const difficulty of ['NORMAL', 'HARD', 'ABYSS']) {
for (let seed = 1; seed <= 200; seed++) {
const seats = [{ seat: 0, playerId: 'ai_0', name: 'AI', isBot: true, connected: true }];
const a = makeMatch({ mode: 'solo', difficulty, seats, seed, fake: true });
const b = makeMatch({ mode: 'solo', difficulty, seats, seed, fake: true });
assert.equal(botPickBand(a.m, a.m.order[0]), before(b.m), `${difficulty} seed ${seed}`);
a.m.dispose();
b.m.dispose();
}
}
});
test('deterministic per seed; one rng draw per pick; with every offered band excluded, the default band', () => {
const seats = [{ seat: 0, playerId: 'ai_0', name: 'AI', isBot: true, connected: true }];
for (const seed of [3, 77, 140]) {
const a = makeMatch({ mode: 'solo', difficulty: 'FUNNY', seats, seed, fake: true });
const b = makeMatch({ mode: 'solo', difficulty: 'FUNNY', seats, seed, fake: true });
assert.equal(botPickBand(a.m, a.m.order[0]), botPickBand(b.m, b.m.order[0]));
a.m.dispose();
b.m.dispose();
}
let draws = 0;
const fake = (gd, r = 0.5) => ({ gd, isSolo: true, rngBots: () => { draws++; return r; } });
botPickBand(fake(funny), null);
assert.equal(draws, 1);
// every offered band tied to a bond 标准 switches off (the default band 华法琳 not offered here): the default band
const data = { ...DATA, bands: {
band_bldsk: { ...DATA.bands.band_bldsk, modeTypeList: ['MULTI'] },
band_paganini: DATA.bands.band_paganini, band_clementia: DATA.bands.band_clementia, band_mlynar: DATA.bands.band_mlynar,
} };
const gd = new GameData(data, 'mode_single_funny');
assert.deepEqual(gd.bandIds(), ['band_paganini', 'band_clementia', 'band_mlynar']);
for (const r of [0, 0.3, 0.999]) assert.equal(botPickBand(fake(gd, r), null), gd.defaultBandId);
// one free band left: always it, whatever the draw (also at the top of the range)
const one = new GameData({ ...data, bands: { ...data.bands, band_amiya: DATA.bands.band_amiya } }, 'mode_single_funny');
for (const r of [0, 0.5, 0.9999999]) assert.equal(botPickBand(fake(one, r), null), 'band_amiya');
});
});
describe('§21.26 found on the way — the client-result layer bound reads the lineup\'s layer 特质', () => {
// coop/FUNNY/107 (4 humans on AI 托管: their band picks changed with the bot's) reached a 6-谢拉格 freeze board at R12 —
// 初雪 + 银灰 (garrison_28: 50 % +1 per freeze in range, no per-battle cap) and 凛御银灰's handed-out 60 % +2 — whose honest
// +112 谢拉格 layers passed the flat 60 + 4·12 = 108: the server rejected the client and re-simulated (SP_VERIFY=all
// agrees with the client on all 57 battles of that match)
const gd = new GameData(DATA, 'mode_multi_funny');
const spec = (round, units, bonds = {}) => ({
kind: 'normal', round, timeLimit: 60, spawns: [], flags: { layerGainsEnabled: true },
players: [{ playerId: 'p_0', units: units.map((chessId, i) => ({ uid: i + 1, kind: 'chess', chessId, row: 9 + (i % 4), col: 2 + (i >> 2) })), bonds }],
});
const res = (gains) => ({ reason: 'cleared', time: 10, perPlayer: { p_0: { killed: 0, total: 0, leaked: [], perfect: true, layerGains: gains, coins: 0, unitsEnd: [], unitStats: [] } } });
const check = (s, gains, opts = { gd }) => { const v = validateClientResult(s, res(gains), opts); return v.ok ? 'ok' : v.reason; };
const SEED_107_R12 = ['chess_char_1_06_b', 'chess_char_4_14_a', 'chess_char_4_13_a', 'chess_char_3_14_a', 'chess_char_3_11_a', 'chess_char_4_22_a', 'chess_char_3_20_a', 'chess_char_5_14_b'];
// GitHub #175 (from PR #192 by @kukiC): the handed-out 华法琳 trait counts at the data cap, 7 / 14
test('华法琳\'s handed-out trait allows its data cap of 7 / 14 (GitHub #175)', () => {
for (const [id, cap] of [['chess_char_4_26_a', 7], ['chess_char_4_26_b', 14]]) {
const s = spec(3, [id, 'chess_char_1_10_a']); // 华法琳 + 古米
// a handed-out trait is counted once per operator of the player (2 here), on top of the flat 60 + 4·round
const bound = 60 + 4 * 3 + 2 * cap;
assert.equal(check(s, { steadShip: bound }), 'ok');
assert.equal(check(s, { steadShip: bound + 1 }), 'layer bound');
}
});
test('an uncapped freeze trait leaves 谢拉格 bounded only by 999: the honest +112 passes; a bond without such a trait keeps 60 + 4·round', () => {
const s = spec(12, SEED_107_R12, { kjeragShip: { count: 6, active: true, tier: 2, layers: 39 }, swiftShip: { count: 2, active: true, tier: 1, layers: 0 } });
assert.equal(check(s, { kjeragShip: 112 }), 'ok', 'the board of coop/FUNNY/107 R12');
assert.equal(check(s, { kjeragShip: 960 }), 'ok', '999 − 39');
assert.equal(check(s, { kjeragShip: 961 }), 'layer bound', 'never past 999');
assert.equal(check(s, { swiftShip: 108 }), 'ok');
assert.equal(check(s, { swiftShip: 109 }), 'layer bound', 'no layer trait names 迅捷: the flat bound');
assert.equal(check(s, { kjeragShip: 112 }, {}), 'layer bound', 'without game data: the flat bound as before');
});
test('capped traits add their per-battle caps (handed-out ones once per operator); 魔王\'s +extra lifts the bonds traits raise', () => {
// 史尔特尔 (精锐): <部署时> 突袭 +16, at most 100 a battle → 60 + 4·3 + 100
const surtr = spec(3, ['chess_char_5_07_b']);
assert.equal(check(surtr, { raidShip: 172 }), 'ok');
assert.equal(check(surtr, { raidShip: 173 }), 'layer bound');
assert.equal(check(surtr, { arcaneShip: 73 }), 'layer bound', 'its other bond: no trait, the flat bound');
// 荒芜拉普兰德 (精锐) hands every 叙拉古 operator garrison_117_b (+2 per kill, at most 200): × the 2 operators
const lap = spec(3, ['chess_char_6_18_b', 'chess_char_1_03_a']);
assert.equal(check(lap, { siracusaShip: 72 + 400 }), 'ok');
assert.equal(check(lap, { siracusaShip: 72 + 401 }), 'layer bound');
// 魔王 in the lineup: every bond a trait raises is bounded by 999 only; the others keep the flat bound
const demon = spec(3, ['chess_char_5_07_b', 'chess_char_4_25_a']);
assert.equal(check(demon, { raidShip: 999 }), 'ok');
assert.equal(check(demon, { arcaneShip: 73 }), 'layer bound');
});
});
test('§21.26 docs: DESIGN (the subsection and the normative lines), META, PLAYING and the CHANGELOG say what the code does', () => {
const doc = (p) => readFileSync(new URL(`../../${p}`, import.meta.url), 'utf8');
const DESIGN = designText(fileURLToPath(new URL('../../', import.meta.url))); // the index + docs/design/ + docs/history/
const at = DESIGN.indexOf('### 21.26 Gaps found while triaging GitHub issues #1 / #8 (v0.1.1)');
assert.ok(at > 0, 'the subsection');
const s = DESIGN.slice(at);
for (const k of ['morphPairings', 'harmonyMembers', '`harmony: 1`', 'bandBondIds', 'botPickBand', '0 of 200 and 0 of 800', '5 of 200 solo and 23 of 800 co-op',
'shared/bandBonds.js', 'bandOffBonds', 'BandOffTag', '本局禁用【拉特兰】盟约,此策略效果可能无法发挥', 'it stays selectable']) assert.ok(s.includes(k), k);
assert.match(doc('docs/DATA.md'), /\| `bondIds` \| `\["lateranoShip"\]` \(潘格尼尼\) \/ `\[\]` \|/, 'DATA.md bands.json bondIds');
assert.match(DESIGN, /bonds:\[\{bondId,count,active,tier,layers,harmony\? \/\* 调和's \+1 is in count, §21\.26 \*\/\}\]/, '§8.2');
assert.match(DESIGN, /bonds: \[ \{ bondId, count, active, tier, layers, harmony\? \/\* 调和's \+1 is in count, §21\.26 \*\/, thresholds, countsHand \} \]/, '§8.3');
assert.match(DESIGN, /Strategy \(§21\.6\): pick a band \(weighted by starting LP; never one built around a bond the mode switches off — `gd\.bandBondIds`, §21\.26\)/, '§6.6');
const META = doc('docs/META.md');
assert.match(META, /\*\*Strategy\*\* \(`botPickBand`\)/);
assert.match(META, /carries `harmony: 1` in both lists/);
assert.match(doc('docs/PLAYING.md'), /盟约详情会写「含调和 \+1」/);
const log = doc('CHANGELOG.md');
const v011 = log.slice(log.indexOf('## 0.1.1'), log.indexOf('## 0.1.0'));
assert.match(v011, /变形同构体的详情卡列出天赋栏里的对应关系/);
assert.match(v011, /盟约详情写明「含调和 \+1」/);
assert.match(v011, /人机不再选择围绕本局禁用盟约的策略(标准模拟下的潘格尼尼、克莱门莎、玛恩纳)/);
assert.match(v011, /选择策略时,围绕本局禁用盟约的策略(标准模拟下的潘格尼尼、克莱门莎、玛恩纳)标出「本局禁用」并在详情里说明,仍可选择/);
assert.match(META, /The strategy draft marks the same bands 本局禁用 for humans \(still selectable\)/);
});