Files
ark-stronghold-protocol/test/ui/playtest6_summons.test.js
T
sganggs 6464e4b46a Playtest #6 fixes: summons placed by hand, boss pool stall, bounties, 联防 leak counter, distinct offers, 机变 card text, phone buy tap, skill triggers, blocking, push force, 孤立, enemies, element bar
- #1 #2 skill / device summons (赫默's drone, 巫恋's doll, 凯瑟琳's devices) are hand cards placed by the player;
  placed summons deploy once at battle start, then a skill summon re-appears at its tile on each cast (the user's call)
- #3 叙拉古 隐匿 ends on time (32 + 0.4 × layers s); 忍冬 S3 迷彩 lasts until her next cast (official), kept apart from 隐匿
- #4 multi-round bounty cards last two battles (the user's call; MULTI_ROUND_BOUNTY_BATTLES), blue / red card text
- #5 the shared boss pool's float dust (≈3.6e-12 HP) stalled Final Assault leaders at 0 %; < 1 HP now clears the pool,
  and the team LP reaching 0 first is a defeat (PRTS)
- #6 #10 机变 cards show their official effect text; an invisible 详情 button no longer swallows the confirming tap on phones
- #7 live ×N counter of a leaker's enemies still on the 联防 field (LP loss stays capped at 10)
- #8 #15 #16 skill triggers per 技能策略: skill-range triggers, injured-ally heals (古米, 塞雷娅), tank take-damage
  triggers with a 3 s cooldown; 新约能天使 S2 shields the highest-aggro operator in range
- #9 official per-enemy model scales (威龙)
- #11 element bar under the unit's HP bar (white = 元素值 left, element-coloured refill after a burst), floor rounding
- #12 枯朽战车 kit per PRTS; every ranged enemy reaches its target's 0.25 collider
- #13 恐惧 wander (萨科塔之翼 at half HP, operator fears make enemies flee) with re-planning after pushes
- #14 push / pull distance by 力度 − 重量 (PRTS table); #17 every blocker attacks what it blocks; ASPD floor 20
- #18 炎佑 is 孤立: no heals or ally buffs
- #19 every free pick-one chess offer (promotion reward, 信标, 寻呼模块) shows different operators
- DESIGN §20 and the normative lines, SIM / META / DATA / PLAYING updated; 2747 unit tests and all 27 browser suites
  green; dev recordings regenerated
2026-10-01 19:08:23 +08:00

118 lines
8.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// User playtest #6 items 1 and 2, the UI side: a summon piece in the hand / on the board says how it takes the field
// (ui/detailPanel.js summonDeployHint / TokenDetail) — 赫默's 医疗探机 once at the battle start, then each time her skill
// fires; 凯瑟琳's 支援装置 with the board — and shows what it does (the device's talent 定向支援信号, the doll's token
// skill), with the numbers of the owner's own variant (a golden owner's summon is stronger: resolveDetail finds the
// owner by `ownerUid`). One switch, shared/constants.js SKILL_SUMMON_START_DEPLOY (the PRTS start-of-battle deploy,
// settled by the user after playtest #6), drives the sim and the hint; the guide (docs/PLAYING.md §4) and docs/SIM.md
// must say the same. The rules themselves:
// test/match/playtest6_summons.test.js (prep) and test/content/playtest6_summons.test.js (battle).
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 { TokenDetail, summonDeployHint, resolveDetail, tokenVariantFor } = await import('../../public/js/ui/detailPanel.js');
const { indexPieces } = await import('../../public/js/ui/gameLogic.js');
const { SKILL_SUMMON_START_DEPLOY } = await import('../../shared/constants.js');
const simTokens = await import('../../server/sim/content/tokens.js');
const { data } = await import('../../public/js/data.js');
const TOKENS = JSON.parse(readFileSync(path.join(ROOT, 'data', 'tokens.json'), 'utf8'));
const textOf = (v) => {
if (v == null || typeof v === 'boolean') return '';
if (typeof v === 'string' || typeof v === 'number') return String(v);
if (Array.isArray(v)) return v.map(textOf).join('');
if (typeof v === 'object' && typeof v.type === 'function' && ['Section', 'Stat', 'LiveTag'].includes(v.type.name)) return textOf(v.type(v.props));
return typeof v === 'object' ? textOf(v.props?.children) : '';
};
test('summonDeployHint: skill summons at the start and with the skill, talent summons with the board, other tokens say nothing', () => {
assert.equal(SKILL_SUMMON_START_DEPLOY, true, 'the user\'s answer after playtest #6: the PRTS start deploy');
assert.match(summonDeployHint(TOKENS.token_10000_silent_healrb), /^作战开始时在摆放的位置部署一次.*发动技能时/);
assert.match(summonDeployHint(TOKENS.token_10006_vodfox_doll), /^作战开始时在摆放的位置部署一次.*发动技能时/);
for (const id of ['token_10041_cathy_catsld', 'token_10028_vigil_wolf', 'token_10017_skadi2_dedant', 'token_10030_mlyss_wtrman']) {
assert.match(summonDeployHint(TOKENS[id]), /作战开始时/, id);
}
assert.equal(summonDeployHint(TOKENS.token_10056_angel2_target), null, '投递坐标 is no hand piece');
assert.equal(summonDeployHint(TOKENS.enemy_9012_acloon), null);
});
test('one switch for the start-of-battle deploy: the sim and the hint read shared/constants.js SKILL_SUMMON_START_DEPLOY', () => {
assert.equal(simTokens.SKILL_SUMMON_START_DEPLOY, SKILL_SUMMON_START_DEPLOY, 'the sim re-exports the shared switch');
const drone = TOKENS.token_10000_silent_healrb;
assert.equal(summonDeployHint(drone), summonDeployHint(drone, SKILL_SUMMON_START_DEPLOY), 'the hint defaults to it');
// off (the playtest #4 reading): only with the skill; on (PRTS, the default): once at the start, then with each skill
assert.match(summonDeployHint(drone, false), /^所属干员发动技能时才在摆放的位置出现/);
assert.match(summonDeployHint(drone, true), /^作战开始时在摆放的位置部署一次,之后所属干员每次发动技能时再次出现/);
assert.match(summonDeployHint(TOKENS.token_10006_vodfox_doll, true), /作战开始时.*部署一次/);
// talent summons deploy with the board either way
assert.equal(summonDeployHint(TOKENS.token_10041_cathy_catsld, true), summonDeployHint(TOKENS.token_10041_cathy_catsld, false));
});
test('the guide and SIM.md state the start-of-battle rule the switch selects (flip it ⇒ update both)', () => {
const playing = readFileSync(path.join(ROOT, 'docs', 'PLAYING.md'), 'utf8');
const sim = readFileSync(path.join(ROOT, 'docs', 'SIM.md'), 'utf8');
if (SKILL_SUMMON_START_DEPLOY) {
assert.match(playing, /医疗探机、诅咒娃娃[^\n]*开战时[^\n]*部署一次/, 'PLAYING.md §4: the drone / doll also deploy once at the start');
assert.match(sim, /deploys once at the battle start/, 'SIM.md token pieces: the PRTS start deploy');
} else {
assert.match(playing, /医疗探机、诅咒娃娃要等所属干员发动技能时才在摆放的位置出现/, 'PLAYING.md §4: only with the skill');
assert.match(sim, /a skill's summon[^.]*waits on its tile and takes the field there\s+each time the skill gives one/, 'SIM.md token pieces');
}
// the exception for unplaced summons is stated for the summons it holds for (test/content/playtest6_summons)
assert.match(playing, /狼群、流形(战术家的援军)没有摆放时会在战术点出现/);
assert.ok(!/没有摆放的召唤物不会出现/.test(playing), 'no blanket "unplaced summons never appear"');
});
test('the summon card: 凯瑟琳\'s device shows the hint and its shield talent; 巫恋\'s doll its token skill', async () => {
await data.loadAll('tokens', 'assets');
const dev = textOf(TokenDetail({ token: TOKENS.token_10041_cathy_catsld, piece: { kind: 'token', id: 'token_10041_cathy_catsld', count: 2 } }));
assert.match(dev, /作战开始时在摆放的位置部署/);
assert.match(dev, /定向支援信号/);
assert.match(dev, /屏障/);
const drone = textOf(TokenDetail({ token: TOKENS.token_10000_silent_healrb, piece: { kind: 'token', id: 'token_10000_silent_healrb', count: 1 } }));
assert.match(drone, /作战开始时在摆放的位置部署一次,之后所属干员每次发动技能时再次出现/);
assert.ok(!/自我销毁/.test(drone), 'the withdraw skill is not listed');
const doll = textOf(TokenDetail({ token: TOKENS.token_10006_vodfox_doll, piece: { kind: 'token', id: 'token_10006_vodfox_doll', count: 1 } }));
assert.match(doll, /攻击力和防御力-25%/);
// a summon on the battlefield (no prep piece) shows no prep hint
assert.ok(!/摆放的位置/.test(textOf(TokenDetail({ token: TOKENS.token_10041_cathy_catsld, piece: null }))));
});
test('a golden owner\'s summon card shows the golden variant: 精锐 巫恋\'s doll −30%, 精锐 赫默\'s drone ATK 114', async () => {
await data.loadAll('tokens', 'assets');
assert.equal(tokenVariantFor(TOKENS.token_10000_silent_healrb, 'chess_char_2_02_b').stats.atk, 114);
assert.equal(tokenVariantFor(TOKENS.token_10000_silent_healrb, 'chess_char_2_02_a').stats.atk, 85);
assert.equal(tokenVariantFor(TOKENS.token_10000_silent_healrb, null).stats.atk, 85, 'unknown owner: the first variant');
// the private view: golden 巫恋 on the board, her doll card in the hand (resolveDetail reads the owner by ownerUid)
const priv = {
board: [{ uid: 7, kind: 'chess', id: 'chess_char_3_15_b', row: 10, col: 4 }, { uid: 9, kind: 'chess', id: 'chess_char_2_02_b', row: 10, col: 5 }],
hand: [{ uid: 8, kind: 'token', id: 'token_10006_vodfox_doll', ownerUid: 7, count: 1 }, { uid: 10, kind: 'token', id: 'token_10000_silent_healrb', ownerUid: 9, count: 1 }],
temp: [],
};
const pieces = indexPieces(priv);
const doll = resolveDetail({ kind: 'piece', uid: 8 }, pieces);
assert.equal(doll.ownerId, 'chess_char_3_15_b');
const dollText = textOf(TokenDetail(doll));
assert.match(dollText, /攻击力和防御力-30%/);
assert.ok(!/-25%/.test(dollText));
const drone = resolveDetail({ kind: 'piece', uid: 10 }, pieces);
assert.match(textOf(TokenDetail(drone)), /114/);
// in battle the own summon unit carries its piece uid: the same owner variant
const unit = resolveDetail({ kind: 'unit', unit: { id: 3, side: 'ally', defId: 'token_10006_vodfox_doll', uid: 8 } }, pieces);
assert.equal(unit.type, 'token');
assert.equal(unit.ownerId, 'chess_char_3_15_b');
});