feedback5: merge WE2 — fidelity (data / match / UI): summon hand counts from the tokens' hidden talents (麦哲伦 / 令 3, 白铁 2), 望's 棋子 on any tile, the 一会儿见! marker no hand card, TRY_SEARCH_ALLY_SKILL → SKILL_RANGE + allies, 斩业星熊's normal attack phys, IS / RA-only modules refused (SO-A / SO-B / RA-A), 黑键 S2 at full SP with a free tile, 重构体 / 战术锚点 placement, 塑心's cap per trait, random grants draw 自选 stock, diyBanned shown, AI 托管 buys 自选, 谬因 S2 / 薇薇安娜 S3 area triggers, #148 regression test, tier*.js / diyRecord shims removed; golden: coop2-NORMAL-70-diy, diy-045 / 078 / 114 / 115 / 116 moved (WE2's own), the W4B moves kept
This commit is contained in:
commit
723ee72d38
79 files changed
+1389
-405
No files matched your search
+14
-13
@@ -12,7 +12,8 @@ import assert from 'node:assert/strict';
|
||||
import { readFileSync, existsSync } from 'node:fs';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { standInRecord, diyRecord, composeUnitRecord, unitForm, statusKey, IDENTITY_FIELDS } from '../shared/standIn.js';
|
||||
import { standInRecord, composeUnitRecord, unitForm, statusKey, IDENTITY_FIELDS } from '../shared/standIn.js';
|
||||
import { diyRecordOf, lockedSelection } from '../shared/diy.js';
|
||||
import { composeStats, composeTalents, resolveLoadout, loadoutRecord, normalizeChess } from '../server/sim/simdata.js';
|
||||
|
||||
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
@@ -212,26 +213,26 @@ test('DIY: prototype picks — the 9 elites at tiers 5 and 6, six 4★ reserves
|
||||
assert.deepEqual(prototypes['6'], ELITES);
|
||||
assert.deepEqual(prototypes['5'], [...RESERVES.filter((id) => !['char_600_cpione', 'char_607_cspec'].includes(id)), ...ELITES]);
|
||||
assert.equal(prototypes['5'].length, 15);
|
||||
// every pick composes in every slot form, with any of its skills (which skill a prototype carries in a slot is an open
|
||||
// question — the data supports any answer), with the derived bonds (协防干员: no prototype has a faction) and no 特质
|
||||
// every pick composes in every slot form — at its locked selection (diy.locked: "技能携带规则与系统补位时一致"; shared/diy.js
|
||||
// diyRecordOf, which replaced standIn.js's prototype-only diyRecord in 0.2.0) — with the derived bonds (协防干员: no
|
||||
// prototype has a faction) and no 特质
|
||||
const data = { chess, backups };
|
||||
for (const [id, s] of Object.entries(backups.diy.slots)) {
|
||||
for (const slot of [chess[id], chess[s.goldenId]]) {
|
||||
for (const p of prototypes[s.tier]) {
|
||||
const form = unitForm(backups, p, slot.status);
|
||||
assert.ok(form, `${slot.chessId} ${p}: form`);
|
||||
for (const sk of form.skills) {
|
||||
const mod = slot.status.equipLevel > 0 ? form.modules[0]?.uniEquipId ?? null : null;
|
||||
const r = diyRecord(slot, p, backups, { skillIndex: sk.index, moduleId: mod });
|
||||
assert.ok(r && r.skill.index === sk.index && r.charId === p, `${slot.chessId} ${p} S${sk.index + 1}`);
|
||||
assert.deepEqual([r.bonds, r.garrisonIds, r.price, r.tier], [['emptyShip'], [], 4, s.tier]);
|
||||
if (mod) assert.equal(r.module.active, true, `${slot.chessId} ${p}: module`);
|
||||
}
|
||||
const lk = lockedSelection(data, s.tier, p);
|
||||
const r = diyRecordOf(slot, { charId: p }, data);
|
||||
assert.ok(r && r.skill.index === lk.skillIndex && r.charId === p, `${slot.chessId} ${p}`);
|
||||
assert.deepEqual([r.bonds, r.garrisonIds, r.price, r.tier], [['emptyShip'], [], 4, s.tier]);
|
||||
if (slot.status.equipLevel > 0 && lk.uniEquipId) assert.equal(r.module.active, true, `${slot.chessId} ${p}: module`);
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.equal(diyRecord(chess.chess_char_6_diy1_a, 'char_601_cguard', backups, { skillIndex: 2 }), null, 'a reserve is not a tier-6 pick');
|
||||
assert.equal(diyRecord(chess.chess_char_5_diy1_a, 'char_600_cpione', backups, { skillIndex: 2 }), null, '预备干员-先锋 is not a pick');
|
||||
assert.equal(diyRecord(chess.chess_char_5_01_a, 'char_608_acpion', backups, { skillIndex: 2 }), null, 'not a DIY slot');
|
||||
assert.equal(diyRecordOf(chess.chess_char_6_diy1_a, { charId: 'char_601_cguard' }, data), null, 'a reserve is not a tier-6 pick');
|
||||
assert.equal(diyRecordOf(chess.chess_char_5_diy1_a, { charId: 'char_600_cpione' }, data), null, '预备干员-先锋 is not a pick');
|
||||
assert.equal(diyRecordOf(chess.chess_char_5_01_a, { charId: 'char_608_acpion' }, data), null, 'not a DIY slot');
|
||||
});
|
||||
|
||||
test('DIY: the 71 owned 6★ picks — a form at every slot status with all three skills, every module at stage 1 and 3, their summons', () => {
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
// test/content/feedback5-we2-area-triggers.test.js — 0.2.0 WE2 #16: kit-side larger-range triggers where no data range can
|
||||
// carry the owner's ACTIVE_RANGE rule (2026-10-05: a MANUAL skill whose running range strictly contains the operator's own
|
||||
// casts as soon as an enemy is inside that larger range):
|
||||
// * 谬因 S2 临界瞬爆 — the beam's unbounded path (O1's report): content trigger range = the path she would fire along now;
|
||||
// * 薇薇安娜 S3 “明灭” — from its second cast of a deployment on 3-2 (O11's report): ACTIVE_RANGE after the first cast ends
|
||||
// (skills.js setTrigger), DEFAULT again at her next deployment.
|
||||
// Run: node --test test/content/feedback5-we2-area-triggers.test.js
|
||||
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { makeBattle, enemyRec, checkInvariants } from '../helpers/battleHarness.js';
|
||||
|
||||
const SLOT = { 5: 'chess_char_5_diy1_a', 6: 'chess_char_6_diy1_a' };
|
||||
const dummy = (key, o = {}) => enemyRec({ key, hp: 1e9, speed: 0, mass: 0, ...o });
|
||||
const ENEMIES = { enemy_dummy: dummy('enemy_dummy'), enemy_fly: dummy('enemy_fly', { motion: 'FLY' }) };
|
||||
function done(h) {
|
||||
checkInvariants(h.b);
|
||||
assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0]));
|
||||
}
|
||||
|
||||
/** `charId` as uid 1 at (row, col) facing RIGHT with skill `skill`. */
|
||||
function field(charId, { skill, row, col, tier = 5, seed = 5 }) {
|
||||
const h = makeBattle({
|
||||
defs: { enemies: ENEMIES }, timeLimit: 900, autoFinish: false, seed,
|
||||
flags: { dpInit: 99, dpPerSec: 0, dpMax: 999, startOpCooldown: 0 }, hooks: ['skillStart', 'skillEnd'], captureNoisy: true,
|
||||
units: [{ uid: 1, diy: { slot: SLOT[tier], charId, skillIndex: skill, uniEquipId: null }, elite: false, row, col }],
|
||||
});
|
||||
h.step();
|
||||
return { h, u: h.unit(1) };
|
||||
}
|
||||
|
||||
test('#16 谬因 S2: an enemy on the beam\'s line beyond her own 6 tiles casts it (data DEFAULT alone would wait); one off the line does not', () => {
|
||||
// her own range (9, 2) … (9, 7); the beam reaches the end of the row
|
||||
{
|
||||
const { h, u } = field('char_4229_aphris', { skill: 1, row: 9, col: 2 });
|
||||
assert.equal(u.skill.rule, 'DEFAULT');
|
||||
assert.equal(u.skill.triggerRanges.length, 1, 'the beam path is a content trigger range');
|
||||
h.spawn('enemy_dummy', { pos: [10, 12] }); // off the line
|
||||
u.skill.gainSp(999);
|
||||
h.run(2);
|
||||
assert.equal(u.skill.activations, 0, 'off the line: no cast');
|
||||
h.spawn('enemy_dummy', { pos: [9, 13] }); // on the line, far beyond (9, 7)
|
||||
assert.ok(h.runUntil(() => u.skill.activations === 1, 1), 'on the beam\'s line: cast');
|
||||
done(h);
|
||||
}
|
||||
// a flyer on the line too (the beam hits air units)
|
||||
{
|
||||
const { h, u } = field('char_4229_aphris', { skill: 1, row: 9, col: 2 });
|
||||
h.spawn('enemy_fly', { pos: [9, 14] });
|
||||
u.skill.gainSp(999);
|
||||
assert.ok(h.runUntil(() => u.skill.activations === 1, 1));
|
||||
done(h);
|
||||
}
|
||||
// S1 / S3 get no such range
|
||||
for (const skill of [0, 2]) {
|
||||
const { h, u } = field('char_4229_aphris', { skill, row: 9, col: 2 });
|
||||
assert.equal(u.skill.triggerRanges.length, 0, `S${skill + 1}`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('#16 薇薇安娜 S3: the first cast of a deployment by the data\'s DEFAULT; once it ended, an enemy on the 3-2 beyond her own reach casts the next one (ACTIVE_RANGE); a redeploy goes back to DEFAULT', () => {
|
||||
const { h, u } = field('char_4098_vvana', { skill: 2, row: 10, col: 3 });
|
||||
assert.equal(u.skill.rule, 'DEFAULT');
|
||||
// (10, 6): on her 3-2 (cols 3–6), outside her own 1-1 (cols 3–4)
|
||||
const far = h.spawn('enemy_dummy', { pos: [10, 6] });
|
||||
u.skill.gainSp(999);
|
||||
h.run(2);
|
||||
assert.equal(u.skill.activations, 0, 'first cast: DEFAULT on her own range — the far enemy does not cast it');
|
||||
// an enemy in her own range: the first cast; then let it run out
|
||||
const near = h.spawn('enemy_dummy', { pos: [10, 4] });
|
||||
assert.ok(h.runUntil(() => u.skill.activations === 1, 3), 'first cast');
|
||||
assert.ok(h.runUntil(() => !u.skill.active, 30), 'it ends');
|
||||
assert.deepEqual([u.skill.rule, u.skill.triggerGrid], ['ACTIVE_RANGE', [[0, 0], [0, 1], [0, 2], [0, 3]]], 'from now on the 3-2');
|
||||
h.b.kill(near, null);
|
||||
u.skill.gainSp(999);
|
||||
assert.ok(h.runUntil(() => u.skill.activations === 2, 4), 'the far enemy alone casts the second one');
|
||||
void far;
|
||||
// a redeploy: the count and the rule start again
|
||||
assert.ok(h.runUntil(() => !u.skill.active, 40));
|
||||
h.b.retreat(u, { reason: 'retreat' });
|
||||
h.step();
|
||||
assert.ok(h.b.redeploy(u, { free: true }));
|
||||
h.step();
|
||||
assert.deepEqual([u.skill.rule, u.skill.triggerGrid], ['DEFAULT', null], 'the next deployment: the data\'s rule');
|
||||
done(h);
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
// test/content/feedback5-we2-garrison90.test.js — 0.2.0 WE2 #30: 塑心's 特质 garrison_90 (bond_actived_maxstack) "<战斗中>
|
||||
// 开启技能时,同一行每有1名干员,当前已激活且层数最多的盟约层数+1(每场作战至多10层)" (elite +2, at most 20): the per-battle cap is
|
||||
// the trait's own, whichever bond is the highest at each gain — a change of the highest bond grants no fresh cap
|
||||
// (server/sim/content/garrisons/battle.js fireGain; PR #178 review).
|
||||
// Run: node --test test/content/feedback5-we2-garrison90.test.js
|
||||
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { makeBattle, chessRec, checkInvariants } from '../helpers/battleHarness.js';
|
||||
|
||||
const B = (layers = 0) => ({ count: 3, active: true, tier: 1, layers });
|
||||
|
||||
/** 塑心's trait on 'op' with one row mate (2 in the row: +2 per skill, elite +4). */
|
||||
function field(gid, bonds) {
|
||||
const chess = { op: { ...chessRec({ id: 'op' }), garrisonIds: [gid] }, m1: { ...chessRec({ id: 'm1' }), garrisonIds: [] } };
|
||||
const h = makeBattle({
|
||||
seed: 3, bonds, defs: { chess, enemies: {} }, autoFinish: false, timeLimit: 400,
|
||||
units: [{ chessId: 'op', row: 10, col: 4 }, { chessId: 'm1', row: 10, col: 6 }],
|
||||
});
|
||||
h.step(1);
|
||||
return h;
|
||||
}
|
||||
const skill = (h) => h.b.emit('skillStart', { unit: h.unit('op'), skill: h.unit('op').skill, reason: 'test' });
|
||||
const gains = (h) => ({ ...h.result().perPlayer.p1.layerGains });
|
||||
|
||||
test('#30 塑心 garrison_90: at most 10 (elite 20) layers per battle in all, even when the highest bond changes', () => {
|
||||
for (const [gid, per, cap] of [['garrison_90_a', 2, 10], ['garrison_90_b', 4, 20]]) {
|
||||
const h = field(gid, { yanShip: B(20), steadShip: B(15) });
|
||||
const n1 = Math.floor(cap / per / 2); // half the cap on 炎 (the highest)
|
||||
for (let i = 0; i < n1; i++) skill(h);
|
||||
assert.deepEqual(gains(h), { yanShip: n1 * per }, `${gid}: 炎 is the highest`);
|
||||
// 坚守 becomes the highest (an outside gain of 30), then many more casts
|
||||
assert.equal(h.b.addLayers('p1', 'steadShip', 30, 'test'), 30);
|
||||
for (let i = 0; i < 20; i++) skill(h);
|
||||
const g = gains(h);
|
||||
assert.equal(g.yanShip, n1 * per, `${gid}: 炎 kept its gain`);
|
||||
assert.equal(g.steadShip - 30, cap - n1 * per, `${gid}: 坚守 got only what was left of the one cap`);
|
||||
checkInvariants(h.b);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
// test/content/feedback5-we2-raid-148.test.js — 0.2.0 WE2 #3 (GitHub #148 「突袭近战干员部署在高台」, a battle screenshot on the
|
||||
// 深水区 map): reproduced on every battle stage — a melee 突袭 member's jump (content/bonds/addon/battle.js raidTile:
|
||||
// grid.canStand for its position) lands on low ground only, never on a 高台; on 战场#08 (act2 m04) it may land on a 围墙 tile
|
||||
// (tile_fence_bound: LOW, buildable ALL — deployable, it attacks from there and blocks no ground enemy, DESIGN §21.23), which
|
||||
// the board draws as a raised concrete area inside an orange railing: what the screenshot most likely shows.
|
||||
// Run: node --test test/content/feedback5-we2-raid-148.test.js
|
||||
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { makeBattle, chessRec, enemyRec } from '../helpers/battleHarness.js';
|
||||
import { getData } from '../../server/data.js';
|
||||
|
||||
const D = getData({ log: { warn() {}, error() {}, info() {} } });
|
||||
const STAGES = Object.keys(D.stages).filter((id) => D.stages[id].kind !== 'unite');
|
||||
const defs = {
|
||||
chess: { m: chessRec({ id: 'm', bonds: ['raidShip'], position: 'MELEE', range: [[0, 0], [0, 1]] }) },
|
||||
enemies: { e: enemyRec({ key: 'e', hp: 1e9, speed: 0, mass: 0 }) },
|
||||
};
|
||||
const RAID = { raidShip: { count: 3, active: true, tier: 1, layers: 1 } };
|
||||
|
||||
test('#3 / GitHub #148: a melee 突袭 member never lands on a 高台 (any stage, any enemy tile of the field); 战场#08 lands it on 围墙 tiles', () => {
|
||||
let jumps = 0, fence = 0;
|
||||
for (const stageId of STAGES) {
|
||||
const st = D.stages[stageId];
|
||||
const tileAt = (r, c) => st.tiles[st.rows[r]?.[c]];
|
||||
// the member's start: the first deployable melee tile of the field
|
||||
let home = null;
|
||||
for (let r = 12; r >= 9 && !home; r--) for (let c = 2; c <= 10 && !home; c++) {
|
||||
const t = tileAt(r, c);
|
||||
if (t && t.height === 'LOW' && (t.buildable === 'ALL' || t.buildable === 'MELEE') && t.tileKey === 'tile_road') home = [r, c];
|
||||
}
|
||||
if (!home) continue;
|
||||
for (let er = 9; er <= 12; er++) for (let ec = 2; ec <= 10; ec++) {
|
||||
const t = tileAt(er, ec);
|
||||
if (!t || t.height !== 'LOW' || (er === home[0] && ec === home[1])) continue;
|
||||
const h = makeBattle({ stageId, defs, bonds: RAID, enemies: [{ key: 'e', pos: [er, ec] }], units: [{ chessId: 'm', row: home[0], col: home[1] }], autoFinish: false, timeLimit: 60 });
|
||||
const u = h.unit('m');
|
||||
h.run(12);
|
||||
if (u.tileR === home[0] && u.tileC === home[1]) continue;
|
||||
jumps++;
|
||||
const lt = tileAt(u.tileR, u.tileC);
|
||||
assert.ok(lt && lt.height === 'LOW' && (lt.buildable === 'ALL' || lt.buildable === 'MELEE'), `${stageId}: enemy ${er},${ec} → ${u.tileR},${u.tileC} ${lt?.tileKey}/${lt?.height}`);
|
||||
if (lt.tileKey === 'tile_fence_bound') fence++;
|
||||
}
|
||||
}
|
||||
assert.ok(jumps > 50, `${jumps} jumps checked`);
|
||||
assert.ok(fence > 0, 'some land on a 围墙 tile (the #148 look)');
|
||||
});
|
||||
@@ -0,0 +1,121 @@
|
||||
// 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枚
|
||||
// 棋子", 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' ? 7 : 6; // 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 — potential 0 keeps 6
|
||||
assert.equal(v('token_10064_wang_stone1', 'char_2027_wang@2/1/4/0').stats.deployLimit, 6);
|
||||
// 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`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -9,7 +9,9 @@ import { skillSpecSource } from '../../server/sim/content/index.js';
|
||||
import { effectiveProfile } from '../../server/sim/ai.js';
|
||||
import { kitCoverage } from '../../tools/kit-coverage.mjs';
|
||||
import { wolfShadows, wolfTacticalPoint } from '../../server/sim/content/tokens.js';
|
||||
import KITS from '../../server/sim/content/kits/tier3.js';
|
||||
import { TIER_KITS } from '../../server/sim/content/kits/index.js';
|
||||
|
||||
const KITS = TIER_KITS[2];
|
||||
|
||||
const ds = getDefaultSource();
|
||||
/** Skill index of `skillId` on chess `id`. */
|
||||
|
||||
@@ -5,9 +5,11 @@ import assert from 'node:assert/strict';
|
||||
import { makeBattle, enemyRec, checkInvariants } from '../helpers/battleHarness.js';
|
||||
import { getDefaultSource } from '../../server/sim/simdata.js';
|
||||
import { effectiveProfile } from '../../server/sim/ai.js';
|
||||
import KITS from '../../server/sim/content/kits/tier3.js';
|
||||
import { TIER_KITS } from '../../server/sim/content/kits/index.js';
|
||||
import { COLS } from '../../server/sim/constants.js';
|
||||
|
||||
const KITS = TIER_KITS[2];
|
||||
|
||||
const ds = getDefaultSource();
|
||||
const D = (id) => ds.getChess(id);
|
||||
const BB = (id) => D(id).skill.bb;
|
||||
|
||||
@@ -4,9 +4,11 @@ import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { makeBattle, enemyRec, checkInvariants } from '../helpers/battleHarness.js';
|
||||
import { getDefaultSource } from '../../server/sim/simdata.js';
|
||||
import kits from '../../server/sim/content/kits/tier4.js';
|
||||
import { TIER_KITS } from '../../server/sim/content/kits/index.js';
|
||||
import { absoluteRangeKeys } from '../../server/sim/targeting.js';
|
||||
|
||||
const kits = TIER_KITS[3];
|
||||
|
||||
const ds = getDefaultSource();
|
||||
const D = (id) => ds.getChess(id);
|
||||
const approx = (a, b, eps = 1e-6, msg = '') => assert.ok(Math.abs(a - b) <= eps * Math.max(1, Math.abs(b)), `${msg} ${a} ≈ ${b}`);
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { makeBattle, enemyRec, chessRec, checkInvariants } from '../helpers/battleHarness.js';
|
||||
import KITS from '../../server/sim/content/kits/tier5.js';
|
||||
import { TIER_KITS } from '../../server/sim/content/kits/index.js';
|
||||
|
||||
const KITS = TIER_KITS[4];
|
||||
|
||||
const approx = (a, b, eps = 1e-6, msg = '') => assert.ok(Math.abs(a - b) <= eps * Math.max(1, Math.abs(b)), `${msg} ${a} ≉ ${b}`);
|
||||
const dummy = (key = 'enemy_dummy', o = {}) => enemyRec({ key, hp: 1e7, speed: 0, ...o });
|
||||
|
||||
@@ -39,12 +39,11 @@ const ANY_ATK = (h, u) => h.hooksOf('damaged').filter((c) => c.source === u && c
|
||||
const extras = (h, u) => h.hooksOf('damaged').filter((c) => c.source === u && (c.dmg?.tags || []).includes('aglna2:extra'));
|
||||
|
||||
/** 予愿安洁莉娜 as uid 1 at (row, col) facing RIGHT, plus `others`. */
|
||||
function field({ tier = 5, elite = false, skill = 0, row = 10, col = 4, others = [], seed = 5, marker = null } = {}) {
|
||||
const piece = marker ? [{ uid: 9, kind: 'token', tokenId: MARKER, ownerUid: 1, row: marker[0], col: marker[1] }] : [];
|
||||
function field({ tier = 5, elite = false, skill = 0, row = 10, col = 4, others = [], seed = 5 } = {}) {
|
||||
const h = makeBattle({
|
||||
defs: { enemies: ENEMIES }, timeLimit: 900, autoFinish: false, seed,
|
||||
flags: { dpPerSec: 0, dpMax: 999 }, hooks: ['damaged', 'skillStart', 'skillEnd', 'statusApplied', 'deploy', 'dodge'], captureNoisy: true,
|
||||
units: [{ uid: 1, diy: { slot: SLOT[tier], charId: AGL, skillIndex: skill }, elite, row, col }, ...piece, ...others],
|
||||
units: [{ uid: 1, diy: { slot: SLOT[tier], charId: AGL, skillIndex: skill }, elite, row, col }, ...others],
|
||||
});
|
||||
h.step();
|
||||
return { h, u: h.unit(1) };
|
||||
@@ -349,19 +348,6 @@ test('S3 move: blocking nothing, she flies (2 tiles/s, 无敌, disarmed) to the
|
||||
assert.equal(u.extraRangeKeys, null);
|
||||
done(h);
|
||||
}
|
||||
// a placed “一会儿见!” piece never deploys by itself; it is the marker
|
||||
{
|
||||
const { h, u } = field({ tier: 6, elite: true, skill: 2, marker: [12, 9] });
|
||||
const piece = h.b.allyUnits.find((t) => t.kind === 'token' && t.uid === 9);
|
||||
assert.ok(piece && !piece.alive && piece.deferDeploy, 'not deployed at the start');
|
||||
u.skill.gainSp(999);
|
||||
h.spawn('enemy_fly', { pos: [11, 7] });
|
||||
assert.ok(h.runUntil(() => u.tileR === 11 && u.tileC === 7, 5));
|
||||
assert.ok(piece.alive && piece.tileR === 10 && piece.tileC === 4, 'the piece marks her deployment tile');
|
||||
u.skill.end('test');
|
||||
assert.ok(!piece.alive && !piece.removed, 'withdrawn, kept as a piece');
|
||||
done(h);
|
||||
}
|
||||
// blocking something, she stays
|
||||
{
|
||||
const { h, u } = field({ tier: 5, skill: 2 });
|
||||
|
||||
@@ -76,7 +76,7 @@ function storeUp(h, u, n, e, max = 60) {
|
||||
return log;
|
||||
}
|
||||
|
||||
test('黑键 in every 自选 form: his operator kit (all three skills authored), the form\'s stats + module attributes, 3-14, ranged arts every 3 s that hits air units, blocks 1, ground-targetable, the data\'s DEFAULT triggers, 空 bond, no 特质', () => {
|
||||
test('黑键 in every 自选 form: his operator kit (all three skills authored), the form\'s stats + module attributes, 3-14, ranged arts every 3 s that hits air units, blocks 1, ground-targetable, the data\'s DEFAULT triggers (S2: the kit casts it), 空 bond, no 特质', () => {
|
||||
assert.equal(OPERATOR_KITS[EBN], KITS[EBN]);
|
||||
for (const f of FORMS_ALL) {
|
||||
const [tier, elite, mod] = f;
|
||||
@@ -88,7 +88,7 @@ test('黑键 in every 自选 form: his operator kit (all three skills authored),
|
||||
[form.stats.maxHp + (m?.attr.maxHp ?? 0), form.stats.atk + (m?.attr.atk ?? 0), form.stats.def + (m?.attr.def ?? 0), form.stats.res + (m?.attr.res ?? 0), 100 + (m?.attr.aspd ?? 0)], `${label(f)}: stats`);
|
||||
assert.deepEqual([u.s.blockCnt, u.profile.attack, u.profile.dmgType, u.profile.canHitFly, u.base.bat], [1, 'ranged', 'arts', true, 3], `${label(f)}: 秘术师`);
|
||||
assert.deepEqual(u.liveRangeGrid, form.rangeGrid, `${label(f)}: 3-14`);
|
||||
assert.equal(u.skill.rule, 'DEFAULT', `${label(f)}: the data's trigger`);
|
||||
assert.equal(u.skill.rule, skill === 1 ? 'NEVER' : 'DEFAULT', `${label(f)}: the data's trigger (S2: the kit's own cast)`);
|
||||
assert.deepEqual([u.def.bonds, u.def.raw.garrisonIds], [['emptyShip'], []], `${label(f)}: bonds / 特质`);
|
||||
assert.ok(!u.s.flags.liftoff && !u.s.flags.camou && !u.s.flags.stealth, `${label(f)}: ground enemies target him`);
|
||||
done(h);
|
||||
@@ -292,16 +292,29 @@ test('S1 渐快急板 (MANUAL, data DEFAULT): 5 s, range 4-1, attack interval ×
|
||||
}
|
||||
});
|
||||
|
||||
test('S2 荒芜回响 (AUTO, data DEFAULT): spends every energy (elite first) on energies + 1 旧日残影 on free tiles of his range — the enemy\'s tile first, then the nearest; fewer tiles ⇒ fewer spent; no free tile ⇒ no cast (the charge back)', () => {
|
||||
test('S2 荒芜回响 (AUTO: full SP and one free placeable tile in his range, no enemy needed — PRTS 备注, prefab _minTileNum 1): spends every energy (elite first) on energies + 1 旧日残影 on free tiles of his range — the enemy\'s tile first, then the nearest; fewer tiles ⇒ fewer spent; no free tile ⇒ no cast', () => {
|
||||
for (const [tier, elite, mod] of [[5, false, null], [6, true, MX]]) {
|
||||
const sk = skillOf(tier, elite, S2);
|
||||
assert.deepEqual([sk.skillType, sk.bb.atk_scale, sk.bb.force, sk.spCost, sk.initSp], ['AUTO', elite ? 2 : 1.7, 1, elite ? 14 : 15, 0]);
|
||||
// no enemy on the field: it casts as soon as its SP is full
|
||||
{
|
||||
const { h, u } = field({ tier, elite, mod, skill: 1 });
|
||||
assert.equal(u.skill.rule, 'NEVER', 'the kit casts it');
|
||||
let before = stored(u);
|
||||
for (let i = 0; i < (sk.spCost + 1) * 30 && u.skill.activations === 0; i++) { before = stored(u); h.step(); }
|
||||
assert.equal(u.skill.activations, 1, 'cast at full SP with no enemy anywhere');
|
||||
approx(h.b.time, sk.spCost, `T${tier}: at ${sk.spCost} s`, 0.2);
|
||||
assert.equal(h.b.allyUnits.filter((t) => t.kind === 'token' && t.defId === REMNANT && t.alive).length, before.n + before.e + 1, 'the energies stored meanwhile + 1 remnants');
|
||||
assert.deepEqual(stored(u), { n: 0, e: 0 }, 'every energy spent');
|
||||
done(h);
|
||||
}
|
||||
const { h, u } = field({ tier, elite, mod, skill: 1 });
|
||||
const N = traitOf(tier, elite, mod).times;
|
||||
holdSkill(u);
|
||||
storeUp(h, u, N, 1);
|
||||
u.skill.gainSp(999);
|
||||
const e = h.spawn('enemy_dummy', { pos: [10, 7] });
|
||||
assert.ok(h.runUntil(() => u.skill.activations === 1, 5), 'cast with an enemy in range');
|
||||
u.skill.gainSp(999);
|
||||
assert.ok(h.runUntil(() => u.skill.activations === 1, 5), 'cast');
|
||||
const rem = h.b.allyUnits.filter((t) => t.kind === 'token' && t.defId === REMNANT);
|
||||
assert.equal(rem.length, N + 2, `${N} + 1 energies + 1 remnants`);
|
||||
assert.deepEqual([rem[0].tileR, rem[0].tileC], [10, 7], 'the enemy\'s tile first');
|
||||
@@ -315,27 +328,30 @@ test('S2 荒芜回响 (AUTO, data DEFAULT): spends every energy (elite first) on
|
||||
// the flat stage's edge (col 2: RANGED tiles; cols 0–1 none): facing LEFT from (10, 2) no tile of his range is free
|
||||
{
|
||||
const { h, u } = field({ tier: 5, skill: 1, row: 10, col: 2, dir: 'LEFT' });
|
||||
holdSkill(u);
|
||||
storeUp(h, u, 3, 1);
|
||||
u.skill.gainSp(999);
|
||||
h.spawn('enemy_dummy', { pos: [10, 1] });
|
||||
assert.ok(h.runUntil(() => u.skill.activations >= 1, 5), 'the DEFAULT condition holds (an enemy in range)');
|
||||
assert.equal(h.b.allyUnits.filter((t) => t.kind === 'token').length, 0, 'no placeable tile: no remnant');
|
||||
assert.ok(u.skill.ready, 'the charge is back');
|
||||
u.skill.gainSp(999);
|
||||
h.run(5);
|
||||
assert.equal(u.skill.activations, 0, 'no placeable tile: no cast, an enemy in range notwithstanding');
|
||||
assert.equal(h.b.allyUnits.filter((t) => t.kind === 'token').length, 0, 'no remnant');
|
||||
assert.ok(u.skill.ready, 'the charge waits');
|
||||
done(h);
|
||||
}
|
||||
// fewer free tiles than energies + 1: facing LEFT from (10, 3) only (10, 2) and (11, 2) are placeable
|
||||
{
|
||||
const { h, u } = field({ tier: 5, skill: 1, row: 10, col: 3, dir: 'LEFT' });
|
||||
holdSkill(u);
|
||||
storeUp(h, u, 3, 1);
|
||||
u.skill.gainSp(999);
|
||||
let atEnd = null;
|
||||
h.b.on('skillEnd', (c) => { if (c.unit === u) atEnd = stored(u); });
|
||||
h.spawn('enemy_dummy', { pos: [10, 2] });
|
||||
u.skill.gainSp(999);
|
||||
assert.ok(h.runUntil(() => u.skill.activations === 1, 5));
|
||||
const rem = h.b.allyUnits.filter((t) => t.kind === 'token' && t.defId === REMNANT);
|
||||
assert.deepEqual(rem.map((t) => [t.tileR, t.tileC]), [[10, 2], [11, 2]], 'the enemy\'s tile, then the other free one');
|
||||
assert.deepEqual(atEnd, { n: 3, e: 0 }, 'one energy spent — the elite one first');
|
||||
assert.ok(h.runUntil(() => energyHits(h, u).length === 3, 2), 'the attack that follows releases the three left');
|
||||
assert.ok(h.runUntil(() => energyHits(h, u).length === 3, 4), 'his next attack (3 s interval) releases the three left');
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -62,7 +62,7 @@ test('斩业星熊 in every 自选 form: her operator kit (all three skills auth
|
||||
assert.deepEqual([u.def.charId, u.def.diyFor, u.skill.id, !!u.kit.generic, u.kit.skillSource], [HSG, SLOT[tier], form.skills[skill].skillId, false, 'skills'], label(f));
|
||||
assert.deepEqual([u.base.maxHp, u.base.atk, u.base.def, u.base.res], [form.stats.maxHp + (m?.attr.maxHp ?? 0), form.stats.atk + (m?.attr.atk ?? 0), form.stats.def, 15], `${label(f)}: stats`);
|
||||
assert.deepEqual([u.s.blockCnt, u.profile.attack, u.profile.canHitFly, u.profile.dmgType, u.base.bat], [3, 'melee', false, 'phys', 1.6], `${label(f)}: 驭法铁卫`);
|
||||
assert.equal(form.dmgType, 'arts', 'the data carries the skill-on type');
|
||||
assert.equal(form.dmgType, 'phys', 'the data carries the normal attack\'s type (build-data classifyAttack: "技能开启时…法术伤害")');
|
||||
assert.deepEqual(u.liveRangeGrid, form.rangeGrid, `${label(f)}: 1-1`);
|
||||
assert.deepEqual([u.def.bonds, u.def.raw.garrisonIds], [['yanShip'], []], `${label(f)}: bonds / 特质`);
|
||||
assert.ok(!u.s.flags.liftoff && !u.s.flags.camou && !u.s.flags.stealth && !u.s.flags.untargetable, `${label(f)}: ground enemies target her`);
|
||||
|
||||
@@ -105,12 +105,13 @@ test('白铁 in every 自选 form: his kit (all three skills authored), stats +
|
||||
{ ok: true, picks: { [SLOT[6]]: { charId: IRON, skillIndex: 2, uniEquipId: Y } } });
|
||||
});
|
||||
|
||||
test('<支援装置> hand pieces: the picked skill\'s device only (variant sources), deploy limit 1 in the data (official 1 + max_deploy_count 1 = 2: reported), the module costs / redeploy times', () => {
|
||||
test('<支援装置> hand pieces: the picked skill\'s device only (variant sources), deploy limit 2 in the data (1 + the hidden max_deploy_count 1: E2 "最多可部署2个"), the module costs / redeploy times', () => {
|
||||
for (const [skill, id] of TOK.entries()) {
|
||||
for (const s of [0, 1, 2]) assert.equal(tokOf(id, 6, true, s, null).sources.includes('skill'), s === skill, `${id} under S${s + 1}`);
|
||||
const v = tokOf(id, 6, true, skill, null);
|
||||
assert.equal(v.stats.deployLimit, 1, `${id}: the data's deploy limit`);
|
||||
assert.equal(v.talents.find((t) => t.bb.max_deploy_count != null).bb.max_deploy_count, 1, `${id}: the hidden max_deploy_count (E2 "最多可部署2个")`);
|
||||
assert.equal(v.talents.find((t) => t.bb.max_deploy_count != null).bb.max_deploy_count, 1, `${id}: the hidden max_deploy_count`);
|
||||
assert.equal(v.stats.deployLimit, 2, `${id}: the data's deploy limit (tools/build-data.mjs tokenTalentDeckBonus)`);
|
||||
assert.deepEqual([v.stats.deckStack, tokOf(id, 6, true, skill, X).stats.deckStack], [3, 4], `${id}: the holding 3 (CRA-X +1)`);
|
||||
}
|
||||
// CRA-X 部署费用 −1 / −1 / −4, CRA-Y 再部署时间 −5 / −5 / −10 (PRTS module page)
|
||||
const cost = (mod) => TOK.map((id, s) => tokOf(id, 6, true, s, mod).stats.cost);
|
||||
|
||||
@@ -98,13 +98,14 @@ test('电弧\'s skills in every form: the data\'s SP (time SP, spCost, initSp, c
|
||||
}
|
||||
});
|
||||
|
||||
test('a 自选 pick: 电弧 is offered at tiers 5 and 6 and a roster with her passes validateDiyPicks (the 岁的界园志异 modules SO-A / SO-B are not ISW-*)', () => {
|
||||
test('a 自选 pick: 电弧 is offered at tiers 5 and 6 and a roster with her passes validateDiyPicks — without a module: the 岁的界园志异 modules SO-A / SO-B are refused like ISW-A (0.2.0 WE2)', () => {
|
||||
const data = { chess: CHESS, backups: BACKUPS };
|
||||
assert.ok(KITTED_CHARS.includes(RAD));
|
||||
for (const t of [5, 6]) assert.ok(diyPool(t, { data, kitted: KITTED_CHARS }).includes(RAD), `tier ${t}`);
|
||||
for (const uniEquipId of [SOA, SOB, null]) {
|
||||
assert.deepEqual(validateDiyPicks({ [SLOT[6]]: { charId: RAD, skillIndex: 0, uniEquipId } }, { data, kitted: KITTED_CHARS }),
|
||||
{ ok: true, picks: { [SLOT[6]]: { charId: RAD, skillIndex: 0, uniEquipId } } });
|
||||
assert.deepEqual(validateDiyPicks({ [SLOT[6]]: { charId: RAD, skillIndex: 0, uniEquipId: null } }, { data, kitted: KITTED_CHARS }),
|
||||
{ ok: true, picks: { [SLOT[6]]: { charId: RAD, skillIndex: 0, uniEquipId: null } } });
|
||||
for (const uniEquipId of [SOA, SOB]) {
|
||||
assert.equal(validateDiyPicks({ [SLOT[6]]: { charId: RAD, skillIndex: 0, uniEquipId } }, { data, kitted: KITTED_CHARS }).error, 'BAD_TARGET', uniEquipId);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@ test('黍 in every 自选 form: her operator kit (all three skills authored), th
|
||||
if (skill === 0) assert.deepEqual([u.skill.rule, sk.trigger.rawRule, sk.skillType, u.skill.triggerAllies, u.skill.triggerHpAtMost], ['DEFAULT', 'DEFAULT', 'AUTO', true, 0.5], `${label(f)}: S1`);
|
||||
if (skill === 1) assert.deepEqual([u.skill.rule, sk.trigger.rawRule, sk.skillType, u.skill.triggerAllies], ['DEFAULT', 'TAKE_DAMAGE', 'MANUAL', false], `${label(f)}: S2 (the 重装 exception)`);
|
||||
if (skill === 2) {
|
||||
assert.deepEqual([sk.trigger.rule, sk.trigger.rawRule, sk.skillType], ['TRY_SEARCH_ALLY_SKILL', 'TRY_SEARCH_ALLY_SKILL', 'MANUAL'], `${label(f)}: S3 data rule`);
|
||||
assert.deepEqual([sk.trigger.rule, sk.trigger.rawRule, sk.trigger.allies, sk.trigger.customRangeGrid, sk.skillType], ['SKILL_RANGE', 'TRY_SEARCH_ALLY_SKILL', true, sk.rangeGrid, 'MANUAL'], `${label(f)}: S3 data rule (build-data TRIGGER_ALLY_RULES)`);
|
||||
assert.deepEqual([u.skill.rule, u.skill.triggerAllies, u.skill.triggerGrid], ['SKILL_RANGE', true, sk.rangeGrid], `${label(f)}: S3 engine rule`);
|
||||
}
|
||||
done(h);
|
||||
|
||||
@@ -14,7 +14,7 @@ import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { makeBattle, enemyRec, chessRec, checkInvariants } from '../helpers/battleHarness.js';
|
||||
import { getDefaultSource } from '../../server/sim/simdata.js';
|
||||
import { PITHST_ELEMENTS } from '../../server/sim/content/kits/tier6.js';
|
||||
import { PITHST_ELEMENTS } from '../../server/sim/content/kits/ops/chess_char_1_15-pithst.js';
|
||||
import { spawnYanyou } from '../../server/sim/content/tokens.js';
|
||||
import { COLS } from '../../server/sim/constants.js';
|
||||
|
||||
|
||||
+17
-5
@@ -103,21 +103,33 @@ test('checkDiyPick: an owned 6★ chooses any of its 3 skills and any module of
|
||||
for (const [slot, pick, re] of bad) assert.match(checkDiyPick(slot, pick, DATA).error, re, JSON.stringify(pick));
|
||||
});
|
||||
|
||||
test('validateDiyPicks: a roster never carries a 集成战略 module (ISW-A) [ASSUMED, the owner\'s decision of 2026-10-05]; the record still composes it', () => {
|
||||
test('validateDiyPicks: a roster never carries a module of another game mode — 集成战略 ISW-A / SO-A / SO-B, 生息演算 RA-A [ASSUMED, the owner\'s decision of 2026-10-05 for ISW-A]; the record still composes it', () => {
|
||||
const backups = DATA.backups;
|
||||
let checked = 0;
|
||||
const refused = new Set();
|
||||
for (const charId of backups.diy.ownedPool) {
|
||||
for (const [slot, key] of [[T5, '2/60/7/1'], [T6, '2/60/7/3']]) {
|
||||
for (const mod of backups.units[charId].forms[key]?.modules ?? []) {
|
||||
const pick = { charId, skillIndex: 0, uniEquipId: mod.uniEquipId };
|
||||
assert.ok(checkDiyPick(slot, pick, DATA).ok, `${charId} ${mod.uniEquipId}: a legal record (the sim and the kits field it)`);
|
||||
const v = validateDiyPicks({ [slot]: pick }, { data: DATA });
|
||||
if (/^ISW-/.test(mod.typeName)) { assert.match(v.detail || '', /集成战略 module/, `${charId} ${mod.uniEquipId}`); assert.equal(isDiyModule(mod), false); checked++; }
|
||||
else assert.ok(v.ok, `${charId} ${mod.uniEquipId}: ${v.detail}`);
|
||||
if (/^(ISW|SO|RA)-/.test(mod.typeName)) {
|
||||
assert.match(v.detail || '', /module of another game mode/, `${charId} ${mod.uniEquipId}`);
|
||||
assert.equal(isDiyModule(mod), false);
|
||||
refused.add(`${mod.typeName} ${mod.uniEquipId}`);
|
||||
} else assert.ok(v.ok, `${charId} ${mod.uniEquipId}: ${v.detail}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.ok(checked >= 6, 'the ISW-A modules of the pool (凯尔希, 傀影, 菲亚梅塔, 提丰, 艾丽妮, 霍尔海雅) at both stages');
|
||||
// 凯尔希, 傀影, 菲亚梅塔, 提丰, 艾丽妮, 霍尔海雅 (ISW-A); 电弧 SO-A / SO-B, 机械师 SO-A; 森蚺 RA-A
|
||||
assert.deepEqual([...refused].map((x) => x.split(' ')[0]).sort(), ['ISW-A', 'ISW-A', 'ISW-A', 'ISW-A', 'ISW-A', 'ISW-A', 'RA-A', 'SO-A', 'SO-A', 'SO-B']);
|
||||
// every refused module's own parts are tied to its mode: the text "在…中" of its trait / talents
|
||||
for (const charId of ['char_4195_radian', 'char_4230_mcnist', 'char_416_zumama']) {
|
||||
for (const mod of backups.units[charId].forms['2/60/7/3'].modules) {
|
||||
if (isDiyModule(mod)) continue;
|
||||
const text = JSON.stringify([mod.traitOverride ?? null, mod.talentChanges ?? []]);
|
||||
assert.match(text, /在(集成战略|【[^】]+】|生息演算)中/, `${charId} ${mod.uniEquipId}: a mode-only text`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('diyRecord: the slot\'s identity (tier, price, merge, status), no 特质, derived bonds, the operator\'s body at the slot form; module active on the elite only, stage 1 / 3', () => {
|
||||
|
||||
+112
-109
@@ -1820,35 +1820,35 @@
|
||||
"time": 211.6,
|
||||
"ticks": 6348,
|
||||
"reason": "cleared",
|
||||
"killed": 82,
|
||||
"killed": 74,
|
||||
"total": 150,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 89,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":82,"total":150,"perfect":false,"leaks":68,"leaksUncounted":0,"coins":0,"dmg":1367204,"bossDmg":0,"heal":0,"deaths":13,"layerGains":{}}},
|
||||
"players": {"p1":{"killed":74,"total":150,"perfect":false,"leaks":76,"leaksUncounted":0,"coins":0,"dmg":1358417,"bossDmg":0,"heal":0,"deaths":13,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_6_diy1_b",0,"uniequip_002_ifrit",1544,1,4,29,535201,0,0,93,4],
|
||||
["p1",2,"op","chess_char_6_diy2_b",1,"uniequip_002_ifrit",1544,1,5,14,235845,0,0,72,39],
|
||||
["p1",3,"op","chess_char_6_diy1_b",2,"uniequip_002_ifrit",1544,1,171,1,98866,1544,0,17,3],
|
||||
["p1",4,"op","chess_char_6_diy2_b",0,"uniequip_003_ifrit",1744,1,13,0,87409,0,0,74,4],
|
||||
["p1",5,"op","chess_char_6_diy1_b",1,"uniequip_003_ifrit",1744,1,6,18,156957,0,0,66,34],
|
||||
["p1",1,"op","chess_char_6_diy1_b",0,"uniequip_002_ifrit",1544,1,4,30,548258,0,0,93,4],
|
||||
["p1",2,"op","chess_char_6_diy2_b",1,"uniequip_002_ifrit",1544,1,5,13,217959,0,0,72,39],
|
||||
["p1",3,"op","chess_char_6_diy1_b",2,"uniequip_002_ifrit",1544,1,171,1,99441,1544,0,17,3],
|
||||
["p1",4,"op","chess_char_6_diy2_b",0,"uniequip_003_ifrit",1744,1,13,1,87328,0,0,74,4],
|
||||
["p1",5,"op","chess_char_6_diy1_b",1,"uniequip_003_ifrit",1744,1,6,12,157838,0,0,65,33],
|
||||
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_003_ifrit",0,0,14,0,63597,1744,0,20,3],
|
||||
["p1",7,"op","chess_char_5_diy1_a",0,null,0,0,121,0,2158,2522,0,3,0],
|
||||
["p1",8,"op","chess_char_5_diy2_a",1,null,0,0,154,8,58152,2522,0,10,1],
|
||||
["p1",9,"op","chess_char_5_diy1_a",2,null,0,0,100,2,21405,2522,0,10,1],
|
||||
["p1",10,"op","chess_char_5_diy2_b",0,"none",0,0,95,2,14759,3052,0,9,0],
|
||||
["p1",11,"op","chess_char_5_diy1_b",1,"none",0,0,90,3,27322,3052,0,3,2],
|
||||
["p1",12,"op","chess_char_5_diy2_b",2,"none",0,0,8,5,65534,1526,0,20,1],
|
||||
["p1",8,"op","chess_char_5_diy2_a",1,null,0,0,154,6,51945,2522,0,11,1],
|
||||
["p1",9,"op","chess_char_5_diy1_a",2,null,0,0,100,2,19121,2522,0,10,1],
|
||||
["p1",10,"op","chess_char_5_diy2_b",0,"none",0,0,96,3,16304,3052,0,10,0],
|
||||
["p1",11,"op","chess_char_5_diy1_b",1,"none",0,0,90,3,32589,3052,0,4,2],
|
||||
["p1",12,"op","chess_char_5_diy2_b",2,"none",0,0,8,3,61880,1526,0,17,1],
|
||||
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
|
||||
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null],
|
||||
["-","~3","device","trap_1105_accrate",null,null,100,1,3,0,0,0,0,0,null]
|
||||
],
|
||||
"enemies": {"enemy_1249_lysdb_2":[60,34,26,5486,596279],"enemy_1251_lysyta":[51,35,16,909,240893],"enemy_1329_cbshld":[39,13,26,8801,530032]},
|
||||
"hooks": [1,171,6348,433,431,1867,2367,0,95,163,92,92,18,29289,22,25,150,68,1,95,0,0,0],
|
||||
"events": {"atk":1247,"deploy":18,"die":95,"dmg":2367,"engage":12,"fx":236,"leak":68,"skill":184,"spawn":165,"status":44},
|
||||
"fx": {"aoe":73,"beam":3,"flame":100,"shieldBreak":60},
|
||||
"snaps": "43307da7 4ff2063f 2a153be0 3aba4c54 30fdf3ac 1f9215af cf66cfaa 8012d31d 985f38b3 a23ece2e 38d610e7 33eab3e4 0832bad4 fdd45552 4c989016 b99015e8 e5e90c65 02b8067a c5784c78 5f21d2f4 59cec77b fe00505d"
|
||||
"enemies": {"enemy_1249_lysdb_2":[60,32,28,4443,590753],"enemy_1251_lysyta":[51,32,19,942,239115],"enemy_1329_cbshld":[39,10,29,9811,528548]},
|
||||
"hooks": [1,171,6348,432,430,1848,2352,0,87,163,91,91,15,29250,22,25,150,76,1,87,0,0,0],
|
||||
"events": {"atk":1258,"deploy":18,"die":87,"dmg":2352,"engage":12,"fx":235,"leak":76,"skill":182,"spawn":165,"status":44},
|
||||
"fx": {"aoe":72,"beam":3,"flame":100,"shieldBreak":60},
|
||||
"snaps": "a92391fb 65d01483 6ce4a17f b71d2eb4 41903aab ff59f11c 19708ae2 91521e30 13cbe1cb 6aedec66 803f5122 4d21778b 6238ae29 63609ca2 b757c3a4 f2beec8c 6962ad79 f76f5262 3fdc9f26 ef4a8d27 c8a0adc1 98f390f9"
|
||||
},
|
||||
"diy-046": {
|
||||
"about": "5_diy1_b=aphris/S1/002_aphris 5_diy2_b=aphris/S2/002_aphris 5_diy1_b=aphris/S3/002_aphris 6_diy1_a=aphris/S1 6_diy2_a=aphris/S2 6_diy1_a=aphris/S3 6_diy2_b=aphris/S1 6_diy1_b=aphris/S2 6_diy2_b=aphris/S3 6_diy1_b=aphris/S1/002_aphris 6_diy2_b=aphris/S2/002_aphris 6_diy1_b=aphris/S3/002_aphris",
|
||||
@@ -3365,10 +3365,10 @@
|
||||
["-","~3","device","trap_1105_accrate",null,null,100,1,3,0,0,0,0,0,null]
|
||||
],
|
||||
"enemies": {"enemy_1019_jshoot_2":[57,41,16,9154,183638],"enemy_1299_ymkilr_2":[27,10,17,47487,203959],"enemy_1422_lrsldr_2":[39,16,23,1386,172539]},
|
||||
"hooks": [1,149,6820,1026,1026,1315,1436,0,88,144,56,55,0,17849,892,94,123,56,1,88,0,6,0],
|
||||
"hooks": [1,149,6820,1026,1026,1315,1436,0,88,144,56,55,0,17851,892,94,123,56,1,88,0,6,0],
|
||||
"events": {"atk":1056,"deploy":23,"die":88,"dmg":1436,"engage":12,"fx":74,"leak":56,"skill":111,"spawn":138,"status":71},
|
||||
"fx": {"aoe":20,"burst":6,"shield":24,"shieldBreak":24},
|
||||
"snaps": "45ecc41c 880ee0dd b36699bc 992f1255 23143b3d 3ba40a64 ead1143c 29b600b7 60ec6974 eff36649 bbaee861 b11de034 d58d5c37 63232bfe 4e305d69 848a4366 cd76bd35 ad1f8c0f 8e3f9ba5 2b3b69eb 71bc494b 17b62c2a 2f3b24a4"
|
||||
"snaps": "45ecc41c 880ee0dd b36699bc 992f1255 23143b3d 3ba40a64 ead1143c 29b600b7 60ec6974 eff36649 b5a79642 02d6faca e4063ab1 22bf8f0f 14cf16ac 3e4fc06b 12025e38 96906102 cd2db10c 106c766a bc2c5496 1bb121b3 410896a5"
|
||||
},
|
||||
"diy-079": {
|
||||
"about": "5_diy1_b=headb2/S1/002_headb2 5_diy2_b=headb2/S2/002_headb2 5_diy1_b=headb2/S3/002_headb2 6_diy1_a=headb2/S1 6_diy2_a=headb2/S2 6_diy1_a=headb2/S3 6_diy2_b=headb2/S1 6_diy1_b=headb2/S2 6_diy2_b=headb2/S3 6_diy1_b=headb2/S1/002_headb2 6_diy2_b=headb2/S2/002_headb2 6_diy1_b=headb2/S3/002_headb2",
|
||||
@@ -4867,45 +4867,45 @@
|
||||
"time": 205.67,
|
||||
"ticks": 6170,
|
||||
"reason": "cleared",
|
||||
"killed": 124,
|
||||
"total": 189,
|
||||
"killed": 121,
|
||||
"total": 183,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 8,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":124,"total":189,"perfect":false,"leaks":65,"leaksUncounted":0,"coins":0,"dmg":549243,"bossDmg":0,"heal":0,"deaths":16,"layerGains":{}}},
|
||||
"players": {"p1":{"killed":121,"total":183,"perfect":false,"leaks":62,"leaksUncounted":0,"coins":0,"dmg":524053,"bossDmg":0,"heal":0,"deaths":17,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_6_diy1_a",0,null,1884,1,191,8,54401,1884,0,59,2],
|
||||
["p1",2,"op","chess_char_6_diy2_a",1,null,0,0,210,41,38926,3768,0,48,2],
|
||||
["p1",3,"op","chess_char_6_diy1_a",2,null,1884,1,218,11,57866,1884,0,43,1],
|
||||
["p1",4,"op","chess_char_6_diy2_b",0,"none",2216,1,196,14,85238,2216,0,68,2],
|
||||
["p1",5,"op","chess_char_6_diy1_b",1,"none",0,0,153,18,110415,4432,0,41,1],
|
||||
["p1",6,"op","chess_char_6_diy2_b",2,"none",0,0,182,13,101208,4432,0,48,1],
|
||||
["p1",1,"op","chess_char_6_diy1_a",0,null,1884,1,190,7,74139,1884,0,56,2],
|
||||
["p1",2,"op","chess_char_6_diy2_a",1,null,0,0,204,35,51212,3768,0,38,2],
|
||||
["p1",3,"op","chess_char_6_diy1_a",2,null,0,0,193,8,36822,3768,0,38,1],
|
||||
["p1",4,"op","chess_char_6_diy2_b",0,"none",2216,1,212,5,23864,2216,0,45,2],
|
||||
["p1",5,"op","chess_char_6_diy1_b",1,"none",0,0,153,18,103390,4432,0,41,1],
|
||||
["p1",6,"op","chess_char_6_diy2_b",2,"none",0,0,182,14,109015,4432,0,46,1],
|
||||
["p1",7,"op","chess_char_5_diy1_a",0,null,1359,1,4,0,0,0,0,0,0],
|
||||
["p1",8,"op","chess_char_5_diy2_a",1,null,1359,1,5,2,31646,0,0,53,53],
|
||||
["p1",9,"op","chess_char_5_diy1_a",2,null,1359,1,154,0,39160,1359,0,42,3],
|
||||
["p1",10,"op","chess_char_5_diy2_b",0,"none",0,0,180,2,9770,3140,0,25,2],
|
||||
["p1",11,"op","chess_char_5_diy1_b",1,"none",0,0,175,0,6679,3140,0,21,4],
|
||||
["p1",12,"op","chess_char_5_diy2_b",2,"none",0,0,141,4,9738,3140,0,12,1],
|
||||
["p1",8,"op","chess_char_5_diy2_a",1,null,1359,1,5,3,28459,0,0,52,0],
|
||||
["p1",9,"op","chess_char_5_diy1_a",2,null,1359,1,176,0,49348,1359,0,39,2],
|
||||
["p1",10,"op","chess_char_5_diy2_b",0,"none",0,0,181,4,14787,3140,0,38,3],
|
||||
["p1",11,"op","chess_char_5_diy1_b",1,"none",0,0,174,1,9156,3140,0,21,4],
|
||||
["p1",12,"op","chess_char_5_diy2_b",2,"none",0,0,141,4,11236,3140,0,12,1],
|
||||
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
|
||||
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null],
|
||||
["-","~3","device","trap_1105_accrate",null,null,100,1,3,0,0,0,0,0,null],
|
||||
["p1","~1","token","token_10024_ebnhlz_rcube",null,null,1000,0,37,0,2623,0,0,0,null],
|
||||
["p1","~2","token","token_10024_ebnhlz_rcube",null,null,1000,0,53,0,1,0,0,0,null],
|
||||
["p1","~3","token","token_10024_ebnhlz_rcube",null,null,1000,0,71,0,1572,0,0,0,null],
|
||||
["p1","~4","token","token_10024_ebnhlz_rcube",null,null,1000,0,83,0,0,0,0,0,null]
|
||||
["p1","~1","token","token_10024_ebnhlz_rcube",null,null,1000,0,37,0,1572,0,0,0,null],
|
||||
["p1","~2","token","token_10024_ebnhlz_rcube",null,null,1000,0,52,0,0,0,0,0,null],
|
||||
["p1","~3","token","token_10024_ebnhlz_rcube",null,null,1000,0,67,0,6859,0,0,0,null],
|
||||
["p1","~4","token","token_10024_ebnhlz_rcube",null,null,1000,0,81,0,4195,0,0,0,null]
|
||||
],
|
||||
"enemies": {
|
||||
"enemy_1197_sfshu": [15,11,4,2272,51823],
|
||||
"enemy_1197_sfshu_2": [39,21,18,6413,151033],
|
||||
"enemy_1198_msfshu": [33,26,7,0,81],
|
||||
"enemy_1198_msfshu_2": [63,54,9,0,225],
|
||||
"enemy_9010_acpupp": [39,12,27,16255,412992]
|
||||
"enemy_1197_sfshu": [15,10,5,1799,50834],
|
||||
"enemy_1197_sfshu_2": [39,20,19,6094,139516],
|
||||
"enemy_1198_msfshu": [30,26,4,0,79],
|
||||
"enemy_1198_msfshu_2": [60,54,6,0,217],
|
||||
"enemy_9010_acpupp": [39,11,28,18930,401042]
|
||||
},
|
||||
"hooks": [1,218,6170,550,550,2484,2286,0,140,209,72,69,61,11555,18,37,189,65,1,153,0,0,0],
|
||||
"events": {"atk":913,"deploy":26,"die":144,"dmg":2286,"engage":11,"fx":143,"leak":65,"skill":141,"spawn":208,"status":408},
|
||||
"fx": {"aoe":4,"dash":3,"displace":8,"ember":13,"pull":8,"shieldBreak":37,"summon":36,"takeoff":9,"telegraph":13,"volley":12},
|
||||
"snaps": "97d774f4 d56f2c40 38cfa54f 7e2be674 45424794 b549bc6d d02f3886 8eef35d8 186b734d ef22c4fe 6eed958f 1c82384a 2d257b7e e4c0bf84 b322c2f5 561c30ac c139ed4b 0f61007b e04f8bf8 c8e87b59 3c8fbdd4"
|
||||
"hooks": [1,212,6170,507,507,2408,2252,0,138,204,19,17,61,12292,18,39,183,62,1,150,0,0,0],
|
||||
"events": {"atk":827,"deploy":26,"die":142,"dmg":2252,"engage":11,"fx":136,"leak":62,"skill":36,"spawn":202,"status":382},
|
||||
"fx": {"aoe":4,"dash":3,"displace":8,"ember":12,"pull":8,"revive":1,"shieldBreak":28,"summon":34,"takeoff":9,"telegraph":12,"volley":17},
|
||||
"snaps": "97d774f4 3a87e9c0 6315b127 d1ff957b 8b0cf599 ffcafc10 f97c63e0 b090bfcf 503cb562 30e78f94 453e3e4b 5c62e222 794d1210 f8da69f3 40109745 bfd2d8ad 9529555d def80e98 4395baef ad6ed566 87adb778"
|
||||
},
|
||||
"diy-115": {
|
||||
"about": "5_diy1_b=ebnhlz/S1/002_ebnhlz 5_diy2_b=ebnhlz/S2/002_ebnhlz 5_diy1_b=ebnhlz/S3/002_ebnhlz 5_diy2_b=ebnhlz/S1/003_ebnhlz 5_diy1_b=ebnhlz/S2/003_ebnhlz 5_diy2_b=ebnhlz/S3/003_ebnhlz 5_diy1_b=ebnhlz/S1/004_ebnhlz 5_diy2_b=ebnhlz/S2/004_ebnhlz 5_diy1_b=ebnhlz/S3/004_ebnhlz 6_diy1_a=ebnhlz/S1 6_diy2_a=ebnhlz/S2 6_diy1_a=ebnhlz/S3",
|
||||
@@ -4916,59 +4916,61 @@
|
||||
"round": 12,
|
||||
"seed": 2479968306,
|
||||
"waveId": "act1autochess_h05",
|
||||
"time": 193.8,
|
||||
"ticks": 5814,
|
||||
"time": 193.6,
|
||||
"ticks": 5808,
|
||||
"reason": "cleared",
|
||||
"killed": 195,
|
||||
"total": 246,
|
||||
"killed": 197,
|
||||
"total": 243,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 0,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":195,"total":246,"perfect":false,"leaks":51,"leaksUncounted":0,"coins":0,"dmg":515319,"bossDmg":0,"heal":0,"deaths":9,"layerGains":{}}},
|
||||
"players": {"p1":{"killed":197,"total":243,"perfect":false,"leaks":46,"leaksUncounted":0,"coins":0,"dmg":532641,"bossDmg":0,"heal":0,"deaths":8,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_5_diy1_b",0,"uniequip_002_ebnhlz",1570,1,10,7,61901,0,0,45,5],
|
||||
["p1",2,"op","chess_char_5_diy2_b",1,"uniequip_002_ebnhlz",1570,1,11,9,35088,0,0,28,25],
|
||||
["p1",3,"op","chess_char_5_diy1_b",2,"uniequip_002_ebnhlz",1570,1,1,9,12719,0,0,24,4],
|
||||
["p1",4,"op","chess_char_5_diy2_b",0,"uniequip_003_ebnhlz",1650,1,3,21,55599,0,0,72,7],
|
||||
["p1",5,"op","chess_char_5_diy1_b",1,"uniequip_003_ebnhlz",1650,1,5,7,56916,0,0,60,33],
|
||||
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_003_ebnhlz",1650,1,7,12,124221,0,0,54,4],
|
||||
["p1",2,"op","chess_char_5_diy2_b",1,"uniequip_002_ebnhlz",1570,1,11,9,33720,0,0,27,0],
|
||||
["p1",3,"op","chess_char_5_diy1_b",2,"uniequip_002_ebnhlz",1570,1,1,11,16041,0,0,24,4],
|
||||
["p1",4,"op","chess_char_5_diy2_b",0,"uniequip_003_ebnhlz",1650,1,3,15,56353,0,0,77,7],
|
||||
["p1",5,"op","chess_char_5_diy1_b",1,"uniequip_003_ebnhlz",1650,1,5,10,52596,0,0,60,6],
|
||||
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_003_ebnhlz",1650,1,7,13,127550,0,0,51,4],
|
||||
["p1",7,"op","chess_char_5_diy1_b",0,"uniequip_004_ebnhlz",1570,1,9,0,0,0,0,0,3],
|
||||
["p1",8,"op","chess_char_5_diy2_b",1,"uniequip_004_ebnhlz",1570,1,12,6,63692,0,0,49,11],
|
||||
["p1",9,"op","chess_char_5_diy1_b",2,"uniequip_004_ebnhlz",0,0,194,2,8623,3140,0,8,2],
|
||||
["p1",10,"op","chess_char_6_diy1_a",0,null,0,0,172,10,12844,2718,0,33,2],
|
||||
["p1",11,"op","chess_char_6_diy2_a",1,null,1025.48,1,275,3,14668,3052,0,17,7],
|
||||
["p1",12,"op","chess_char_6_diy1_a",2,null,0,0,269,0,16023,4077,0,12,1],
|
||||
["p1","~1","token","token_10024_ebnhlz_rcube",null,null,1000,0,29,0,10534,0,0,0,null],
|
||||
["p1","~2","token","token_10024_ebnhlz_rcube",null,null,1000,0,32,0,0,0,0,0,null],
|
||||
["p1","~3","token","token_10024_ebnhlz_rcube",null,null,1000,0,56,0,7762,0,0,0,null],
|
||||
["p1","~4","token","token_10024_ebnhlz_rcube",null,null,1000,0,78,1,4611,0,0,0,null],
|
||||
["p1","~5","token","token_10024_ebnhlz_rcube",null,null,1000,0,87,0,0,0,0,0,null],
|
||||
["p1","~6","token","token_10024_ebnhlz_rcube",null,null,1000,0,106,0,7762,0,0,0,null],
|
||||
["p1","~7","token","token_10024_ebnhlz_rcube",null,null,1000,0,128,0,0,0,0,0,null],
|
||||
["p1","~8","token","token_10024_ebnhlz_rcube",null,null,1000,0,133,0,2772,0,0,0,null],
|
||||
["p1","~9","token","token_10024_ebnhlz_rcube",null,null,1000,0,150,0,2219,0,0,0,null],
|
||||
["p1","~10","token","token_10024_ebnhlz_rcube",null,null,1000,0,162,0,0,0,0,0,null],
|
||||
["p1","~11","token","token_10024_ebnhlz_rcube",null,null,1000,0,169,1,4611,0,0,0,null],
|
||||
["p1","~12","token","token_10024_ebnhlz_rcube",null,null,1000,0,197,0,2218,0,0,0,null],
|
||||
["p1","~13","token","token_10024_ebnhlz_rcube",null,null,1000,0,219,0,0,0,0,0,null],
|
||||
["p1","~14","token","token_10024_ebnhlz_rcube",null,null,1000,0,220,0,2772,0,0,0,null],
|
||||
["p1","~15","token","token_10024_ebnhlz_rcube",null,null,1000,0,240,0,2776,0,0,0,null],
|
||||
["p1","~16","token","token_10024_ebnhlz_rcube",null,null,1000,1,254,0,0,0,0,0,null],
|
||||
["p1","~17","token","token_10024_ebnhlz_rcube",null,null,1000,0,256,0,4990,0,0,0,null]
|
||||
["p1",8,"op","chess_char_5_diy2_b",1,"uniequip_004_ebnhlz",1570,1,12,8,66486,0,0,49,13],
|
||||
["p1",9,"op","chess_char_5_diy1_b",2,"uniequip_004_ebnhlz",0,0,225,5,6116,3140,0,10,2],
|
||||
["p1",10,"op","chess_char_6_diy1_a",0,null,0,0,164,8,12174,2718,0,31,2],
|
||||
["p1",11,"op","chess_char_6_diy2_a",1,null,1025.48,1,275,5,13604,3052,0,16,0],
|
||||
["p1",12,"op","chess_char_6_diy1_a",2,null,81.61,1,270,2,15684,3995,0,14,1],
|
||||
["p1","~1","token","token_10024_ebnhlz_rcube",null,null,1000,0,29,0,0,0,0,0,null],
|
||||
["p1","~2","token","token_10024_ebnhlz_rcube",null,null,1000,0,30,1,12752,0,0,0,null],
|
||||
["p1","~3","token","token_10024_ebnhlz_rcube",null,null,1000,0,55,0,5544,0,0,0,null],
|
||||
["p1","~4","token","token_10024_ebnhlz_rcube",null,null,1000,0,70,0,4990,0,0,0,null],
|
||||
["p1","~5","token","token_10024_ebnhlz_rcube",null,null,1000,0,72,0,0,0,0,0,null],
|
||||
["p1","~6","token","token_10024_ebnhlz_rcube",null,null,1000,0,92,0,4990,0,0,0,null],
|
||||
["p1","~7","token","token_10024_ebnhlz_rcube",null,null,1000,0,122,0,12197,0,0,0,null],
|
||||
["p1","~8","token","token_10024_ebnhlz_rcube",null,null,1000,0,125,0,0,0,0,0,null],
|
||||
["p1","~9","token","token_10024_ebnhlz_rcube",null,null,1000,0,138,0,2772,0,0,0,null],
|
||||
["p1","~10","token","token_10024_ebnhlz_rcube",null,null,1000,0,155,0,2221,0,0,0,null],
|
||||
["p1","~11","token","token_10024_ebnhlz_rcube",null,null,1000,0,160,0,0,0,0,0,null],
|
||||
["p1","~12","token","token_10024_ebnhlz_rcube",null,null,1000,0,171,0,4990,0,0,0,null],
|
||||
["p1","~13","token","token_10024_ebnhlz_rcube",null,null,1000,0,204,0,9425,0,0,0,null],
|
||||
["p1","~14","token","token_10024_ebnhlz_rcube",null,null,1000,0,216,0,0,0,0,0,null],
|
||||
["p1","~15","token","token_10024_ebnhlz_rcube",null,null,1000,0,223,0,2772,0,0,0,null],
|
||||
["p1","~16","token","token_10024_ebnhlz_rcube",null,null,1000,0,243,0,2776,0,0,0,null],
|
||||
["p1","~17","token","token_10024_ebnhlz_rcube",null,null,1000,1,254,0,0,0,0,0,null],
|
||||
["p1","~18","token","token_10024_ebnhlz_rcube",null,null,1000,0,257,0,4990,0,0,0,null],
|
||||
["p1","~19","token","token_10024_ebnhlz_rcube",null,null,1000,0,272,0,0,0,0,0,null]
|
||||
],
|
||||
"enemies": {
|
||||
"enemy_1195_sfyin": [45,24,21,8753,152906],
|
||||
"enemy_1196_msfyin": [48,44,4,0,90],
|
||||
"enemy_1197_sfshu": [39,30,9,4234,206523],
|
||||
"enemy_1198_msfshu": [90,76,14,0,242],
|
||||
"enemy_1195_sfyin": [45,30,15,8741,173581],
|
||||
"enemy_1196_msfyin": [60,51,9,0,105],
|
||||
"enemy_1197_sfshu": [39,25,14,4164,201605],
|
||||
"enemy_1198_msfshu": [75,70,5,0,216],
|
||||
"enemy_1199_sfjin": [12,12,0,0,186325],
|
||||
"enemy_1200_msfjin": [12,9,3,0,348]
|
||||
"enemy_1200_msfjin": [12,9,3,0,346]
|
||||
},
|
||||
"hooks": [1,281,5814,450,450,1977,2088,0,204,271,104,102,0,27867,0,31,246,51,1,204,0,0,0],
|
||||
"events": {"atk":450,"deploy":35,"die":220,"dmg":2088,"engage":11,"fx":189,"leak":51,"skill":206,"spawn":275},
|
||||
"fx": {"aoe":11,"displace":13,"pull":13,"summon":83,"volley":69},
|
||||
"snaps": "b588f908 018cc7d1 02df24b0 d7fb7008 01901fca 88a112ed f0544f8d 91caefd2 24dd8e93 26798334 2b5c19c3 091cb2b8 45da698c 1d8e5f22 91d224aa a413c6ff af6b4e5f d69eff16 da950540 179fa098"
|
||||
"hooks": [1,280,5808,450,450,1944,2063,0,205,269,47,45,0,28772,0,29,243,46,1,205,0,0,0],
|
||||
"events": {"atk":450,"deploy":37,"die":223,"dmg":2063,"engage":11,"fx":197,"leak":46,"skill":92,"spawn":274},
|
||||
"fx": {"aoe":13,"displace":15,"pull":15,"summon":86,"volley":68},
|
||||
"snaps": "b588f908 30b58fc4 0067aae8 073e6898 0cbd5145 a0736062 6e599248 f14345d4 7c4901d1 2f52111e a8cc7280 ac1fd796 f60de3a9 9b4b7177 cc02ceba e6974a52 cf27fc35 a4a00c5a a7501807 6a48f2bc"
|
||||
},
|
||||
"diy-116": {
|
||||
"about": "6_diy1_b=ebnhlz/S1 6_diy2_b=ebnhlz/S2 6_diy1_b=ebnhlz/S3 6_diy2_b=ebnhlz/S1/002_ebnhlz 6_diy1_b=ebnhlz/S2/002_ebnhlz 6_diy2_b=ebnhlz/S3/002_ebnhlz 6_diy1_b=ebnhlz/S1/003_ebnhlz 6_diy2_b=ebnhlz/S2/003_ebnhlz 6_diy1_b=ebnhlz/S3/003_ebnhlz 6_diy2_b=ebnhlz/S1/004_ebnhlz 6_diy1_b=ebnhlz/S2/004_ebnhlz 6_diy2_b=ebnhlz/S3/004_ebnhlz",
|
||||
@@ -4986,31 +4988,32 @@
|
||||
"total": 123,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": null,
|
||||
"rngDraws": 5,
|
||||
"rngDraws": 6,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":1,"total":123,"perfect":false,"leaks":122,"leaksUncounted":0,"coins":0,"dmg":271272,"bossDmg":0,"heal":0,"deaths":19,"layerGains":{}}},
|
||||
"players": {"p1":{"killed":1,"total":123,"perfect":false,"leaks":122,"leaksUncounted":0,"coins":0,"dmg":286822,"bossDmg":0,"heal":0,"deaths":19,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_6_diy1_b",0,"none",1570,1,147,0,23015,3140,0,31,2],
|
||||
["p1",2,"op","chess_char_6_diy2_b",1,"none",0,0,123,0,19192,3140,0,10,1],
|
||||
["p1",3,"op","chess_char_6_diy1_b",2,"none",1570,1,134,0,52931,1570,0,26,2],
|
||||
["p1",4,"op","chess_char_6_diy2_b",0,"uniequip_002_ebnhlz",0,0,146,0,15092,3140,0,15,1],
|
||||
["p1",5,"op","chess_char_6_diy1_b",1,"uniequip_002_ebnhlz",0,0,82,0,13594,3140,0,7,1],
|
||||
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_ebnhlz",0,0,78,0,9982,3140,0,6,0],
|
||||
["p1",7,"op","chess_char_6_diy1_b",0,"uniequip_003_ebnhlz",0,0,74,0,14427,3490,0,4,0],
|
||||
["p1",8,"op","chess_char_6_diy2_b",1,"uniequip_003_ebnhlz",0,0,7,0,14109,1745,0,5,1],
|
||||
["p1",9,"op","chess_char_6_diy1_b",2,"uniequip_003_ebnhlz",0,0,9,0,19195,1745,0,7,1],
|
||||
["p1",10,"op","chess_char_6_diy2_b",0,"uniequip_004_ebnhlz",0,0,79,0,8532,3140,0,4,0],
|
||||
["p1",11,"op","chess_char_6_diy1_b",1,"uniequip_004_ebnhlz",0,0,4,1,20024,1570,0,6,1],
|
||||
["p1",12,"op","chess_char_6_diy2_b",2,"uniequip_004_ebnhlz",0,0,6,0,34442,1570,0,6,1],
|
||||
["p1","~1","token","token_10024_ebnhlz_rcube",null,null,1000,0,27,0,15400,0,0,0,null],
|
||||
["p1","~2","token","token_10024_ebnhlz_rcube",null,null,1000,0,28,0,4454,0,0,0,null],
|
||||
["p1","~3","token","token_10024_ebnhlz_rcube",null,null,1000,0,29,0,6883,0,0,0,null]
|
||||
["p1",1,"op","chess_char_6_diy1_b",0,"none",1570,1,148,1,25031,3140,0,31,2],
|
||||
["p1",2,"op","chess_char_6_diy2_b",1,"none",0,0,124,0,18799,3140,0,10,2],
|
||||
["p1",3,"op","chess_char_6_diy1_b",2,"none",1570,1,135,0,49295,1570,0,24,2],
|
||||
["p1",4,"op","chess_char_6_diy2_b",0,"uniequip_002_ebnhlz",0,0,147,0,16828,3140,0,16,1],
|
||||
["p1",5,"op","chess_char_6_diy1_b",1,"uniequip_002_ebnhlz",0,0,83,0,13594,3140,0,7,1],
|
||||
["p1",6,"op","chess_char_6_diy2_b",2,"uniequip_002_ebnhlz",0,0,79,0,9982,3140,0,6,0],
|
||||
["p1",7,"op","chess_char_6_diy1_b",0,"uniequip_003_ebnhlz",0,0,75,0,14427,3490,0,4,0],
|
||||
["p1",8,"op","chess_char_6_diy2_b",1,"uniequip_003_ebnhlz",0,0,7,0,16808,1745,0,7,0],
|
||||
["p1",9,"op","chess_char_6_diy1_b",2,"uniequip_003_ebnhlz",0,0,9,0,22161,1745,0,7,1],
|
||||
["p1",10,"op","chess_char_6_diy2_b",0,"uniequip_004_ebnhlz",0,0,80,0,8532,3140,0,4,0],
|
||||
["p1",11,"op","chess_char_6_diy1_b",1,"uniequip_004_ebnhlz",0,0,4,0,18212,1570,0,4,1],
|
||||
["p1",12,"op","chess_char_6_diy2_b",2,"uniequip_004_ebnhlz",0,0,6,0,39389,1570,0,8,1],
|
||||
["p1","~1","token","token_10024_ebnhlz_rcube",null,null,1000,0,26,0,4454,0,0,0,null],
|
||||
["p1","~2","token","token_10024_ebnhlz_rcube",null,null,1000,0,27,0,17360,0,0,0,null],
|
||||
["p1","~3","token","token_10024_ebnhlz_rcube",null,null,1000,0,28,0,4876,0,0,0,null],
|
||||
["p1","~4","token","token_10024_ebnhlz_rcube",null,null,1000,0,37,0,7074,0,0,0,null]
|
||||
],
|
||||
"enemies": {"enemy_1006_shield_3":[21,0,21,8166,127088],"enemy_1043_zomsbr":[66,1,65,14521,60428],"enemy_1402_tgshd_2":[36,0,36,7843,83756]},
|
||||
"hooks": [1,147,6528,189,189,333,355,10424,20,145,11,11,0,8727,0,21,123,122,1,20,0,1,0],
|
||||
"events": {"atk":189,"deploy":24,"die":23,"dmg":355,"engage":12,"fx":96,"leak":122,"skill":22,"spawn":138,"status":36},
|
||||
"fx": {"aoe":3,"burst":1,"displace":5,"expose":67,"pull":5,"summon":3,"volley":12},
|
||||
"snaps": "389ca6e3 c8f155a4 e2e7a3b2 2f4c8838 6a9c72f0 8c585b7e a26f5d0c 80f56fed a04d35ab 88374c55 043f03c3 acc9e8b4 6dc5a029 a28ee86b d72cad57 77ed9536 4cd333ad 94a7e729 29475fd8 b7079863 f08523df a6844f3a"
|
||||
"enemies": {"enemy_1006_shield_3":[21,0,21,8025,133387],"enemy_1043_zomsbr":[66,1,65,13522,62006],"enemy_1402_tgshd_2":[36,0,36,8983,91429]},
|
||||
"hooks": [1,148,6528,187,187,341,362,10129,20,146,11,11,0,8777,0,22,123,122,1,20,0,1,0],
|
||||
"events": {"atk":187,"deploy":25,"die":24,"dmg":362,"engage":12,"fx":108,"leak":122,"skill":22,"spawn":139,"status":38},
|
||||
"fx": {"aoe":4,"burst":1,"displace":6,"expose":74,"pull":6,"summon":4,"volley":13},
|
||||
"snaps": "389ca6e3 d4a0046f e8e318e4 2dfb6564 d6b9ac5b 05f87e6d 4fddc6c0 abdaf56e 564d85ec f523de7f 8fc91296 e8062bef a97ba650 2328edb5 b076db9a 504c2000 80dda69d 2e369d1e 56349199 70fdf2ec 97f6afb9 059a1cef"
|
||||
},
|
||||
"diy-117": {
|
||||
"about": "5_diy1_a=mgllan/S1 5_diy2_a=mgllan/S2 5_diy1_a=mgllan/S3 5_diy2_b=mgllan/S1 5_diy1_b=mgllan/S2 5_diy2_b=mgllan/S3 5_diy1_b=mgllan/S1/002_mgllan 5_diy2_b=mgllan/S2/002_mgllan 5_diy1_b=mgllan/S3/002_mgllan 5_diy2_b=mgllan/S1/003_mgllan 5_diy1_b=mgllan/S2/003_mgllan 5_diy2_b=mgllan/S3/003_mgllan",
|
||||
|
||||
+13
-13
@@ -1623,7 +1623,7 @@
|
||||
"boost": null,
|
||||
"setup": {"stageId":"act1autochess_m03","bossId":"boss_7","hiddenBossId":"boss_10","factions":["ELEMENT","INVISIBLE","REFLECTION"]},
|
||||
"end": {"victory":true,"roundsPassed":14,"hiddenReached":false,"hiddenCleared":false,"reason":"victory","teamLp":55,"bossPoolLeft":0,"virtualMs":122900},
|
||||
"rng": {"setup":25,"shop":461,"waves":0,"draft":34,"bots":16433,"meta":20},
|
||||
"rng": {"setup":25,"shop":503,"waves":0,"draft":34,"bots":16504,"meta":20},
|
||||
"errors": {"engine":0,"logged":0,"sim":0,"dispatcher":0},
|
||||
"rounds": [
|
||||
{"round":1,"h_0":[1,27,4,0,0,5,1,"3e4697a0",0,"",5,5,0,3],"ai_0":[1,28,4,0,0,1,2,"a1734bc6",0,"sargonShip 0/2, soloShip 1/0",5,5,0,4]},
|
||||
@@ -1666,18 +1666,18 @@
|
||||
},
|
||||
{
|
||||
"round": 11,
|
||||
"h_0": [1,27,14,0,2,5,8,"7c42dc27",357,"arcaneShip 0/2, indomShip 1/16, preciShip 2/109, sargonShip 1/137, skillfulShip 1/8, steadShip 1/51, suntShip 1/0, victoriaShip 0/19, visiShip 1/36",19,19,0,105],
|
||||
"h_0": [1,27,14,0,2,6,8,"a5cfb056",363,"arcaneShip 0/2, indomShip 1/16, preciShip 2/116, sargonShip 1/139, steadShip 1/51, suntShip 1/0, swiftShip 1/5, victoriaShip 0/19, visiShip 1/36",19,19,0,106],
|
||||
"ai_0": [1,28,12,0,0,5,8,"fe5ac1a4",53,"sargonShip 0/4, siracusaShip 1/12, skillfulShip 1/12, soloShip 1/0, suntShip 2/0, visiShip 1/0, yanShip 1/29",19,19,0,99]
|
||||
},
|
||||
{
|
||||
"round": 12,
|
||||
"h_0": [1,27,16,0,2,6,8,"3cdb76eb",388,"arcaneShip 0/2, indomShip 2/16, preciShip 2/141, sargonShip 1/179, skillfulShip 0/8, steadShip 0/51, suntShip 1/0, victoriaShip 0/19, visiShip 1/52",33,33,0,122],
|
||||
"ai_0": [1,28,12,0,0,6,8,"547fcf92",67,"sargonShip 0/8, siracusaShip 1/16, skillfulShip 1/18, soloShip 1/0, suntShip 2/0, yanShip 1/33",16,33,17,112]
|
||||
"h_0": [1,27,16,0,2,6,8,"750c100e",381,"arcaneShip 0/2, emptyShip 1/0, indomShip 0/16, preciShip 2/148, sargonShip 1/181, steadShip 0/51, suntShip 1/0, swiftShip 0/5, victoriaShip 0/19, visiShip 1/52",33,33,0,125],
|
||||
"ai_0": [1,28,12,0,0,6,8,"d3c53bfd",63,"sargonShip 0/8, siracusaShip 1/12, skillfulShip 1/18, soloShip 1/0, suntShip 2/0, visiShip 1/0, yanShip 1/33",18,33,15,112]
|
||||
},
|
||||
{
|
||||
"round": 13,
|
||||
"h_0": [1,27,16,0,0,6,8,"d0f5bd71",508,"arcaneShip 0/2, indomShip 1/16, preciShip 2/173, sargonShip 1/239, skillfulShip 0/8, steadShip 0/51, suntShip 1/0, swiftShip 1/12, victoriaShip 0/19, visiShip 1/68",50,50,0,138],
|
||||
"ai_0": [1,28,12,0,0,6,8,"d2ff3200",90,"sargonShip 0/8, siracusaShip 1/24, skillfulShip 1/24, soloShip 1/0, suntShip 2/0, victoriaShip 0/5, yanShip 1/42",32,50,18,125]
|
||||
"h_0": [1,27,16,4,0,6,8,"d90306b7",486,"arcaneShip 0/2, indomShip 0/16, maniShip 1/0, preciShip 2/166, sargonShip 1/209, steadShip 0/51, suntShip 1/0, swiftShip 0/5, victoriaShip 1/31, visiShip 1/80",50,50,0,140],
|
||||
"ai_0": [1,28,12,0,0,6,8,"5700d2b4",81,"miraShip 1/0, sargonShip 0/8, siracusaShip 1/20, skillfulShip 1/24, soloShip 1/0, suntShip 2/0, visiShip 1/0, yanShip 1/37",14,50,36,124]
|
||||
}
|
||||
],
|
||||
"players": {
|
||||
@@ -1691,9 +1691,9 @@
|
||||
"title": {"id":"comment_1","name":"卫戍之星","picId":"comment_icon_1","text":"对敌方领袖造成伤害最高(仅胜利时)"},
|
||||
"trophies": 4,
|
||||
"reward": 400,
|
||||
"stats": {"activatedLayers":534,"bossDamage":572474,"dmgDealt":4818483,"gold":154,"itemsEquipped":23,"kills":349,"leaks":0,"lpLost":0,"merges":4,"perfectRounds":13,"refreshes":23},
|
||||
"lineup": ["5_21_b@12,8[3_07_e_b+1_05_e_a]","5_22_a@11,7","4_20_b@11,8","5_02_a@10,5[5_01_e_a+6_01_e_a]","6_05_a@10,6[2_01_e_a+4_08_e_a]","6_06_a@10,8[1_01_e_a+3_04_e_a]","4_07_b@9,8[5_03_e_a+4_11_e_b]","4_23_b@9,9[4_04_e_a]"],
|
||||
"bonds": ["arcaneShip 0/2","indomShip 1/16","preciShip 2/173","sargonShip 1/239","skillfulShip 0/8","steadShip 0/51","suntShip 1/0","swiftShip 1/22","victoriaShip 0/19","visiShip 1/84"]
|
||||
"stats": {"activatedLayers":502,"bossDamage":596123,"dmgDealt":5126250,"gold":156,"itemsEquipped":23,"kills":364,"leaks":0,"lpLost":0,"merges":4,"perfectRounds":13,"refreshes":29},
|
||||
"lineup": ["6_05_a@12,8[2_01_e_a+4_08_e_a]","5_02_a@11,6[5_01_e_a+6_01_e_a]","6_08_a@11,7[3_09_e_a]","4_20_b@11,8[6_07_e_a+1_05_e_a]","6_11_a@10,6","5_21_b@10,8[3_04_e_a+6_09_e_a]","4_07_b@9,8[3_07_e_a+5_07_e_a]","4_23_b@9,9[4_04_e_b+4_11_e_a]"],
|
||||
"bonds": ["arcaneShip 0/2","indomShip 0/16","maniShip 1/0","preciShip 2/166","sargonShip 1/209","steadShip 0/51","suntShip 1/0","swiftShip 0/5","victoriaShip 1/31","visiShip 1/96"]
|
||||
},
|
||||
"ai_0": {
|
||||
"alive": true,
|
||||
@@ -1705,9 +1705,9 @@
|
||||
"title": {"id":"comment_3","name":"坚若磐石","picId":"comment_icon_3","text":"目标生命值损失最少"},
|
||||
"trophies": 4,
|
||||
"reward": 400,
|
||||
"stats": {"activatedLayers":113,"bossDamage":215026,"dmgDealt":2640057,"gold":137,"itemsEquipped":20,"kills":247,"leaks":46,"lpLost":0,"merges":6,"perfectRounds":8,"refreshes":13},
|
||||
"lineup": ["6_05_a@12,8[3_07_e_a+6_09_e_a]","2_04_b@11,7","1_07_b@11,8","6_11_a@10,6","3_03_b@10,8[3_11_e_b+2_04_e_a]","5_02_a@9,7[1_02_e_b]","1_08_b@9,8[4_06_e_a+1_05_e_a]","3_02_b@9,9[4_10_e_a+4_11_e_a]"],
|
||||
"bonds": ["maniShip 1/0","preciShip 1/0","sargonShip 1/8","siracusaShip 1/24","skillfulShip 1/30","soloShip 1/0","suntShip 2/0","victoriaShip 0/10","yanShip 1/51"]
|
||||
"stats": {"activatedLayers":105,"bossDamage":191377,"dmgDealt":2449031,"gold":135,"itemsEquipped":20,"kills":231,"leaks":62,"lpLost":0,"merges":6,"perfectRounds":8,"refreshes":12},
|
||||
"lineup": ["2_04_b@12,8[6_02_e_a+6_09_e_a]","5_02_a@11,7","6_05_a@11,8","5_20_a@10,5[1_05_e_a+1_02_e_b]","6_11_a@10,6","3_03_b@10,8[3_11_e_b+2_04_e_a]","1_08_b@9,8[4_06_e_a+4_11_e_a]","3_02_b@9,9[4_04_e_a]"],
|
||||
"bonds": ["maniShip 1/0","miraShip 1/0","preciShip 1/0","sargonShip 1/8","siracusaShip 1/28","skillfulShip 1/30","soloShip 1/0","suntShip 1/0","visiShip 1/0","yanShip 1/39"]
|
||||
}
|
||||
},
|
||||
"diy": {
|
||||
@@ -1730,7 +1730,7 @@
|
||||
[9,"",""],
|
||||
[10,"",""],
|
||||
[11,"",""],
|
||||
[12,"",""],
|
||||
[12,"","6_diy1_a→char_609_acguad"],
|
||||
[13,"",""]
|
||||
],
|
||||
"stock": {"chess_char_5_diy1_a":8,"chess_char_5_diy2_a":8,"chess_char_6_diy1_a":5,"chess_char_6_diy2_a":5}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
// test/match/feedback5-we2-bot-diy.test.js — 0.2.0 WE2 #13: AI 托管 of a human with 自选 picks buys the player's own slotted
|
||||
// pieces more readily (bot.js buyScore +DIY_PIECE_BONUS) and levels the 调度中心 one round earlier for the step that opens a
|
||||
// slotted tier (levelUp, diyOpensAt). Bots field no 自选 piece: nothing changes for them.
|
||||
// Run: node --test test/match/feedback5-we2-bot-diy.test.js
|
||||
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { buyScoreOf, levelUp, diyOpensAt, DIY_PIECE_BONUS } from '../../server/match/bot.js';
|
||||
import { diyGameData } from '../../server/match/player/diy.js';
|
||||
import { makeMatch, give, chessOfTier, checkInvariants } from './harness.js';
|
||||
|
||||
const T5A = 'chess_char_5_diy1_a';
|
||||
const PICK = { charId: 'char_112_siege', skillIndex: 2, uniEquipId: 'uniequip_002_siege' }; // 推进之王
|
||||
|
||||
function prep(seed = 911) {
|
||||
const seats = [
|
||||
{ seat: 0, playerId: 'p_0', name: 'P0', isBot: false, connected: true, diy: { [T5A]: PICK } },
|
||||
{ seat: 1, playerId: 'p_1', name: 'P1', isBot: false, connected: true },
|
||||
];
|
||||
const h = makeMatch({ mode: 'coop', difficulty: 'NORMAL', seats, seed }).start();
|
||||
h.toPrep(1);
|
||||
return { h, m: h.m, p0: h.ps('p_0'), p1: h.ps('p_1') };
|
||||
}
|
||||
|
||||
test('#13 buyScore: the player\'s own 自选 piece scores DIY_PIECE_BONUS more than the same record without the 自选 mark', () => {
|
||||
const { m, p0 } = prep();
|
||||
const rec = p0.gd.chess(T5A);
|
||||
assert.ok(rec.diyFor, 'a composed 自选 record');
|
||||
const withMark = buyScoreOf(m, p0, T5A);
|
||||
// the same player and record, only the 自选 mark taken off (a view of the match's data with that record)
|
||||
const plain = { ...rec };
|
||||
delete plain.diyFor;
|
||||
const saved = p0.gd;
|
||||
p0.gd = diyGameData(m.gd, new Map([[T5A, plain]]));
|
||||
let without;
|
||||
try { without = buyScoreOf(m, p0, T5A); } finally { p0.gd = saved; }
|
||||
assert.equal(withMark - without, DIY_PIECE_BONUS);
|
||||
checkInvariants(m);
|
||||
m.dispose();
|
||||
});
|
||||
|
||||
test('#13 levelUp: one round earlier for the step that opens a slotted tier (round 9: level 4 → 5 at a price above 2); a player without picks waits', () => {
|
||||
const { m, p0, p1 } = prep(912);
|
||||
m.round = 9;
|
||||
for (const ps of [p0, p1]) {
|
||||
ps.shop.level = 4;
|
||||
ps.shop.upgradePrice = 5;
|
||||
ps.funds = 6;
|
||||
const ids = chessOfTier(1).filter((id) => m.pool.has(id) && m.pool.left(id) > 0).slice(0, 6);
|
||||
for (const id of ids) give(m, ps, id, 'hand');
|
||||
}
|
||||
assert.ok(diyOpensAt(p0, 5) && !diyOpensAt(p1, 5) && !diyOpensAt(p0, 6));
|
||||
levelUp(m, p0);
|
||||
levelUp(m, p1);
|
||||
assert.equal(p0.shop.level, 5, 'p_0 (a tier-5 自选 piece) levels now');
|
||||
assert.equal(p1.shop.level, 4, 'p_1 waits for round 10 (LEVEL_TARGET)');
|
||||
m.dispose();
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
// test/match/feedback5-we2-diy-grants.test.js — 0.2.0 WE2 #9: an effect / reward / 机变 card that grants a player a random
|
||||
// operator draws that player's 自选 stock too (「自选干员放入后模拟中的补给池随机范围也将被相应扩大」; PRTS 新手教程 "调度中心随机资源
|
||||
// 的范围将被扩大"): effectsMeta ctx.rollChess / ctx.rollPool (player/diy.js diyStockEntries), choices.js 驰援 fallback —
|
||||
// the roll's own tier rules, bonds through the player's view; never another player's stock.
|
||||
// Run: node --test test/match/feedback5-we2-diy-grants.test.js
|
||||
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { makeCtx } from '../../server/match/effectsMeta.js';
|
||||
import { applyCard, tacticCard } from '../../server/match/choices.js';
|
||||
import { makeMatch, checkInvariants } from './harness.js';
|
||||
|
||||
const T5A = 'chess_char_5_diy1_a';
|
||||
const SIEGE = 'char_112_siege'; // 推进之王 (维多利亚)
|
||||
const PICK = { charId: SIEGE, skillIndex: 2, uniEquipId: 'uniequip_002_siege' };
|
||||
|
||||
/** A co-op match: p_0 slots 推进之王 at T5A, p_1 slots nothing; first prep. */
|
||||
function prep(seed = 911) {
|
||||
const seats = [
|
||||
{ seat: 0, playerId: 'p_0', name: 'P0', isBot: false, connected: true, diy: { [T5A]: PICK } },
|
||||
{ seat: 1, playerId: 'p_1', name: 'P1', isBot: false, connected: true },
|
||||
];
|
||||
const h = makeMatch({ mode: 'coop', difficulty: 'NORMAL', seats, seed }).start();
|
||||
h.toPrep(1);
|
||||
return { h, m: h.m, p0: h.ps('p_0'), p1: h.ps('p_1') };
|
||||
}
|
||||
/** Take every shared-pool copy of the chess `pred` admits (the draw then sees only what is left); m.restore() gives them back. */
|
||||
function drain(m, pred) {
|
||||
const back = (m.drained ??= []);
|
||||
for (const [id, e] of m.pool.entries) if (pred(id, e) && e.left > 0) back.push([id, m.pool.take(id, e.left)]);
|
||||
m.restore = () => { for (const [id, n] of back.splice(0)) m.pool.give(id, n); };
|
||||
}
|
||||
|
||||
test('#9 effect rolls (ctx.rollChess, any tier / bond filter) draw the player\'s 自选 stock — its own player only', () => {
|
||||
const { m, p0, p1 } = prep();
|
||||
assert.equal(p0.diyStock.left(T5A), 8);
|
||||
drain(m, (id, e) => e.tier === 5);
|
||||
const c0 = makeCtx(m, p0, { key: 'test' }, 'onTest');
|
||||
const c1 = makeCtx(m, p1, { key: 'test' }, 'onTest');
|
||||
assert.equal(c0.rollChess({ tier: 5 }), T5A, 'the only tier-5 copies left: her stock');
|
||||
assert.equal(c1.rollChess({ tier: 5 }), null, 'another player never draws it');
|
||||
// the 调度中心 level is not the shop's gate here: an effect's tier rule alone (p_0 is at level 1)
|
||||
assert.ok(p0.shop.level < 5);
|
||||
// a bond filter reads the operator: 推进之王 is 维多利亚
|
||||
drain(m, (id) => (m.gd.chess(id)?.bonds || []).includes('victoriaShip'));
|
||||
assert.equal(c0.rollChess({ bond: 'victoriaShip' }), T5A);
|
||||
assert.equal(c0.rollChess({ bond: 'yanShip', tier: 5 }), null, 'not a 炎 operator');
|
||||
// granting it takes one copy of her own stock
|
||||
assert.ok(c0.grantChess(T5A));
|
||||
assert.equal(p0.diyStock.left(T5A), 7);
|
||||
m.restore();
|
||||
checkInvariants(m);
|
||||
m.dispose();
|
||||
});
|
||||
|
||||
test('#9 reward pools (ctx.rollPool: a shared-pool chess pool) and the 维多利亚驰援 card draw it too', () => {
|
||||
const { m, p0, p1 } = prep(910);
|
||||
drain(m, (id, e) => e.tier === 5);
|
||||
const c0 = makeCtx(m, p0, { key: 'test' }, 'onTest');
|
||||
const c1 = makeCtx(m, p1, { key: 'test' }, 'onTest');
|
||||
assert.deepEqual(c0.rollPool('pool_chess_shop_5_reward'), { kind: 'chess', id: T5A, golden: false });
|
||||
assert.equal(c1.rollPool('pool_chess_shop_5_reward'), null);
|
||||
// 维多利亚驰援 (allybuff_select_7_7, single_special_choice_gain_bond_chess) with every 维多利亚 copy of the pool taken:
|
||||
// the card grants her 推进之王 from her own stock
|
||||
drain(m, (id) => (m.gd.chess(id)?.bonds || []).includes('victoriaShip'));
|
||||
const card = tacticCard(m.gd.choices.cards.tactic.find((c) => c.effectId === 'allybuff_select_7_7'));
|
||||
const owned = (ps) => [...ps.board.values(), ...ps.hand, ...ps.temp].filter((x) => x && x.kind === 'chess' && x.id === T5A).length;
|
||||
const before = p0.diyStock.left(T5A);
|
||||
applyCard(m, p0, card);
|
||||
assert.equal(owned(p0), 1, 'she got her 自选 piece');
|
||||
assert.equal(p0.diyStock.left(T5A), before - 1, 'from her stock');
|
||||
const had1 = [...p1.hand, ...p1.temp].filter(Boolean).length;
|
||||
applyCard(m, p1, card);
|
||||
assert.equal([...p1.hand, ...p1.temp].filter(Boolean).length, had1, 'p_1 has no 自选 piece to draw: nothing');
|
||||
m.restore();
|
||||
checkInvariants(m);
|
||||
m.dispose();
|
||||
});
|
||||
@@ -0,0 +1,181 @@
|
||||
// test/match/feedback5-we2-placement.test.js — 0.2.0 WE2: where the 自选 summon pieces may be placed in prep.
|
||||
// #22 望's 棋子 go on any tile class (PRTS 棋子 部署位置 "全部位", 备注 "游戏内召唤物信息与实际不符(显示为仅部署在近战位)").
|
||||
// #26 Mon3tr's 重构体 only inside her attack range on the ground (her talent "可以在攻击范围内的地面使用一个…重构体");
|
||||
// 凯尔希·思衡托's 战术锚点 only on a ranged tile outside her attack range (PRTS 战术锚点 特性 "仅可以部署在凯尔希·思衡托攻击范围外的
|
||||
// 远程位") — server (PlayerState._legal / summonRange / summonExcluded, board.js 'high'), the client mirror (gameLogic
|
||||
// canPlace) and the re-orientation lift.
|
||||
// Run: node --test test/match/feedback5-we2-placement.test.js
|
||||
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { ERR } from '../../shared/constants.js';
|
||||
import { FIELD, canPlace, placeClass, tileKey, parseKey } from '../../server/match/board.js';
|
||||
import { placementContext, canPlace as clientCanPlace, diyGetter } from '../../public/js/ui/gameLogic.js';
|
||||
import { DATA, makeMatch, checkInvariants } from './harness.js';
|
||||
|
||||
const T5A = 'chess_char_5_diy1_a';
|
||||
const WANG = 'char_2027_wang';
|
||||
const STONE = 'token_10064_wang_stone1';
|
||||
const MONSTR = 'char_4179_monstr';
|
||||
const PROSTS = 'token_10050_monstr_prosts';
|
||||
const KALTS2 = 'char_1052_kalts2';
|
||||
const ANCHOR = 'token_10068_kalts2_mtship';
|
||||
|
||||
/** A solo match whose human slots `picks`, in its first prep on `stage` (战场#01: 3 高台 tiles) with an empty board and hand. */
|
||||
function prep(picks, seed = 2101, stage = 'act2autochess_m01') {
|
||||
const seats = [{ seat: 0, playerId: 'p_0', name: 'P0', isBot: false, connected: true, diy: picks }];
|
||||
const h = makeMatch({ mode: 'solo', difficulty: 'NORMAL', seats, seed }).start();
|
||||
h.toPrep(1);
|
||||
h.setStage(stage);
|
||||
const ps = h.ps('p_0');
|
||||
for (const p of [...ps.board.values(), ...ps.hand.filter(Boolean)]) if (p.kind === 'chess') ps.returnCopies(p);
|
||||
ps.board.clear();
|
||||
ps.hand.fill(null);
|
||||
ps.recompute();
|
||||
ps.funds = 100;
|
||||
return { h, m: h.m, ps };
|
||||
}
|
||||
|
||||
/** Free tiles of the player's deploy map of class `cls` ('melee' | 'ranged'), reading order. */
|
||||
function tilesOf(ps, cls) {
|
||||
const map = ps.deployMap();
|
||||
const out = [];
|
||||
for (let r = FIELD.r1; r >= FIELD.r0; r--) for (let c = FIELD.c0; c <= FIELD.c1; c++) {
|
||||
if (!ps.board.has(tileKey(r, c)) && map.get(tileKey(r, c)) === cls) out.push([r, c]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Gain slot `slotId` (its 自选 operator) and place it on the first free tile of its class; returns the piece. */
|
||||
function placeDiy(m, ps, slotId, at = null) {
|
||||
const piece = ps.acquireChess(slotId, { source: 'buy' });
|
||||
assert.ok(piece, `${slotId} gained`);
|
||||
const map = ps.deployMap();
|
||||
const pos = placeClass(ps, ps.gd.chess(slotId));
|
||||
let tile = at;
|
||||
for (let r = FIELD.r1; r >= FIELD.r0 && !tile; r--) for (let c = FIELD.c0; c <= FIELD.c1 && !tile; c++) {
|
||||
if (!ps.board.has(tileKey(r, c)) && canPlace(map, pos, r, c)) tile = [r, c];
|
||||
}
|
||||
assert.deepEqual(m.handle('p_0', { t: 'g.move', uid: piece.uid, to: { area: 'board', row: tile[0], col: tile[1] } }), { ok: true }, `${slotId} placed`);
|
||||
return piece;
|
||||
}
|
||||
|
||||
test('#22 望\'s 棋子: a hand piece goes on a 高台 (ranged tile) as well as on the ground — PRTS 棋子 全部位', () => {
|
||||
const { m, ps } = prep({ [T5A]: { charId: WANG, skillIndex: 0 } });
|
||||
assert.equal(ps.gd.token(STONE).position, 'ALL');
|
||||
const wang = placeDiy(m, ps, T5A);
|
||||
const stack = ps.hand.find((p) => p && p.kind === 'token' && p.id === STONE && p.ownerUid === wang.uid);
|
||||
assert.ok(stack && stack.count === 6, 'her 6 棋子 joined the hand');
|
||||
const high = tilesOf(ps, 'ranged');
|
||||
const ground = tilesOf(ps, 'melee');
|
||||
assert.ok(high.length && ground.length, 'the stage has both tile classes');
|
||||
assert.deepEqual(m.handle('p_0', { t: 'g.move', uid: stack.uid, to: { area: 'board', row: high[0][0], col: high[0][1] } }), { ok: true }, 'onto a 高台');
|
||||
const left = ps.hand.find((p) => p && p.kind === 'token' && p.id === STONE);
|
||||
assert.deepEqual(m.handle('p_0', { t: 'g.move', uid: left.uid, to: { area: 'board', row: ground[0][0], col: ground[0][1] } }), { ok: true }, 'onto the ground');
|
||||
// still refused off the deploy map (a forbidden tile of the field)
|
||||
const map = ps.deployMap();
|
||||
let off = null;
|
||||
for (let r = FIELD.r1; r >= FIELD.r0 && !off; r--) for (let c = FIELD.c0; c <= FIELD.c1 && !off; c++) if (!map.get(tileKey(r, c)) && !ps.board.has(tileKey(r, c))) off = [r, c];
|
||||
if (off) {
|
||||
const again = ps.hand.find((p) => p && p.kind === 'token' && p.id === STONE);
|
||||
assert.deepEqual(m.handle('p_0', { t: 'g.move', uid: again.uid, to: { area: 'board', row: off[0], col: off[1] } }), { error: ERR.BAD_TILE }, 'not a deployable tile');
|
||||
}
|
||||
checkInvariants(m);
|
||||
m.dispose();
|
||||
});
|
||||
|
||||
/** The owner's attack-range keys on the board (the server's own reading). */
|
||||
const rangeOf = (ps, owner) => ps._ownerRangeOf({ kind: 'token', ownerUid: owner.uid, id: '' });
|
||||
/** The summon stack of `owner` in the hand. */
|
||||
const stackOf = (ps, owner, tokenId) => ps.hand.find((p) => p && p.kind === 'token' && p.id === tokenId && p.ownerUid === owner.uid);
|
||||
/** The client's legality of dropping `uid` on (r, c), from the player's private view. */
|
||||
function clientOk(m, ps, uid, r, c) {
|
||||
const priv = ps.privateView();
|
||||
const getChess = diyGetter((id) => DATA.chess[id] ?? null, priv, DATA);
|
||||
const ctx = placementContext({
|
||||
priv, stage: m.stage, editable: true, getChess, backups: DATA.backups,
|
||||
getToken: (id) => DATA.tokens[id] ?? DATA.backups.tokens[id] ?? null, getItem: (id) => DATA.items[id] ?? null,
|
||||
});
|
||||
return clientCanPlace(ctx, uid, { area: 'board', row: r, col: c }).ok;
|
||||
}
|
||||
/** Every free tile of the field: [r, c, serverLegal]. */
|
||||
function freeTiles(ps, piece) {
|
||||
const out = [];
|
||||
for (let r = FIELD.r1; r >= FIELD.r0; r--) for (let c = FIELD.c0; c <= FIELD.c1; c++) if (!ps.board.has(tileKey(r, c))) out.push([r, c, ps._legal(piece, r, c)]);
|
||||
return out;
|
||||
}
|
||||
|
||||
test('#26 Mon3tr\'s 重构体: only on a ground tile of her attack range — data ownerRange from her talent; server and client agree', () => {
|
||||
assert.equal(DATA.backups.tokens[PROSTS].ownerRange, true);
|
||||
const { m, ps } = prep({ [T5A]: { charId: MONSTR, skillIndex: 0 } });
|
||||
const mon = placeDiy(m, ps, T5A, [10, 5]);
|
||||
const stack = stackOf(ps, mon, PROSTS);
|
||||
assert.ok(stack, 'her 重构体 joined the hand');
|
||||
const range = rangeOf(ps, mon);
|
||||
const map = ps.deployMap();
|
||||
const tiles = freeTiles(ps, stack);
|
||||
let inside = 0, outside = 0;
|
||||
for (const [r, c, legal] of tiles) {
|
||||
const k = tileKey(r, c);
|
||||
assert.equal(legal, range.has(k) && map.get(k) === 'melee', `server ${k}`);
|
||||
assert.equal(clientOk(m, ps, stack.uid, r, c), legal, `client ${k}`);
|
||||
if (legal) inside++; else if (map.get(k) === 'melee') outside++;
|
||||
}
|
||||
assert.ok(inside > 0 && outside > 0, `${inside} legal ground tiles in her range, ${outside} outside`);
|
||||
const [r0, c0] = tiles.find((t) => t[2]);
|
||||
assert.deepEqual(m.handle('p_0', { t: 'g.move', uid: stack.uid, to: { area: 'board', row: r0, col: c0 } }), { ok: true });
|
||||
const off = tiles.find(([r, c]) => map.get(tileKey(r, c)) === 'melee' && !range.has(tileKey(r, c)));
|
||||
const again = ps.board.get(tileKey(r0, c0));
|
||||
assert.deepEqual(m.handle('p_0', { t: 'g.move', uid: again.uid, to: { area: 'board', row: off[0], col: off[1] } }), { error: ERR.BAD_TILE }, 'outside her range');
|
||||
checkInvariants(m);
|
||||
m.dispose();
|
||||
});
|
||||
|
||||
test('#26 凯尔希·思衡托\'s 战术锚点 (S3): only on a ranged tile outside her attack range; re-oriented so that her range covers it, it goes back to the hand', () => {
|
||||
const rec = DATA.backups.tokens[ANCHOR];
|
||||
assert.deepEqual([rec.position, rec.ownerRangeOutside, rec.rangedTilesOnly, rec.ownerRange], ['RANGED', true, true, false]);
|
||||
// act1 m03: 高台 at (10, 3), (10, 4), (11, 7), (11, 8); she stands on the ground at (12, 5), facing RIGHT — her y-6 takes
|
||||
// (11, 7) in and leaves the other three out
|
||||
const { h, m, ps } = prep({ [T5A]: { charId: KALTS2, skillIndex: 2 } }, 2102, 'act1autochess_m03');
|
||||
const map = ps.deployMap();
|
||||
const highs = [...map].filter(([, v]) => v === 'ranged').map(([k]) => parseKey(k));
|
||||
assert.ok(highs.length >= 2, 'the stage has 高台 tiles');
|
||||
const kal = placeDiy(m, ps, T5A, [12, 5]);
|
||||
const stack = stackOf(ps, kal, ANCHOR);
|
||||
assert.ok(stack && (stack.count || 1) === 1, 'one 战术锚点 in the hand (S3)');
|
||||
const range = rangeOf(ps, kal);
|
||||
const tiles = freeTiles(ps, stack);
|
||||
let legalN = 0;
|
||||
for (const [r, c, legal] of tiles) {
|
||||
const k = tileKey(r, c);
|
||||
assert.equal(legal, map.get(k) === 'ranged' && !range.has(k), `server ${k}`);
|
||||
assert.equal(clientOk(m, ps, stack.uid, r, c), legal, `client ${k}`);
|
||||
if (legal) legalN++;
|
||||
}
|
||||
const insideHigh = highs.find(([r, c]) => range.has(tileKey(r, c)) && !ps.board.has(tileKey(r, c)));
|
||||
const ground = tiles.find(([r, c]) => map.get(tileKey(r, c)) === 'melee' && !range.has(tileKey(r, c)));
|
||||
assert.ok(ground, 'a ground tile outside her range');
|
||||
assert.deepEqual(m.handle('p_0', { t: 'g.move', uid: stack.uid, to: { area: 'board', row: ground[0], col: ground[1] } }), { error: ERR.BAD_TILE }, 'not on the ground');
|
||||
if (insideHigh) assert.deepEqual(m.handle('p_0', { t: 'g.move', uid: stack.uid, to: { area: 'board', row: insideHigh[0], col: insideHigh[1] } }), { error: ERR.BAD_TILE }, 'not inside her range');
|
||||
assert.ok(legalN > 0, 'a 高台 outside her range');
|
||||
// placed on a 高台 outside her range; then she turns so that her range takes that tile in: the anchor goes back
|
||||
for (const dir of ['RIGHT', 'UP', 'DOWN', 'LEFT']) {
|
||||
const atDir = { key: tileKey(12, 5), piece: kal, dir };
|
||||
const keys = ps._ownerRangeOf({ kind: 'token', ownerUid: kal.uid, id: '' }, atDir);
|
||||
const spot = tiles.find(([r, c, legal]) => legal && keys.has(tileKey(r, c)));
|
||||
if (!spot) continue;
|
||||
assert.deepEqual(m.handle('p_0', { t: 'g.move', uid: stack.uid, to: { area: 'board', row: spot[0], col: spot[1] } }), { ok: true }, 'onto a free 高台 outside her range');
|
||||
const placed = ps.board.get(tileKey(spot[0], spot[1]));
|
||||
assert.equal(placed.id, ANCHOR);
|
||||
const toasts = h.sent.length;
|
||||
assert.deepEqual(m.handle('p_0', { t: 'g.move', uid: kal.uid, to: { area: 'board', row: 12, col: 5 }, dir }), { ok: true }, `re-oriented ${dir}`);
|
||||
assert.ok(!ps.board.has(tileKey(spot[0], spot[1])), 'the anchor left the board');
|
||||
assert.ok(stackOf(ps, kal, ANCHOR), 'back on its stack');
|
||||
const said = h.sent.slice(toasts).filter(([, msg]) => msg.t === 'm.toast').map(([, msg]) => msg.msgid || msg.text);
|
||||
assert.ok(said.some((x) => String(x).includes('只能部署在召唤者攻击范围外')), `the toast: ${said.join(' / ')}`);
|
||||
checkInvariants(m);
|
||||
m.dispose();
|
||||
return;
|
||||
}
|
||||
assert.fail('no facing of hers covers a legal 高台');
|
||||
});
|
||||
@@ -12,7 +12,7 @@ after(async () => { if (srv) await srv.close(); });
|
||||
test('/sim/ serves the simulation modules read-only; nothing else of server/ is reachable', async () => {
|
||||
srv = await startServer({ port: 0, host: '127.0.0.1', quiet: true });
|
||||
const get = (p, headers = {}) => fetch(srv.url + p, { redirect: 'manual', headers });
|
||||
for (const p of ['/sim/spec.js', '/sim/Battle.js', '/sim/simdata.js', '/sim/constants.js', '/sim/content/index.js', '/sim/content/kits/tier1.js', '/sim/content/support/index.js']) {
|
||||
for (const p of ['/sim/spec.js', '/sim/Battle.js', '/sim/simdata.js', '/sim/constants.js', '/sim/content/index.js', '/sim/content/kits/index.js', '/sim/content/support/index.js']) {
|
||||
const r = await get(p);
|
||||
assert.equal(r.status, 200, p);
|
||||
assert.match(r.headers.get('content-type'), /^text\/javascript/, p);
|
||||
|
||||
@@ -12,7 +12,7 @@ import { COLS } from '../../server/sim/constants.js';
|
||||
import { makeBattle, chessRec, enemyRec, checkInvariants } from '../helpers/battleHarness.js';
|
||||
import { findSummonTile } from '../../server/sim/content/tokens.js';
|
||||
import { frontTile, sideTiles, alliesAround, N4 } from '../../server/sim/content/support/index.js';
|
||||
import { freeTileAround } from '../../server/sim/content/kits/tier1.js';
|
||||
import { freeTileAround } from '../../server/sim/content/kits/shared/tier1.js';
|
||||
import { DIRS as PROTOCOL_DIRS } from '../../shared/protocol.js';
|
||||
|
||||
const K = (r, c) => r * COLS + c;
|
||||
|
||||
@@ -22,7 +22,7 @@ import { makeBattle, chessRec, enemyRec, checkInvariants } from '../helpers/batt
|
||||
import { getDefaultSource, hasGeneratedData } from '../../server/sim/simdata.js';
|
||||
import { effectiveProfile } from '../../server/sim/ai.js';
|
||||
import { PUSH_TILES, PUSH_TILES_EFFECT, PULL_STOP_RADIUS, ASPD_MIN, COLS } from '../../server/sim/constants.js';
|
||||
import { alliesInGridOf } from '../../server/sim/content/kits/tier1.js';
|
||||
import { alliesInGridOf } from '../../server/sim/content/kits/shared/tier1.js';
|
||||
import { Unit } from '../../server/sim/units.js';
|
||||
import { spawnYanyou, TOKEN_IDS } from '../../server/sim/content/tokens.js';
|
||||
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
// test/ui/feedback5-we2-diybanned.test.js — 0.2.0 WE2 #11: the player's own 自选 pieces left out of the shop because every
|
||||
// bond of the operator is switched off this match (m.private.diyBanned) are named under 本局禁用干员 — the briefing, the
|
||||
// strategy draft's 本局信息 dialog (ui/matchInfo.js) and the in-game 本局信息 tab (ui/enemyDrawer.js).
|
||||
// Run: node --test test/ui/feedback5-we2-diybanned.test.js
|
||||
|
||||
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 G = await import('../../public/js/ui/gameLogic.js');
|
||||
const { matchInfoModel, DiyBannedLine } = await import('../../public/js/ui/matchInfo.js');
|
||||
const { data } = await import('../../public/js/data.js');
|
||||
const { setLang, addMessages } = await import('../../shared/i18n.js');
|
||||
const { DATA } = await import('../match/harness.js');
|
||||
await data.loadAll('chess', 'bonds', 'assets', 'backups');
|
||||
|
||||
const T5A = 'chess_char_5_diy1_a';
|
||||
const T6A = 'chess_char_6_diy1_a';
|
||||
const SIEGE = 'char_112_siege';
|
||||
const LING = 'char_2023_ling';
|
||||
const D = { chess: DATA.chess, backups: DATA.backups };
|
||||
const getChess = (id) => (Object.hasOwn(DATA.chess, id) ? DATA.chess[id] : null);
|
||||
const priv = {
|
||||
diy: { [T5A]: { charId: SIEGE, skillIndex: 2, uniEquipId: 'uniequip_002_siege' }, [T6A]: { charId: LING, skillIndex: 0, uniEquipId: null } },
|
||||
diyBanned: [T6A],
|
||||
};
|
||||
const textOf = (v) => (Array.isArray(v) ? v.map(textOf).join('') : v == null || typeof v === 'boolean' ? '' : typeof v === 'object' ? textOf(v.props?.children) : String(v));
|
||||
|
||||
test('#11 diyBannedPieces: the banned slots with their operator\'s name, slot order; none without picks', () => {
|
||||
assert.deepEqual(G.diyBannedPieces(priv, getChess, D), [{ slotId: T6A, charId: LING, name: DATA.backups.units[LING].name }]);
|
||||
assert.deepEqual(G.diyBannedPieces({ diy: priv.diy, diyBanned: [] }, getChess, D), []);
|
||||
assert.deepEqual(G.diyBannedPieces({ diyBanned: [T6A] }, getChess, D), [], 'no pick for the slot: nothing');
|
||||
assert.deepEqual(G.diyBannedPieces(null, getChess, D), []);
|
||||
});
|
||||
|
||||
test('#11 the match-info model and its line: the own banned 自选 piece is named; nothing without priv / diyBanned', () => {
|
||||
const pub = { bannedChess: [], drawnDisabledBonds: [], modeId: 'mode_multi_hard' };
|
||||
const model = matchInfoModel(pub, { bonds: [], chess: getChess, priv, diyData: D });
|
||||
assert.deepEqual(model.diyBanned.map((x) => x.slotId), [T6A]);
|
||||
setLang('zh');
|
||||
const line = DiyBannedLine({ model });
|
||||
assert.ok(line, 'a line');
|
||||
const txt = textOf(line);
|
||||
assert.ok(txt.includes('自选') && txt.includes(DATA.backups.units[LING].name) && txt.includes('不会出现在你的商店'), txt);
|
||||
assert.equal(DiyBannedLine({ model: matchInfoModel(pub, { bonds: [], chess: getChess }) }), null, 'no priv: no line');
|
||||
assert.equal(DiyBannedLine({ model: matchInfoModel(pub, { bonds: [], chess: getChess, priv: { ...priv, diyBanned: [] }, diyData: D }) }), null);
|
||||
// English: the msgid is in public/i18n/en.json
|
||||
const en = JSON.parse(readFileSync(path.join(ROOT, 'public/i18n/en.json'), 'utf8'));
|
||||
const id = '{names}的盟约本局全部禁用,不会出现在你的商店';
|
||||
assert.ok(en[id] && en[id].includes('{names}'), 'translated with its placeholder');
|
||||
addMessages('en', { [id]: en[id], 自选: en['自选'] });
|
||||
setLang('en');
|
||||
try {
|
||||
const t2 = textOf(DiyBannedLine({ model }));
|
||||
assert.ok(t2.includes('Custom') && t2.includes('not in your shop'), t2);
|
||||
} finally { setLang('zh'); }
|
||||
});
|
||||
@@ -193,17 +193,18 @@ test('the briefing, the strategy draft and the in-game 本局信息 tab all read
|
||||
const brief = read('public/js/screens/briefing.js');
|
||||
assert.match(brief, /import \{ MatchInfo, matchInfoModel \} from '\.\.\/ui\/matchInfo\.js';/);
|
||||
assert.match(brief, /<section class="brief__right">\s*<\$\{MatchInfo\} model=\$\{info\} \/>\s*<\/section>/);
|
||||
assert.match(brief, /const info = matchInfoModel\(pub, \{ bonds: gd\.list\('bonds'\), chess: gd\.chess, mode \}\);/);
|
||||
// (+ the viewer's 自选 pieces out of the shop: priv / diyData, 0.2.0 WE2)
|
||||
assert.match(brief, /const info = matchInfoModel\(pub, \{ bonds: gd\.list\('bonds'\), chess: gd\.chess, mode, priv, diyData: \{ chess: data\.get\('chess'\), backups: data\.get\('backups'\) \} \}\);/);
|
||||
assert.doesNotMatch(brief, /brief-legend|brief-banned|function BondRow|bannedPerBond|disabledBondSets/, 'no own copy of the blocks');
|
||||
const draft = read('public/js/screens/bandDraft.js');
|
||||
assert.match(draft, /import \{ MatchInfoDialog, matchInfoModel \} from '\.\.\/ui\/matchInfo\.js';/);
|
||||
assert.match(draft, /const info = infoOpen \? matchInfoModel\(pub, \{ bonds: gd\.list\('bonds'\), chess: gd\.chess, mode \}\) : null;/);
|
||||
assert.match(draft, /const info = infoOpen \? matchInfoModel\(pub, \{ bonds: gd\.list\('bonds'\), chess: gd\.chess, mode, priv, diyData: \{ chess: data\.get\('chess'\), backups: data\.get\('backups'\) \} \}\) : null;/);
|
||||
assert.match(draft, /data-testid="match-info-open"[\s\S]*?onClick=\$\{\(\) => setInfoOpen\(true\)\}>查看禁用盟约与干员</);
|
||||
assert.match(draft, /<\$\{MatchInfoDialog\} open=\$\{infoOpen\} onClose=\$\{\(\) => setInfoOpen\(false\)\} model=\$\{info\}/);
|
||||
// a turn change or my pick closes it; the draft's end unmounts the screen
|
||||
assert.match(draft, /const turnKey = `\$\{draft\.turnPid \|\| ''\}\|\$\{myPick \|\| ''\}`;\n\s*useEffect\(\(\) => \{ setInfoOpen\(false\); \}, \[turnKey\]\);/);
|
||||
const drawer = read('public/js/ui/enemyDrawer.js');
|
||||
assert.match(drawer, /import \{ matchInfoModel \} from '\.\/matchInfo\.js';/);
|
||||
assert.match(drawer, /import \{ matchInfoModel, DiyBannedLine \} from '\.\/matchInfo\.js';/);
|
||||
assert.match(drawer, /matchInfoModel\(pub, \{\s*bonds: data\.list\('bonds'\), chess: \(id\) => data\.lookup\('chess', id\), mode: data\.get\('config'\)\?\.modes\?\.\[pub\?\.modeId\],/);
|
||||
assert.doesNotMatch(drawer, /bannedPerBond|disabledBondSets/, 'the drawer derives nothing on its own');
|
||||
});
|
||||
Reference in new issue
Block a user