122 lines
8.1 KiB
JavaScript
122 lines
8.1 KiB
JavaScript
// test/content/feedback5-we2-summon-hand.test.js — 0.2.0 WE2 #21: the hand count of every 自选 summoner's placeable summon
|
|
// is its deploy limit (PRTS 卫戍协议/帮助 §战斗部署 "根据召唤物部署数量上限(非初始持有量),发送等量召唤物至手牌区"), and the
|
|
// data's deploy limit / holding carry the tokens' own talent additions (tools/build-data.mjs tokenTalentDeckBonus: the
|
|
// hidden "TOKEN数" talent max_deploy_count / max_deck_stack_cnt on top of the attribute frame). Official counts: the owners'
|
|
// talents (麦哲伦 / 令 / 电弧 "最多同时部署3个", SUM-Y stage 2+ "最多同时部署4个", 白铁 / 娜斯提 "最多可部署2个", 望 "可以使用6枚
|
|
// 棋子" — 7 at full potential (望's 潜能 3 「第一天赋效果增强」, the owner's decision of 2026-10-07) —, TRP-X +1), PRTS 幻影 备注 "最大可部署数量为3" (夜莺).
|
|
// #24: a summon no owner shows (displayTokenDict: the variants' `display` source) is its skill's own object, never a hand
|
|
// card — 予愿安洁莉娜 S3's “一会儿见!” (PRTS 予愿安洁莉娜 S3 备注: the skill deploys it at her initial tile).
|
|
// Run: node --test test/content/feedback5-we2-summon-hand.test.js
|
|
|
|
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { readFileSync } from 'node:fs';
|
|
import { getData } from '../../server/data.js';
|
|
import { GameData } from '../../server/match/gamedata.js';
|
|
import { diyGameData } from '../../server/match/player/diy.js';
|
|
import { diyRecord, diySlot } from '../../shared/diy.js';
|
|
|
|
const load = (f) => JSON.parse(readFileSync(new URL(`../../data/${f}.json`, import.meta.url), 'utf8'));
|
|
const CHESS = load('chess');
|
|
const BACKUPS = load('backups');
|
|
const DATA = { chess: CHESS, backups: BACKUPS };
|
|
const SLOT = { 5: 'chess_char_5_diy1_a', 6: 'chess_char_6_diy1_a' };
|
|
const STAGE = { 5: 1, 6: 3 };
|
|
const statusOf = (tier, elite) => (elite ? `2/60/7/${STAGE[tier]}` : '2/1/4/0');
|
|
|
|
/** The official hand count of `charId`'s summon under a module at its stage (null module = none). */
|
|
function official(charId, mod, stage) {
|
|
switch (charId) {
|
|
case 'char_248_mgllan': return mod === 'uniequip_003_mgllan' && stage >= 2 ? 4 : 3; // SUM-Y stage 2+: 最多同时部署4个
|
|
case 'char_2023_ling': return mod === 'uniequip_002_ling' && stage >= 2 ? 4 : 3; // SUM-Y stage 2+
|
|
case 'char_4195_radian': return 3;
|
|
case 'char_4072_ironmn': return 2;
|
|
case 'char_4212_nasti': return 2;
|
|
case 'char_179_cgbird': return 3; // PRTS 幻影 备注
|
|
case 'char_2027_wang': return mod === 'uniequip_002_wang' ? 8 : 7; // 7 at full potential, TRP-X +1
|
|
case 'char_4048_doroth': return 9; // 10 / 13, the hand's 9 cap
|
|
default: return 1;
|
|
}
|
|
}
|
|
|
|
const gd = new GameData(getData({ log: { warn() {}, error() {}, info() {} } }), 'mode_multi_hard');
|
|
|
|
test('every 自选 summoner: the hand count of each placeable summon in every form, skill and module is the official one', () => {
|
|
const owners = new Map(); // charId → placeable token ids it makes
|
|
for (const [tid, t] of Object.entries(BACKUPS.tokens)) {
|
|
if (t.kind !== 'summon' || t.placeable !== true) continue;
|
|
for (const key of Object.keys(t.variants)) {
|
|
const cid = key.split('@')[0];
|
|
if (!owners.has(cid)) owners.set(cid, new Set());
|
|
owners.get(cid).add(tid);
|
|
}
|
|
}
|
|
assert.ok(['char_248_mgllan', 'char_2023_ling', 'char_4195_radian', 'char_4072_ironmn', 'char_179_cgbird', 'char_2027_wang'].every((c) => owners.has(c)));
|
|
let checked = 0;
|
|
for (const [charId] of owners) {
|
|
if (!BACKUPS.diy.ownedPool.includes(charId)) continue;
|
|
for (const tier of [5, 6]) {
|
|
for (const elite of [false, true]) {
|
|
const form = BACKUPS.units[charId].forms[statusOf(tier, elite)];
|
|
for (const mod of elite ? [null, ...(form.modules ?? []).map((m) => m.uniEquipId)] : [null]) {
|
|
for (const s of form.skills) {
|
|
const pick = { charId, skillIndex: s.index, uniEquipId: mod };
|
|
const slot = diySlot(SLOT[tier], DATA);
|
|
const id = elite ? slot.goldenId : slot.baseId;
|
|
const rec = diyRecord(SLOT[tier], pick, { elite, data: DATA });
|
|
assert.ok(rec, `${charId} T${tier} ${elite ? 'elite' : 'normal'} ${mod} S${s.index + 1}`);
|
|
const view = diyGameData(gd, new Map([[id, rec]]));
|
|
for (const { tokenId, count } of view.placeableTokens(id, { skillIndex: s.index, moduleId: mod })) {
|
|
assert.equal(count, official(charId, mod, elite ? STAGE[tier] : 0), `${charId} T${tier} ${elite ? 'elite' : 'normal'} ${mod ?? 'none'} S${s.index + 1}: ${tokenId}`);
|
|
checked++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
assert.ok(checked > 200, `${checked} hand counts checked`);
|
|
});
|
|
|
|
test('the data: deploy limit / holding = the attribute frame + the token talents\' max_deploy_count / max_deck_stack_cnt (a module token part of the same talent replaces it)', () => {
|
|
const v = (tid, owner) => BACKUPS.tokens[tid].variants[owner];
|
|
// 麦哲伦: 1 + 2 standing, 0 + 5 held; SUM-Y +3 held, stage 3 its token part max_deploy_count 3 ⇒ 4
|
|
const d1 = 'token_10005_mgllan_drone1';
|
|
assert.deepEqual([v(d1, 'char_248_mgllan@2/1/4/0').stats.deployLimit, v(d1, 'char_248_mgllan@2/1/4/0').stats.deckStack], [3, 5]);
|
|
assert.deepEqual([v(d1, 'char_248_mgllan@2/60/7/1').byModule.uniequip_003_mgllan.stats.deployLimit, v(d1, 'char_248_mgllan@2/60/7/1').byModule.uniequip_003_mgllan.stats.deckStack], [3, 8]);
|
|
assert.deepEqual([v(d1, 'char_248_mgllan@2/60/7/3').byModule.uniequip_003_mgllan.stats.deployLimit, v(d1, 'char_248_mgllan@2/60/7/3').byModule.uniequip_003_mgllan.stats.deckStack], [4, 8]);
|
|
// 夜莺's 幻影: its visible talent's max_deploy_count 2 (RIN-Y stage 3 restates the same talent: not counted twice)
|
|
const ph = 'token_10003_cgbird_bird';
|
|
assert.equal(v(ph, 'char_179_cgbird@2/1/4/0').stats.deployLimit, 3);
|
|
assert.equal(v(ph, 'char_179_cgbird@2/60/7/3').byModule.uniequip_003_cgbird.stats.deployLimit, 3);
|
|
// 望's 棋子: the rank-2 talent (+1) is a potential talent (望's 潜能 3) — at full potential 6 + 1 = 7, holding 7 + 1 = 8
|
|
assert.deepEqual([v('token_10064_wang_stone1', 'char_2027_wang@2/1/4/0').stats.deployLimit, v('token_10064_wang_stone1', 'char_2027_wang@2/1/4/0').stats.deckStack], [7, 8]);
|
|
// the chess summons of tokens.json have no such talent: 凯瑟琳's 爬行号 2 of 3, everyone else 1
|
|
const tokens = load('tokens');
|
|
assert.equal(tokens.token_10041_cathy_catsld.variants.chess_char_4_11_a.stats.deployLimit, 2);
|
|
assert.equal(tokens.token_10000_silent_healrb.variants.chess_char_2_02_a.stats.deployLimit, 1);
|
|
});
|
|
|
|
test('#24 a summon no owner shows is never a hand card: 予愿安洁莉娜 S3\'s “一会儿见!” is not placeable, every placeable summon is displayed', () => {
|
|
const MARKER = 'token_10071_aglna2_agairp';
|
|
const AGLNA2 = 'char_1015_aglna2';
|
|
assert.equal(BACKUPS.tokens[MARKER].placeable, false);
|
|
const shows = (v) => (v.sources ?? []).includes('display') || Object.values(v.bySkill ?? {}).some((a) => (a.sources ?? []).includes('display'));
|
|
for (const t of [...Object.values(load('tokens')), ...Object.values(BACKUPS.tokens)]) {
|
|
if (t.kind !== 'summon') continue;
|
|
if (t.placeable) assert.ok(Object.values(t.variants).some(shows), `${t.tokenId} ${t.name}: placeable and displayed`);
|
|
else if (!Object.values(t.variants).some(shows)) assert.ok(t.displayType === 'HIDDEN' || t.tokenId === MARKER, `${t.tokenId} ${t.name}: undisplayed ⇒ HIDDEN (or the marker)`);
|
|
}
|
|
for (const tier of [5, 6]) {
|
|
for (const elite of [false, true]) {
|
|
const slot = diySlot(SLOT[tier], DATA);
|
|
const id = elite ? slot.goldenId : slot.baseId;
|
|
for (const skillIndex of [0, 1, 2]) {
|
|
const rec = diyRecord(SLOT[tier], { charId: AGLNA2, skillIndex, uniEquipId: null }, { elite, data: DATA });
|
|
const view = diyGameData(gd, new Map([[id, rec]]));
|
|
assert.deepEqual(view.placeableTokens(id, { skillIndex, moduleId: null }), [], `T${tier} ${elite ? 'elite' : 'normal'} S${skillIndex + 1}: no hand piece`);
|
|
}
|
|
}
|
|
}
|
|
});
|