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
This commit is contained in:
sganggs committed 2026-10-01 19:08:23 +08:00
1 parent e6628f0cfe
commit 6464e4b46a
138 files changed
+7946 -1379

No files matched your search

+25 -18
View File
@@ -82,14 +82,14 @@ test('1_01 隐现 elite: +self_ammo after `duration` s on field; a random other
done(h);
});
test('1_02 角峰: 抗寒体质 HP/DEF/RES ×(1+x); 雪原卫士 RES +flat (normal and elite)', () => {
test('1_02 角峰: 抗寒体质 (重装 TAKE_DAMAGE) HP/DEF/RES ×(1+x); 雪原卫士 RES +flat (normal and elite)', () => {
for (const id of ['chess_char_1_02_a', 'chess_char_1_02_b']) {
const bb = bbOf(id), t = tal(id);
const h = run({ defs: { enemies: { e: dummy('e') } }, units: [{ chessId: id, row: 9, col: 5, carryState: READY }], enemies: [{ key: 'e', pos: [9, 5] }] });
const h = run({ defs: { enemies: { e: dummy('e', { atk: 100, bat: 1 }) } }, units: [{ chessId: id, row: 9, col: 5, carryState: READY }], enemies: [{ key: 'e', pos: [9, 5] }] });
const u = h.unit(id);
approx(u.s.res, u.base.res + t.magic_resistance, `${id} talent RES`);
h.step();
assert.ok(u.skill.active);
assert.ok(h.runUntil(() => u.skill.active, 3), 'cast by the first hit');
assert.equal(h.hooksOf('skillStart').find((c) => c.unit === u).reason, 'TAKE_DAMAGE');
approx(u.s.maxHp, u.base.maxHp * (1 + bb.max_hp), 'HP');
approx(u.s.def, u.base.def * (1 + bb.def), 'DEF');
approx(u.s.res, (u.base.res + t.magic_resistance) * (1 + bb.magic_resistance), 'RES');
@@ -131,10 +131,12 @@ test('1_04 深巡: fin darts pierce attack@max_target enemies on the line + 1 s
enemies: [{ key: 'e_sea', pos: [9, 5] }, { key: 'e', pos: [9, 6] }, { key: 'e', pos: [9, 7] }],
});
const u = h.unit(id);
h.step();
h.b.dealDamage(h.enemies()[0], u, { amount: 10, type: 'phys' }); // 重装: TAKE_DAMAGE
h.run(3.5);
assert.ok(u.skill.active);
approx(u.s.atk, u.base.atk * (1 + bb.atk));
const firstAtk = h.hooksOf('attack').find((c) => c.attacker === u);
const firstAtk = h.hooksOf('attack').find((c) => c.attacker === u && c.isSkill);
assert.equal(firstAtk.targets.length, bb['attack@max_target'], 'hits every enemy of the line');
const sl = statuses(h, 'sluggish', (c) => c.source === u);
assert.ok(sl.length >= 3 && sl.every((c) => c.duration === bb['attack@sluggish']));
@@ -328,7 +330,7 @@ test('1_09 跃跃 回环射手: each attack is a boomerang (out 15, back 3.75 ti
done(h2);
});
test('1_10 古米: TAKE_DAMAGE 备用军粮 heals the most injured nearby ally for heal_scale × ATK; elite low-HP bonus; pan stun', () => {
test('1_10 古米: 备用军粮 (自动触发: an injured ally of the skill range) heals the most injured nearby ally for heal_scale × ATK; elite low-HP bonus; pan stun', () => {
for (const id of ['chess_char_1_10_a', 'chess_char_1_10_b']) {
const bb = bbOf(id), tb = tbOf(id), t = tal(id);
const h = run({
@@ -343,7 +345,7 @@ test('1_10 古米: TAKE_DAMAGE 备用军粮 heals the most injured nearby ally f
const hh = heals(h, u, (c) => c.target === yak)[0];
const k = tb.hp_ratio != null ? tb.heal_scale : 1;
approx(hh.amount, u.s.atk * bb.heal_scale * k, `${id} heal`);
assert.equal(h.hooksOf('skillStart').find((c) => c.unit === u).reason, 'TAKE_DAMAGE');
assert.equal(h.hooksOf('skillStart').find((c) => c.unit === u).reason, 'SKILL_RANGE');
// talent: pan procs stun the enemy t.stun s
h.runUntil(() => statuses(h, 'stun', (c) => c.source === u).length > 0, 200);
const st = statuses(h, 'stun', (c) => c.source === u)[0];
@@ -588,11 +590,12 @@ test('1_20 雷蛇: 反击电弧 arts on up to 3 enemies, self-stun `stun` s afte
const u = h.unit(id);
h.run(3);
assert.ok(u.skill.active);
assert.equal(h.hooksOf('skillStart').find((c) => c.unit === u).reason, 'TAKE_DAMAGE', '重装: cast by the hit');
approx(u.s.atk, u.base.atk * (1 + bb.atk));
approx(u.s.interval, u.base.bat * (1 + bb.base_attack_time) * 100 / u.s.aspd, 'attack interval +70 % (PRTS 增大(+70%): a ratio, not +0.7 s)');
const a0 = h.hooksOf('attack').find((c) => c.attacker === u);
const a0 = h.hooksOf('attack').find((c) => c.attacker === u && c.isSkill);
assert.equal(a0.targets.length, 3);
assert.ok(dealt(h, u, (c) => c.dmg.isAttack).every((c) => c.type === 'arts'));
assert.ok(dealt(h, u, (c) => c.dmg.isAttack && c.dmg.isSkill).every((c) => c.type === 'arts'));
h.runUntil(() => !u.skill.active, 30);
h.step();
const st = statuses(h, 'stun', (c) => c.target === u);
@@ -631,9 +634,11 @@ test('2_01 送葬人: 最终旅程 double hits and shorter interval; 终结改
done(h);
});
test('2_02 赫默: 医疗无人机 lands next to the injured ally and expires; 强化注射 MEDIC ASPD; elite ×1.15 on ground units', () => {
test('2_02 赫默: 医疗无人机 lands on the tile its piece was placed on and expires; 强化注射 MEDIC ASPD; elite ×1.15 on ground units', () => {
const id = 'chess_char_2_02_a', t = tal(id);
const h = run({ units: [{ chessId: id, row: 10, col: 4, carryState: READY }, { chessId: 'chess_char_1_02_a', row: 9, col: 6 }, { chessId: 'chess_char_2_14_a', row: 12, col: 4 }] });
// the drone is a hand piece the player placed next to the ally (user playtest #6); S2 brings it there
const h = run({ units: [{ chessId: id, row: 10, col: 4, carryState: READY, uid: 1 }, { chessId: 'chess_char_1_02_a', row: 9, col: 6 }, { chessId: 'chess_char_2_14_a', row: 12, col: 4 },
{ kind: 'token', tokenId: 'token_10000_silent_healrb', ownerUid: 1, row: 9, col: 5 }] });
const u = h.unit(id), yak = h.unit('chess_char_1_02_a'), fl = h.unit('chess_char_2_14_a');
h.step();
yak.hp = yak.s.maxHp * 0.3;
@@ -643,7 +648,7 @@ test('2_02 赫默: 医疗无人机 lands next to the injured ally and expires;
approx(yak.s.aspd, yak.base.aspd, 'non-medic unaffected');
const drone = h.b.allyUnits.find((x) => x.kind === 'token');
assert.ok(drone && drone.alive, 'drone deployed');
assert.ok(Math.max(Math.abs(drone.tileR - yak.tileR), Math.abs(drone.tileC - yak.tileC)) <= 1, 'next to the injured ally');
assert.deepEqual([drone.tileR, drone.tileC], [9, 5], 'on its placed tile, next to the injured ally');
assert.ok(drone.s.flags.untargetable);
h.run(10);
assert.equal(drone.alive, false, 'self-destructs after its lifetime');
@@ -669,7 +674,8 @@ test('2_03 崖心 (hidden): 束缚链 drags max_target enemies to the front tile
const pulled = h.enemies().filter((e) => e.x < 6);
assert.equal(pulled.length, bb.max_target);
for (const e of pulled) {
assert.ok(Math.hypot(e.x - 5, e.y - 10) < 0.3, `at the front tile (${e.x},${e.y})`);
// 中力 vs weight 1 (受力等级 0): "必定拉至身前" — to the 急停 radius 0.6708 around her centre (PRTS 推与拉)
approx(Math.hypot(e.x - 4, e.y - 10), 0.6708, `at her front (${e.x},${e.y})`, 1e-6);
const tr = dealt(h, u, (c) => c.target === e && c.type === 'true');
assert.equal(tr.length, 1);
approx(tr[0].amount, u.s.atk * bb.atk_scale);
@@ -686,7 +692,7 @@ test('2_03 崖心 (hidden): 束缚链 drags max_target enemies to the front tile
h3.step();
const drag = dealt(h3, h3.unit(idb), tagged('drag'));
assert.equal(drag.length, 1);
approx(drag[0].amount, tb.value * 2 / tb.dist, 'value per dist tiles', 0.06);
approx(drag[0].amount, tb.value * (3 - 0.6708) / tb.dist, 'value per dist tiles', 0.06);
done(h3);
});
@@ -1065,8 +1071,7 @@ test('2_17 折桠: 生存决心 trembles ground enemies around, ATK/DEF up, hits
units: [{ chessId: id, row: 9, col: 5, carryState: READY }], enemies: [{ key: 'e', pos: [9, 5] }, { key: 'r', pos: [10, 6] }, { key: 'e', pos: [9, 8] }],
});
const u = h.unit(id);
h.step();
assert.ok(u.skill.active);
assert.ok(h.runUntil(() => u.skill.active, 2), '重装 TAKE_DAMAGE: cast by the first hit');
approx(u.s.atk, u.base.atk * (1 + bb.atk));
approx(u.s.def, u.base.def * (1 + bb.def));
const [blk, adj, far] = h.enemies();
@@ -1227,8 +1232,9 @@ test('1_19 野鬃 normal: 一致向前 only on the first deployment', () => {
done(h);
});
test('2_02 赫默: the drone never lands on the home tile of a knocked-out operator', () => {
const h = run({ units: [{ chessId: 'chess_char_2_02_a', row: 10, col: 4, carryState: READY }, { chessId: 'chess_char_1_02_a', row: 10, col: 5 }, { chessId: 'chess_char_1_10_a', row: 9, col: 5 }] });
test('2_02 赫默: the drone takes its own placed tile, never the home tile of a knocked-out operator', () => {
const h = run({ units: [{ chessId: 'chess_char_2_02_a', row: 10, col: 4, carryState: READY, uid: 1 }, { chessId: 'chess_char_1_02_a', row: 10, col: 5 }, { chessId: 'chess_char_1_10_a', row: 9, col: 5 },
{ kind: 'token', tokenId: 'token_10000_silent_healrb', ownerUid: 1, row: 11, col: 5 }] });
h.step();
const yak = h.unit('chess_char_1_02_a'), gum = h.unit('chess_char_1_10_a');
gum.hp = gum.s.maxHp * 0.3;
@@ -1236,6 +1242,7 @@ test('2_02 赫默: the drone never lands on the home tile of a knocked-out opera
h.run(0.5);
const d = h.b.allyUnits.find((x) => x.kind === 'token');
assert.ok(d && d.alive, 'drone deployed');
assert.deepEqual([d.tileR, d.tileC], [11, 5], 'its placed tile');
assert.ok(!(d.tileR === yak.homeR && d.tileC === yak.homeC), 'not on the dead operator tile');
done(h);
});