feedback1 integrate: WC 深水区 refuses deployment; tactical points only inside the summoner's attack range

# Conflicts:
#	server/match/bot.js
This commit is contained in:
sganggs committed 2026-10-03 06:35:09 +08:00
commit b1644f5ad7
27 files changed
+870 -81

No files matched your search

+3 -1
View File
@@ -23,7 +23,9 @@
// + 脆弱 ×damage_scale for weak[limit] s
// 助力 deputShip all operators DEF +(base + per·L), redeploy time ×(1 + respawn_time)
// 突袭 raidShip member idle ≥ no_attack_duration s (or skill ready) with no enemy in range → "保留技力立即再部署"
// next to the most advanced ground enemy: a real redeployment (retreat + free redeploy on the
// next to the most advanced ground enemy, on a free tile its position may be deployed on
// (grid.canStand: never the 深水区 — player report #3 after 0.1.0, members dropped into 战场#08's
// pool after an enemy wading in it): a real redeployment (retreat + free redeploy on the
// landing tile, full HP, `deploy` fires — 部署时 traits such as 史尔特尔, 突袭手雷, 卡西米尔, 叙拉古)
// with its SP / charges kept (engine redeploy tile + keepSp); ATK/HP +(base + per·L)
// until it leaves the field; later redeploys use its board tile again;
+3 -1
View File
@@ -24,7 +24,9 @@
// the field only (unite / boss fields hold both halves); Battle opts.deviceOverrides switch every half.
// `deviceOverridesOf(battle)` is the union (informational).
// Terrain numbers come from `stage.special` (fallback: the device's skill blackboard, then research 05 values).
// Not implemented: the devices of act1 m05–m07 (canoe platforms, sandstorm + mounds, bushes) — weight 0 this season.
// Not implemented: the devices of act1 m05–m07 (canoe platforms, sandstorm + mounds, bushes) — weight 0 this season. The
// canoe (特制水上平台) makes its 深水区 deployable in the prep's deploy map only (server/match/board.js); the sim's Grid
// keeps those tiles NONE (grid.js DEPLOY_REFUSED_TILES), so automatic placements never use them [ASSUMED].
import { COLS, ROWS } from '../constants.js';
import { performAttack } from '../ai.js';
+6 -4
View File
@@ -137,12 +137,14 @@ function freeTile(battle, r, c) {
/** Walkable ground tile a melee summon can stand on. */
const groundTile = (battle, r, c) => battle.grid.groundPassable(r, c) && battle.grid.canStand(r, c);
/**
* Tactical point (战术点) of a tactician: `prefer` (the board piece's tile, i.e. the player's choice) when usable,
* else the shared tactical point (tokens.js tacticalPoint = Battle.findTacticalPoint: a free walkable tile of its
* initial range on an enemy ground path first, then the nearest one).
* Tactical point (战术点) of a tactician: `prefer` (the board piece's tile, i.e. the player's choice) when usable —
* free, standable ground (never 深水区: grid.canStand) inside her initial range ("只能部署在召唤者攻击范围内"; the prep
* already keeps the piece there, PlayerState._legal) —, else the shared tactical point (tokens.js tacticalPoint =
* Battle.findTacticalPoint: a free walkable tile of its initial range on an enemy ground path first, then the nearest).
*/
function tacticalPoint(battle, unit, prefer = null) {
if (prefer && freeTile(battle, prefer[0], prefer[1]) && groundTile(battle, prefer[0], prefer[1])) return prefer;
const inRange = (r, c) => (unit.baseRangeKeys || unit.rangeKeys || []).includes(r * COLS + c);
if (prefer && freeTile(battle, prefer[0], prefer[1]) && groundTile(battle, prefer[0], prefer[1]) && inRange(prefer[0], prefer[1])) return prefer;
return sharedTacticalPoint(battle, unit);
}
/** Tokens `tokenId` summoned by / placed for `owner` (board pieces included). */
+29 -4
View File
@@ -5,6 +5,13 @@
// Stage input: `{ id, rows: string[19] (bottom-first), legend?: {glyph: {...}}, devices?: [...] }`.
// Legend entries are normalised from either the research format (heightType/buildableType/passableMask/tileKey)
// or the build-data format (height/build/passable/terrain); missing glyphs fall back to DEFAULT_LEGEND.
// `build` is the EFFECTIVE deploy type: a tile whose mechanism refuses deployment (DEPLOY_REFUSED_TILES: 深水区
// tile_deepsea — PRTS 深水区 地形信息 "地形机制:拒绝部署(待补充)"; player report after 0.1.0: operators stood in
// 战场#08's pool and 突袭 members jumped into it) is NONE whatever its level buildableType (ALL), so `canStand` — every
// automatic placement: the 突袭 landing tile, tactical points, summon tiles — and the path tie-break below treat it as
// ground no operator can stand on. data/stages.json legends already carry the effective value (tools/build-data.mjs).
// Known limit [ASSUMED]: the 特制水上平台 that makes a 深水区 deployable (act1 m05, weight 0 this season) exists only in
// the match's deploy map; the sim does not model those canoes (content/devices.js), so here their tiles stay NONE.
//
// Pathfinding = the official client's (`Torappu.Battle.SPFA`, research 08 §3.1/§3.4): one FLOW FIELD per destination,
// a FIFO SPFA from the destination over the rect with the 4 neighbours UP (row+1), RIGHT, DOWN, LEFT (in that order,
@@ -18,7 +25,9 @@
// operator can block them"). Two refinements on top of the official algorithm; `dist` (so the crate cost 1000 and
// every grid route length) stays exactly the official one:
// * tie-break: the SPFA relaxes on (dist, pen) lexicographically, `pen` = number of NON-BLOCKABLE walkable tiles
// (floor / gate / goal / teleport tiles — `blockable()` false: not LOW ground buildable for melee) on the chain.
// (floor / gate / goal / teleport / 深水区 tiles — `blockable()` false: not LOW ground buildable for melee) on the
// chain (no gate route of 战场#08 changed when its 深水区 became non-buildable; a few off-route smoothed steps of its
// boss field, rows 4–5, now go around the water instead of cutting across it).
// Among equal-length chains the one with the fewest non-blockable tiles wins; remaining ties go to the first
// parent in SPFA order (the official order unless a pen improvement re-queued a tile).
// * smoothing: a line of sight may only cross a non-blockable tile (including the corner tiles of a diagonal step)
@@ -67,10 +76,19 @@ export const DEFAULT_LEGEND = Object.freeze({
O: { height: 'LOW', build: 'NONE', pass: 'ALL', key: 'tile_telout', special: 'telout' },
m: { height: 'LOW', build: 'ALL', pass: 'ALL', key: 'tile_mire', terrain: 'mire' },
g: { height: 'LOW', build: 'ALL', pass: 'ALL', key: 'tile_smog', terrain: 'smog' },
d: { height: 'LOW', build: 'ALL', pass: 'ALL', key: 'tile_deepsea', terrain: 'deepsea' },
d: { height: 'LOW', build: 'NONE', pass: 'ALL', key: 'tile_deepsea', terrain: 'deepsea' },
i: { height: 'LOW', build: 'ALL', pass: 'ALL', key: 'tile_infection', terrain: 'infection' },
});
/**
* Tile keys whose mechanism refuses deployment although the level's buildableType allows it: 深水区 tile_deepsea (PRTS
* 深水区 地形信息 "部署类型 全部位 … 地形机制 拒绝部署(待补充)"; PRTS 作战机制: the 地形标记 AdvancedBuildableMask —
* 深水 among them — "限制玩家仅能部署匹配的单位于其上"; the season-1 战场#05 puts a 特制水上平台 — "在水上建立可以部署
* 任意单位的平台" — on every one of its 深水区 tiles). Shared with tools/build-data.mjs (the stages.json legend's
* `buildable`).
*/
export const DEPLOY_REFUSED_TILES = Object.freeze(new Set(['tile_deepsea']));
const TERRAIN_BY_KEY = { tile_mire: 'mire', tile_smog: 'smog', tile_deepsea: 'deepsea', tile_infection: 'infection', tile_deepwater: 'deepsea' };
const SPECIAL_BY_KEY = { tile_start: 'start', tile_end: 'end', tile_telin: 'telin', tile_telout: 'telout' };
@@ -92,7 +110,8 @@ export function normalizeLegendEntry(glyph, e) {
const heightRaw = e.height ?? e.heightType;
const height = heightRaw == null ? base.height : (/HIGH/i.test(String(heightRaw)) ? 'HIGH' : 'LOW');
const buildRaw = e.build ?? e.buildable ?? e.buildableType;
const build = buildRaw == null ? base.build : (buildRaw === true ? 'ALL' : buildRaw === false ? 'NONE' : String(buildRaw).toUpperCase());
const build = DEPLOY_REFUSED_TILES.has(key) ? 'NONE'
: buildRaw == null ? base.build : (buildRaw === true ? 'ALL' : buildRaw === false ? 'NONE' : String(buildRaw).toUpperCase());
let pass;
if (e.pass != null) pass = normPass(e.pass);
else if (e.passable != null || e.passableMask != null) pass = normPass(e.passable ?? e.passableMask);
@@ -171,11 +190,17 @@ export class Grid {
flyPassable(r, c) { return this.inRect(r, c) && this.tile(r, c).pass !== 'NONE'; }
/** Whether a unit may stand on this tile. `ranged` units may also use LOW ALL/MELEE tiles (DESIGN §3). */
/**
* Whether a unit may be placed on this tile (every automatic placement: the 突袭 landing tile, tactical points, summon
* tiles). `ranged` units may also use LOW ALL/MELEE tiles (DESIGN §3). The effective `build` refuses the 深水区
* (DEPLOY_REFUSED_TILES); a hard-blocked tile (OB_BLOCK: a 射击台 switched on by a map card, a mound) takes no melee
* unit — the match's deploy map makes a 射击台 ranged-only (server/match/board.js).
*/
canStand(r, c, { ranged = false } = {}) {
const t = this.tile(r, c);
if (t.build === 'NONE') return false;
if (ranged) return t.build === 'ALL' || t.build === 'RANGED' || (t.height === 'LOW' && t.build === 'MELEE');
if (this.inBounds(r, c) && (this.obstacle[r * COLS + c] & OB_BLOCK)) return false;
return t.height === 'LOW' && (t.build === 'ALL' || t.build === 'MELEE');
}