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
+20
-18
@@ -16,9 +16,10 @@
|
||||
// 3. lineup: the deployed set maximizes unit value + activated bond tiers (exact counting via computeBonds) +
|
||||
// composition (chooseLineup: greedy seed + swap hill-climbing).
|
||||
// 4. placement (planLayout): the round's routes are traced over the own board from the enemy preview (ground
|
||||
// routes on the stage's device-aware ground paths, flying routes through their checkpoints) and weighted by
|
||||
// their enemies; an exposure model (tile time × DPS of the covering units, blocker hold time, flyers only for
|
||||
// anti-air) is maximized greedily — blockers first, then damage dealers by DPS, then healers — over every
|
||||
// routes on the stage's device-aware ground paths, flying routes through their checkpoints; 近地悬浮 enemies walk
|
||||
// the ground path but count as flyers) and weighted by their enemies; an exposure model (tile time × DPS of the
|
||||
// covering units, blocker hold time, flyers only for anti-air) is maximized greedily — blockers first, then damage
|
||||
// dealers by DPS, then healers — over every
|
||||
// (legal tile, direction) pair: each unit's range grid is rotated per direction (DESIGN §3; RIGHT is tried first
|
||||
// and kept on ties, so symmetric ranges and melee units whose front adds nothing stay facing the gates), so
|
||||
// ranged units turn toward the enemy path tiles they cover best and blockers toward the road; on 气流 tiles
|
||||
@@ -37,11 +38,12 @@
|
||||
|
||||
import { GEO } from '../../shared/constants.js';
|
||||
import { deriveSeed } from '../sim/rng.js';
|
||||
import { freeSlot, legalTiles, positionClass, parseKey, tileKey, FIELD, pieceDir } from './board.js';
|
||||
import { freeSlot, legalTiles, positionClass, parseKey, tileKey, FIELD, pieceDir, boardTileOf, BOSS_MIRROR_COL } from './board.js';
|
||||
import { rotateOffset, normDir, mirrorDir, oppositeDir } from '../sim/dir.js';
|
||||
import { itemKey } from './gamedata.js';
|
||||
import { computeBonds } from './bondsMeta.js';
|
||||
import { withBounties } from './waves.js';
|
||||
import { HOVER_KEYS } from '../sim/content/enemies.js';
|
||||
|
||||
/**
|
||||
* Drive a step generator (planLayoutSteps, createRehearsalSteps, arrangeSteps, botPrepBeginSteps …) to its end in one
|
||||
@@ -101,6 +103,8 @@ export function botPickCard(m, ps, cards, available) {
|
||||
const chessRec = (m, id) => m.gd.chess(id);
|
||||
const isHealer = (c) => !!c && (c.dmgType === 'heal' || c.attackKind === 'heal');
|
||||
const isBlocker = (c) => !!c && positionClass(c) === 'melee' && (c.stats?.blockCnt ?? 1) > 0 && c.attackKind !== 'none';
|
||||
/** 近地悬浮 enemies walk a ground route but are air units (no block, anti-air only — DESIGN §19). */
|
||||
const HOVER = new Set(HOVER_KEYS);
|
||||
const hitsFly = (c) => !!c && !!c.canHitFly && !isHealer(c) && c.attackKind !== 'none';
|
||||
|
||||
/** Distinct owned members per bond (board + hand + temp), and per-bond member sets. */
|
||||
@@ -330,9 +334,7 @@ function traceLine(points) {
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Boss field → own board: board rows 9–12 are boss rows 2–5 (sim BOSS_ROW_OFFSET −7); the right side is mirrored. */
|
||||
const BOSS_ROW_SHIFT = 7;
|
||||
const BOSS_MIRROR_COL = 20;
|
||||
/** Boss field → own board: board.js boardTileOf (board rows 9–12 are boss rows 2–5; the right side mirrored). */
|
||||
const BOSS_MID_COL = 10;
|
||||
/** Dwell (s) the planner assumes on a leader's first own tiles (several leaders fight from their spawn point). */
|
||||
const BOSS_DWELL = 30;
|
||||
@@ -340,17 +342,16 @@ const BOSS_DWELL = 30;
|
||||
const LEADER_WEIGHT = 10;
|
||||
const LEADER_HP = 40000;
|
||||
|
||||
/** The boss-round wave of a player's group and its side ('L' | 'R'), or null. */
|
||||
/** The boss-round wave of a player's group and its side ('L' | 'R'), or null (Match.bossGroupOf). */
|
||||
function bossWaveOf(m, ps) {
|
||||
if (!ps || !Array.isArray(m.bossWaves)) return null;
|
||||
const g = m.bossWaves.find((x) => Array.isArray(x.players) && x.players.includes(ps.playerId));
|
||||
if (!g || !g.wave) return null;
|
||||
return { wave: g.wave, side: g.players.indexOf(ps.playerId) === 1 ? 'R' : 'L' };
|
||||
const g = typeof m.bossGroupOf === 'function' ? m.bossGroupOf(ps) : null;
|
||||
return g && g.wave ? { wave: g.wave, side: g.side } : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The round's routes over the own board (cached per round): [{ n, fly, tiles: ['r,c'…] (gate → objective, own
|
||||
* region only, objective tile excluded), tileTime (s per tile), hp }], a tile → [[route, index]] index, and the
|
||||
* The round's routes over the own board (cached per round): [{ n, fly (air units: flyers, and the 近地悬浮 HOVER_KEYS
|
||||
* on their ground path), tiles: ['r,c'…] (gate → objective, own region only, objective tile excluded), tileTime (s per
|
||||
* tile), hp }], a tile → [[route, index]] index, and the
|
||||
* aggregated ground flow per tile. Built from the wave preview (spawn counts per route, enemy HP / speed with the
|
||||
* round's multipliers). Boss rounds (pass `ps`): the player's boss-field template mapped onto the own board (rows −7,
|
||||
* the right player's half mirrored; only the routes that end on the player's half), the leader weighted heavily with
|
||||
@@ -367,7 +368,7 @@ export function fieldModel(m, ps = null) {
|
||||
const gpaths = (st && (st.groundPathsWithDevices || st.groundPaths)) || {};
|
||||
const routesOut = [];
|
||||
const toBoard = boss
|
||||
? ([r, c]) => [r + BOSS_ROW_SHIFT, boss.side === 'R' ? BOSS_MIRROR_COL - c : c]
|
||||
? ([r, c]) => boardTileOf(boss.side === 'R' ? 'bossR' : 'bossL', r, c)
|
||||
: (p) => p;
|
||||
const pushRoute = (tilesRC, n, fly, hp, speed, dwell = 0) => {
|
||||
const own = tilesRC.map(toBoard).filter(([r, c]) => inRect(r, c)).map(([r, c]) => tileKey(r, c));
|
||||
@@ -400,8 +401,9 @@ export function fieldModel(m, ps = null) {
|
||||
// the leader counts like LEADER_WEIGHT tough enemies: its huge pool makes damage on it worth a lot everywhere
|
||||
const hp = isLeader ? LEADER_HP : ((e && e.stats && e.stats.maxHp) || 1000) * ((s.mods && s.mods.hpMul) || 1);
|
||||
const spd = ((e && e.stats && e.stats.moveSpeed) || 1) * ((s.mods && s.mods.speedMul) || 1);
|
||||
const key = `${s.routeIndex}|${isLeader ? 'boss' : ''}`;
|
||||
const a = per.get(key) || { ri: s.routeIndex, boss: isLeader, n: 0, hp: 0, spd: 0 };
|
||||
const air = HOVER.has(s.enemyKey);
|
||||
const key = `${s.routeIndex}|${isLeader ? 'boss' : ''}|${air ? 'air' : ''}`;
|
||||
const a = per.get(key) || { ri: s.routeIndex, boss: isLeader, air, n: 0, hp: 0, spd: 0 };
|
||||
a.n += isLeader ? LEADER_WEIGHT : n; a.hp += hp * n; a.spd += spd * n;
|
||||
per.set(key, a);
|
||||
}
|
||||
@@ -418,7 +420,7 @@ export function fieldModel(m, ps = null) {
|
||||
} else {
|
||||
const key = `${rt.start[0]},${rt.start[1]}->${rt.end[0]},${rt.end[1]}`;
|
||||
const path = Array.isArray(gpaths[key]) ? gpaths[key] : traceLine([rt.start, ...(boss && Array.isArray(rt.checkpoints) ? rt.checkpoints : []), rt.end]);
|
||||
pushRoute(path, a.n, false, a.hp / cnt, a.spd / cnt, dwell);
|
||||
pushRoute(path, a.n, a.air, a.hp / cnt, a.spd / cnt, dwell); // a hovering enemy: ground tiles, air unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user