Playtest #4 fixes: tile picking, knocked-down state, element gauges, draft timer, live stats, shop items, drones, 赫默, 炎佑, UI flash
- #1 picking by the tile under the pointer (prep, battle, pen, equipment drops); the dragged model stays under the pointer (mouse and touch); pixel probe removed; phone ghost-click on the underframe fixed - #2 two-tap confirm on 机变 cards (like the shop) - #3 a 同盟 room with one human is untimed like 独立模拟 (no countdown outside battles) - #4 strategy draft: one synced countdown, 30 s per human turn, timeout takes the highlighted band - #5 special 维式重锤 (4) and 突变细胞 no longer sold; item card shows where they come from - #6 operator skill-mode sounds no longer play on normal attacks (纯烬艾雅法拉 and 55 others) - #7 detail card shows live battle stats and the prep-time battle-start stats (g.unitStats) - #8 official element gauges on units; burst cooldown (阻回) per PRTS - #9 knocked-down operators stay on their tile with a redeploy countdown ring - #10 drone behaviour per PRTS (暴鸰, 先兆者 shells, 黑云, 萨科塔之眼) - #11 赫默's drone only from her skill; placeable summons limited to talent tokens - #12 炎佑 stays where it is with no enemies; 祛恶之焰 as a continuous channel - #13 the occasional giant bond icon (loading texture scaled ×46) fixed - docs (DESIGN §18), recordings, data and tests updated (2467 unit tests, all browser suites green)
This commit is contained in:
1 parent
0aba602ecf
commit
d4f868cd19
107 files changed
+4510
-1653
No files matched your search
@@ -1,8 +1,12 @@
|
||||
// INFO_CHECK, band draft (order / skip / timeouts / 队友已选) and 机变 SP drafts (order / timers / auto-assign / effects).
|
||||
// INFO_CHECK, band draft (order / skip / one turn clock / timeouts / highlighted band / 队友已选) and 机变 SP drafts (order /
|
||||
// timers / auto-assign / effects); a single human (solo or co-op with AI teammates) is never timed outside battles.
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { ERR, PHASE } from '../../shared/constants.js';
|
||||
import { DATA, makeMatch, checkInvariants } from './harness.js';
|
||||
import { BAND_TURN_SECONDS } from '../../server/match/Match.js';
|
||||
|
||||
const TURN_MS = BAND_TURN_SECONDS * 1000;
|
||||
|
||||
test('INFO_CHECK: ends when every human confirmed (bots/departed count as ready) or at the 25 s deadline', () => {
|
||||
const h = makeMatch({ mode: 'coop', humans: 2, bots: 1, seed: 1 }).start();
|
||||
@@ -61,16 +65,27 @@ test('band draft (co-op): random order, one pick per turn, NOT_YOUR_TURN, one sk
|
||||
m.dispose();
|
||||
});
|
||||
|
||||
test('band draft timers: 12 s per turn → 华法琳; the 50 s step cap assigns the rest; bots pick by themselves', () => {
|
||||
test('band draft timers (user playtest #4 item 4): one countdown of BAND_TURN_SECONDS per turn → the highlighted band or 华法琳; AI seats pick at once; no step cap', () => {
|
||||
const h = makeMatch({ mode: 'coop', humans: 2, bots: 2, seed: 9 }).start();
|
||||
const m = h.m;
|
||||
m.handle('p_0', { t: 'g.infoReady' });
|
||||
m.handle('p_1', { t: 'g.infoReady' });
|
||||
h.sched.advance(1);
|
||||
assert.equal(m.phase, PHASE.BAND_DRAFT);
|
||||
// humans never act: each human turn times out after 12 s; bots pick within ~1 s
|
||||
h.run(() => m.phase !== PHASE.BAND_DRAFT, { maxTime: 60000 });
|
||||
// humans never act: each human turn times out after BAND_TURN_SECONDS; bots pick at once (no bot turn is timed out)
|
||||
const t0 = h.sched.now();
|
||||
const seen = [];
|
||||
h.onBroadcast.push((msg) => { if (msg.t === 'm.public' && msg.phase === PHASE.BAND_DRAFT && msg.draft) seen.push(msg); });
|
||||
h.run(() => m.phase !== PHASE.BAND_DRAFT, { maxTime: 10 * TURN_MS });
|
||||
assert.equal(m.phase, PHASE.BATTLE_CHECK);
|
||||
const took = h.sched.now() - t0;
|
||||
assert.ok(took >= 2 * TURN_MS - 5 && took <= 2 * TURN_MS + 50, `two human turns of ${BAND_TURN_SECONDS} s each (${took} ms)`);
|
||||
// every published draft view: the step's deadline IS the current turn's
|
||||
assert.ok(seen.length > 0);
|
||||
for (const pub of seen) {
|
||||
assert.equal(pub.deadline, pub.draft.turnDeadline, 'one countdown');
|
||||
assert.equal(pub.draft.turnSeconds, BAND_TURN_SECONDS);
|
||||
}
|
||||
// a timeout gives 华法琳 while no teammate holds it, else the first free strategy by sortId — never 队友已选
|
||||
// (Match.defaultBand): replay the assignments in draft order
|
||||
const taken = new Set();
|
||||
@@ -83,17 +98,22 @@ test('band draft timers: 12 s per turn → 华法琳; the 50 s step cap assigns
|
||||
}
|
||||
assert.equal(new Set(Object.values(m.draft.picks)).size, 4, 'no duplicate strategies');
|
||||
m.dispose();
|
||||
// overall cap: with 4 humans × 12 s > 50 s the last one is assigned at the cap
|
||||
// 4 humans idle: 4 whole turns (the old 50 s step cap would have cut the last ones short)
|
||||
const h2 = makeMatch({ mode: 'coop', humans: 4, seed: 9 }).start();
|
||||
for (const ps of h2.m.players.values()) h2.m.handle(ps.playerId, { t: 'g.infoReady' });
|
||||
h2.sched.advance(1);
|
||||
const t0 = h2.sched.now();
|
||||
h2.run(() => h2.m.phase !== PHASE.BAND_DRAFT, { maxTime: 120000 });
|
||||
assert.ok(h2.sched.now() - t0 <= 50000 + 5, `ended by the 50 s cap (${h2.sched.now() - t0} ms)`);
|
||||
const first = h2.m.draft.order[0];
|
||||
assert.equal(h2.ps(first).bandId, 'band_bldsk', 'the first timeout gets 华法琳');
|
||||
assert.deepEqual(h2.m.handle(first, { t: 'g.bandFocus', bandId: 'band_amiya' }), { ok: true });
|
||||
const s0 = h2.sched.now();
|
||||
h2.run(() => h2.m.phase !== PHASE.BAND_DRAFT, { maxTime: 10 * TURN_MS });
|
||||
const took2 = h2.sched.now() - s0;
|
||||
assert.ok(took2 >= 4 * TURN_MS - 5 && took2 <= 4 * TURN_MS + 50, `four turns (${took2} ms)`);
|
||||
assert.equal(h2.ps(first).bandId, 'band_amiya', 'the first timeout takes the highlighted strategy');
|
||||
assert.equal(h2.ps(h2.m.draft.order[1]).bandId, 'band_bldsk', 'the next one the official default');
|
||||
assert.equal(new Set([...h2.m.players.values()].map((ps) => ps.bandId)).size, 4, 'the rest get distinct free strategies');
|
||||
h2.m.dispose();
|
||||
// data and code say the same: data/config.json timers.bandTurn (tools/build-data.mjs) = Match.BAND_TURN_SECONDS
|
||||
assert.equal(DATA.config.timers.bandTurn, BAND_TURN_SECONDS);
|
||||
});
|
||||
|
||||
test('band draft (solo): free pick, no timer, no skip; mode-restricted bands rejected', () => {
|
||||
@@ -214,3 +234,43 @@ test('机变 tactic defaults: team cards reach teammates; layers / funds / free
|
||||
});
|
||||
|
||||
const awaitImport = await import('../../server/match/choices.js');
|
||||
|
||||
test('a single human (a 同盟 room started alone or with AI teammates only) is never timed outside battles — user playtest #4 item 3', () => {
|
||||
for (const bots of [0, 3]) {
|
||||
const h = makeMatch({ mode: 'coop', difficulty: 'NORMAL', humans: 1, bots, seed: 12, fake: true, instant: false }).start();
|
||||
const m = h.m;
|
||||
assert.equal(m.soloUntimed, true);
|
||||
assert.equal(m.isSolo, false, 'the co-op mode rules stay');
|
||||
const seen = new Map(); // phase → deadlines published
|
||||
const note = (pub) => { if (!seen.has(pub.phase)) seen.set(pub.phase, []); seen.get(pub.phase).push(pub.deadline); };
|
||||
for (const msg of h.bc) if (msg.t === 'm.public') note(msg);
|
||||
h.onBroadcast.push((msg) => { if (msg.t === 'm.public') note(msg); });
|
||||
assert.equal(m.phase, PHASE.INFO_CHECK);
|
||||
h.sched.advance(10 * 60_000);
|
||||
assert.equal(m.phase, PHASE.INFO_CHECK, `briefing waits (${bots} AI)`);
|
||||
m.handle('p_0', { t: 'g.infoReady' });
|
||||
h.sched.advance(1);
|
||||
h.run(() => m.draftTurn() === 'p_0', { maxTime: 1000 });
|
||||
h.sched.advance(10 * 60_000);
|
||||
assert.equal(m.phase, PHASE.BAND_DRAFT, 'the strategy draft waits');
|
||||
assert.ok(h.drive(() => m.phase === PHASE.PREP && m.round === 2), 'reached PREP R2');
|
||||
h.sched.advance(60 * 60_000);
|
||||
assert.equal(m.phase, PHASE.PREP, 'prep waits');
|
||||
assert.ok(h.drive(() => m.phase === PHASE.PREP && m.round === 4), 'past the R3 机变');
|
||||
h.flushAll();
|
||||
for (const ph of [PHASE.INFO_CHECK, PHASE.BAND_DRAFT, PHASE.BATTLE_CHECK, PHASE.ROUND_START, PHASE.SP_DRAFT, PHASE.PREP, PHASE.SETTLE]) {
|
||||
assert.ok(seen.has(ph), `saw ${ph}`);
|
||||
assert.ok(seen.get(ph).every((d) => d === 0), `${ph}: no countdown with a single human (${bots} AI): ${seen.get(ph)}`);
|
||||
}
|
||||
assert.ok(seen.get(PHASE.COMBAT).some((d) => d > 0), 'the battles keep their clock');
|
||||
m.dispose();
|
||||
}
|
||||
// two humans: the co-op timers stay
|
||||
const h = makeMatch({ mode: 'coop', humans: 2, bots: 1, seed: 12 }).start();
|
||||
assert.equal(h.m.soloUntimed, false);
|
||||
const pub = h.lastBc('m.public');
|
||||
assert.equal(pub.deadline - pub.serverNow, 25000, 'INFO_CHECK 25 s');
|
||||
h.toPrep(1);
|
||||
assert.ok(h.m.deadline > 0, 'co-op prep is timed');
|
||||
h.m.dispose();
|
||||
});
|
||||
Reference in new issue
Block a user