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

+23 -19
View File
@@ -168,11 +168,12 @@ test('#4: a blocker attacks the enemy it blocks even when it stands outside its
assert.ok(h.hooksOf('damaged').some((c) => c.source === u && c.target === x()), 'it hits its blocked enemy');
});
test('#4: a ranged operator on a melee tile blocks, but never attacks or casts for the enemy held behind it (阻挡(近战限定))', () => {
// PRTS 索敌的概念: "我方索敌优先级:阻挡(近战限定)"; "远程位干员通常而言不会优先攻击自己阻挡的目标;甚至如果这个目标被阻挡在
// 该干员的身后,该干员可能因为敌人未处于自身攻击范围而无法攻击到这个敌人". Facing LEFT, its range (9,6),(9,5) never holds
// the enemy blocked at x ≈ 6.71; a melee operator in the same spot hits it and casts its DEFAULT skill (帮助: "敌人被近战
// 干员自身阻挡" satisfies the target condition).
test('#4 (revised by the user after playtest #6): a ranged operator on a melee tile also hits and casts for the enemy it holds behind it', () => {
// The playtest #5 QA had made blocked-first melee-only (PRTS 索敌的概念 "阻挡(近战限定)"); the user's rule after
// playtest #6 — "阻挡了就一定要能打到": officially the collision pushes a blocked enemy to its blocker's front, so
// whatever blocks an enemy can hit it. Facing LEFT, the range (9,6),(9,5) never holds the enemy blocked at x ≈ 6.71;
// the ranged blocker hits it and casts its DEFAULT skill like a melee one (帮助: the blocked enemy satisfies the
// target condition).
const run = (id, profession) => {
const h = makeBattle({
defs: { chess: { [id]: chessRec({ id, profession, stats: { atk: 100, blockCnt: 1, maxHp: 1e6 }, skill: { spCost: 1, initSp: 1, duration: 5 } }) }, enemies: { enemy_x: foe('enemy_x') } },
@@ -187,13 +188,12 @@ test('#4: a ranged operator on a melee tile blocks, but never attacks or casts f
checkInvariants(h.b);
return { hits: h.hooksOf('damaged').filter((c) => c.source === u && c.target === x()).length, casts: u.skill.activations, still: x().blockedBy === u };
};
const ranged = run('t_rg', 'SNIPER');
assert.equal(ranged.hits, 0, 'the ranged blocker never hits the enemy behind it');
assert.equal(ranged.casts, 0, 'nor casts its skill for it');
assert.ok(ranged.still, 'it still holds the enemy');
const melee = run('t_ml', 'WARRIOR');
assert.ok(melee.hits > 0, 'a melee blocker hits it');
assert.equal(melee.casts, 1, 'and casts its skill');
for (const [id, profession] of [['t_rg', 'SNIPER'], ['t_ml', 'WARRIOR']]) {
const r = run(id, profession);
assert.ok(r.hits > 0, `${profession}: hits the enemy it blocks behind it`);
assert.equal(r.casts, 1, `${profession}: and casts its skill`);
assert.ok(r.still, `${profession}: it still holds the enemy`);
}
});
test('#4: "melee" is the deploy position — a 领主 with a ranged attack (银灰) still hits the enemy it holds behind it', () => {
@@ -213,10 +213,10 @@ test('#4: "melee" is the deploy position — a 领主 with a ranged attack (银
assert.ok(h.hooksOf('damaged').some((c) => c.source === u && c.target === x()), 'it hits the enemy it blocks');
});
test('#4: blocked-first is a melee priority — a ranged blocker shoots the enemy nearest the goal', () => {
test('#4 (revised after playtest #6): blocked-first holds for every blocker — a ranged blocker keeps shooting the enemy it blocks', () => {
// range (9,5)–(9,7) facing RIGHT: the walker it blocks stands on (9,7); a flyer passes over the operator towards the
// goal. The ranged operator picks by its own order (least remaining distance first), a melee one (able to hit air)
// keeps hitting the enemy it blocks.
// goal. Melee or ranged, the blocker keeps hitting the enemy it blocks (user's rule: the blocked enemy stands in front
// of its blocker officially) — the ranged one no longer switches to the flyer nearer the goal.
const run = (id, o) => {
const h = makeBattle({
defs: {
@@ -232,10 +232,13 @@ test('#4: blocked-first is a melee priority — a ranged blocker shoots the enem
assert.ok(h.runUntil(() => walker()?.blockedBy === u, 30), 'the walker is blocked');
assert.ok(h.runUntil(() => flyer() && flyer().x < 4.4, 30), 'the flyer passed');
assert.equal(walker().blockedBy, u);
return h.hooksOf('damaged').filter((c) => c.source === u && c.target === flyer()).length;
return { fl: h.hooksOf('damaged').filter((c) => c.source === u && c.target === flyer()).length, wk: h.hooksOf('damaged').filter((c) => c.source === u && c.target === walker()).length };
};
assert.ok(run('t_rg2', { profession: 'SNIPER' }) > 0, 'the ranged blocker shot the flyer while it was nearer the goal');
assert.equal(run('t_ml2', { profession: 'WARRIOR', attackKind: 'melee', canHitFly: true }), 0, 'the melee blocker kept to its blocked enemy');
for (const [id, o] of [['t_rg2', { profession: 'SNIPER' }], ['t_ml2', { profession: 'WARRIOR', attackKind: 'melee', canHitFly: true }]]) {
const r = run(id, o);
assert.equal(r.fl, 0, `${o.profession}: kept to its blocked enemy`);
assert.ok(r.wk > 0, `${o.profession}: hit the walker it blocks`);
}
});
test('#4: a skywalker (起飞: blockFly) blocks flyers at the air block radius 0.8944', () => {
@@ -257,7 +260,8 @@ test('#4: 瑕光 S2 puts the enemy she blocks to sleep although it stands outsid
const id = 'chess_char_3_12_a';
const skillIndex = ds.rawChess(id).skills.find((s) => s.skillId === 'skchr_blemsh_2').index;
const h = makeBattle({
defs: { enemies: { enemy_x: foe('enemy_x') } }, timeLimit: 60,
// (a MANUAL 重装 skill: cast by the blocked enemy's hit — TAKE_DAMAGE)
defs: { enemies: { enemy_x: foe('enemy_x', { atk: 100, bat: 1 }) } }, timeLimit: 60,
units: [{ chessId: id, row: 9, col: 6, skillIndex }], enemies: [{ key: 'enemy_x', time: 0 }],
});
const u = h.unit(id);