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:
1 parent
d4f868cd19
commit
15291c2293
104 files changed
+5539
-662
No files matched your search
+39
-9
@@ -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 });
|
||||
|
||||
Reference in new issue
Block a user