Playtest #5 fixes: hovering enemies, target order, blocking hand-over, 联防 knocked-out, elements, huge-boss hit area, maps, gear icon, phone HUD, 至简 price

- #1 近地悬浮 enemies count as air units (Unit.isFlying): ground-only attacks, splash, terrain and traps skip them;
  PRTS 不可对空 skill effects made ground-only; 掠海漂移体 / 吉兆飞鳞 drop modes; the bot plans them as air
- #2 an operator knocked out before 联防 is fielded and force-exited (PRTS 强制退场): it lies on its tile with the
  redeploy ring and redeploys (full timer [ASSUMED]); partners, observers and the server fallback included
- #3 element audit of every operator, item and enemy: 海嗣 numbers are official; bursts are 无来源, 元素脆弱 only
  raises 元素伤害, 元素抗性 applied, 盟约·辅助干员 fills all three gauges, 菲莱 / 哈洛德 / 纯烬 / enemy kits fixed
- #4 blocking by the official contact radius (0.7071): another operator in contact takes over at once when the
  blocker dies, withdraws or is stunned; melee blockers attack what they block
- #5 enemy target order per PRTS 索敌: blocker, special priority, taunt, last deployed; initial deploy 从上到下>从左到右,
  operators before summons; special priorities break ties by taunt then deploy order
- #6 act2 m01's normal-field blower machines (row 13) are drawn in the normal / 联防 views
- #7 Final Assault prep legality reads the player's half of the boss field (fence tiles deployable); all 11 stages
  audited against the official level files
- #8 regular settings gear; #9 the prep camera keeps the bench and temp rows clear of the shop bar on phones
- #10 huge bosses have the official 4.95 × 2.95 hit area (sim/body.js) and are 自缚 / unblockable
- #11 至简 costs 1: 购买价格为N's bb.price is a discount off the tier price, applied before other price effects
- DESIGN §19 and the normative §3 / §5 / §6 / §7 lines, SIM / META / DATA / PLAYING updated; 2595 unit tests and
  every browser suite green; dev recordings regenerated
This commit is contained in:
sganggs committed 2026-09-30 19:12:36 +08:00
1 parent d4f868cd19
commit 15291c2293
104 files changed
+5539 -662

No files matched your search

+39 -9
View File
@@ -496,9 +496,10 @@ export class Match {
ps.eliminate(passedRound);
this.tickerText(`${ps.name}博士中途退出了模拟`);
if (this.bossWaves && (phase === PHASE.ROUND_START || phase === PHASE.SP_DRAFT || phase === PHASE.PREP)) {
// before the boss fight: pair the players left again (the prep preview shows the new partner / template)
// before the boss fight: pair the players left again (the prep preview shows the new partner / template); a
// player moved to the other half re-checks its board there at once (recompute → deployMap, marks it private)
this._planBossWaves();
for (const p of this.alivePlayers()) this.markPrivate(p);
for (const p of this.alivePlayers()) p.recompute();
}
this.markPublic();
if (this.teamLp != null) this._syncTeamLp();
@@ -606,8 +607,35 @@ export class Match {
}
humans() { return this.order.filter((p) => !p.isBot && !p.left); }
deployMapFor(ps) {
return buildDeployMap(this.stage, { deviceOverrides: ps.deviceOverrides, tileOverrides: ps.tileOverrides });
/**
* The boss-round group of a player (`bossWaves`: the seat pairs of finalAssault.js pairPlayers, planned in startRound
* before the players' round start) and its side — 'L', or 'R' for the second player of a pair (the mirrored right
* half) — or null outside a boss round / for a player without a field.
* @returns {{ wave: object, players: string[], side: 'L'|'R' } | null}
*/
bossGroupOf(ps) {
if (!ps || !Array.isArray(this.bossWaves)) return null;
const g = this.bossWaves.find((x) => Array.isArray(x.players) && x.players.includes(ps.playerId));
return g ? { wave: g.wave, players: g.players, side: g.players.indexOf(ps.playerId) === 1 ? 'R' : 'L' } : null;
}
/**
* The field a player deploys on (server/match/board.js DEPLOY_FIELDS): in a boss round (最终攻势 / 隐秘核心, from its
* ROUND_START on — the pairing exists before PlayerState.startRound's recompute) the player's half of the boss field,
* 'bossL' or 'bossR' (bossGroupOf); otherwise its own normal board. User playtest #5 item 7.
* @returns {'normal'|'bossL'|'bossR'}
*/
deployFieldOf(ps) {
const g = this.bossGroupOf(ps);
return !g ? 'normal' : g.side === 'R' ? 'bossR' : 'bossL';
}
/**
* A fresh deploy map of a player on `field` (default: the field it deploys on now) under its device / tile overrides.
* Pure — no PlayerState cache is touched (the read-only checker invariants.js uses it; PlayerState.deployMap caches).
*/
deployMapFor(ps, field = this.deployFieldOf(ps)) {
return buildDeployMap(this.stage, { deviceOverrides: ps.deviceOverrides, tileOverrides: ps.tileOverrides, field });
}
dispatch(ps, hook, ev = {}, opts = {}) {
@@ -813,10 +841,9 @@ export class Match {
nextEnemiesFor(ps) {
if (!ps.alive) return [];
if (this.bossWaves) {
const g = this.bossWaves.find((x) => x.players.includes(ps.playerId));
const g = this.bossGroupOf(ps);
if (!g) return [];
const side = g.players.indexOf(ps.playerId) === 1 ? 'R' : 'L';
return previewOf([...g.wave.spawns, ...bountySpawns(this.gd, this.round, g.wave, ps.bounties, ps.playerId, { solo: this.isSolo, side })]);
return previewOf([...g.wave.spawns, ...bountySpawns(this.gd, this.round, g.wave, ps.bounties, ps.playerId, { solo: this.isSolo, side: g.side })]);
}
if (!this.wave) return [];
const bounty = bountySpawns(this.gd, this.round, this.wave, ps.bounties, ps.playerId, { solo: this.isSolo });
@@ -1305,14 +1332,17 @@ export class Match {
this.wave = null;
this.bossWaves = null;
const alive = this.alivePlayers();
for (const ps of alive) ps.startRound(r);
// the round's enemies (shared composition, generated now so the prep preview is exact)
// the round's enemies (shared composition, generated now so the prep preview is exact). Planned BEFORE the players'
// round start: its recompute() checks the board on the field the player deploys on this round (deployFieldOf reads
// the boss pairing), so R14 → R15 never re-checks a boss-field board against the normal field (user playtest #5
// item 7). rngWaves is used only here, so the order leaves every random stream unchanged.
const isBoss = r === this.gd.bossRound || r === this.gd.hiddenRound;
if (isBoss) {
this._planBossWaves();
} else {
this.wave = buildNormalWave(this.gd, this.rngWaves, this.factions, r);
}
for (const ps of alive) ps.startRound(r);
for (const ps of alive) this.dispatch(ps, 'onRoundStart', { round: r });
for (const ps of alive) ps.recompute();
this.setDeadline(DELAYS.ROUND_START / 1000, () => this.afterRoundStart(), { silent: this.soloUntimed });