feedback2: merge WF — 钩索师 / 推击手 may stand on a 高台 (branch trait 可以放置于远程位; GitHub #32 item 4)

This commit is contained in:
sganggs committed 2026-10-03 23:39:47 +08:00
commit 2f5cc6edbc
13 files changed
+286 -21

No files matched your search

+19 -2
View File
@@ -4,7 +4,8 @@
// stage legend (data/stages.json `tiles[glyph]`: height + buildable) and the stage devices:
// 'melee' — LOW tile with buildable ALL/MELEE, or a 深水区 under an active 特制水上平台 (waterPlatform, "在水上建立可以
// 部署任意单位的平台"): melee AND ranged chess may stand here ("所有行动内远程干员可部署在近战位").
// 'ranged' — HIGH tile with buildable ALL/RANGED, or a tile under an active platform (射击台): ranged only.
// 'ranged' — HIGH tile with buildable ALL/RANGED, or a tile under an active platform (射击台): ranged only (and the
// 钩索师 / 推击手, placement 'all' below).
// (absent) — not deployable (NONE, forbidden, road lanes, tiles under active crates/mounds, the 深水区 — the legend's
// `buildable` is the effective type: tile_deepsea refuses deployment, PRTS 深水区 地形信息
// "拒绝部署(待补充)", although its level buildableType is ALL; player report #3 after 0.1.0, 战场#08's
@@ -22,6 +23,9 @@
// (10–12, 8), which are '#' on the normal field. The result equals stages[id].deployTiles.bossLeft / bossRight
// mapped to board coordinates (test/match/playtest5-deploy.test.js).
//
// Chess follow their `position` (MELEE ⇒ melee tiles, RANGED ⇒ any deployable), except a MELEE operator whose branch
// trait reads "可以放置于远程位" (钩索师, 推击手: chess.json `placement` 'all' ⇒ any deployable tile, `positionClass`;
// GitHub issue #32 item 4, DESIGN §22.6).
// Tokens follow their own `position` (ALL ⇒ any deployable tile, MELEE ⇒ melee tiles, RANGED ⇒ any deployable).
// A token whose text reads "只能部署在召唤者攻击范围内" (tokens.json `ownerRange`: the tacticians' 援军 — 伺夜's 狼群,
// 缪尔赛思's 流形; PRTS 狼群 特性) also needs a tile of its owner's attack range: `ownerRangeKeys` = the owner's range
@@ -128,8 +132,21 @@ export function buildDeployMap(stage, { deviceOverrides = {}, tileOverrides = {}
return map;
}
/** Placement class of a chess / token record: 'melee' | 'ranged' | 'all'. */
/**
* Placement class of a chess / token record: 'melee' | 'ranged' | 'all' — its position class, widened to 'all' for a
* chess whose branch trait reads "可以放置于远程位" (钩索师, 推击手: chess.json `placement` 'all', DESIGN §22.6), which may
* stand on the ranged (高台) tiles too.
*/
export function positionClass(rec) {
return rec && rec.placement === 'all' ? 'all' : basePositionClass(rec);
}
/**
* The record's own position class ('melee' | 'ranged' | 'all'), without the placement widening — the class the battle
* plays (a MELEE operator still blocks on a ground tile; on a 高台 it blocks nothing): the bots' blocker test and the
* tiles they plan for it (bot.js).
*/
export function basePositionClass(rec) {
const p = rec && typeof rec.position === 'string' ? rec.position.toUpperCase() : 'ALL';
if (p === 'MELEE') return 'melee';
if (p === 'RANGED') return 'ranged';
+8 -4
View File
@@ -33,7 +33,9 @@
// the ground path but count as flyers) and weighted by their enemies; an exposure model (tile time × DPS of the
// covering units against the round's DEF / RES, 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 of the server's deploy map (no 深水区): each unit's range grid — the one it is
// (legal tile, direction) pair of the server's deploy map (no 深水区; a 钩索师 / 推击手, which may also stand on
// a 高台 — placement 'all', DESIGN §22.6 —, is planned on the ground tiles like any blocker, as in 0.1.1
// [ASSUMED: the lineup counts it as a blocker]): each unit's range grid — the one it is
// deployed with, rangeRec (loadoutRecord attackRangeGrid) — 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
@@ -75,7 +77,7 @@
import { GEO } from '../../shared/constants.js';
import { deriveSeed } from '../sim/rng.js';
import { ASPD_MIN } from '../sim/constants.js';
import { freeSlot, countFree, legalTiles, canPlace, positionClass, parseKey, tileKey, FIELD, pieceDir, boardTileOf, BOSS_MIRROR_COL } from './board.js';
import { freeSlot, countFree, legalTiles, canPlace, positionClass, basePositionClass, 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';
@@ -274,7 +276,8 @@ function tacticScore(m, ps, card) {
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';
// the record's own position: a 钩索师 / 推击手 (placement 'all', DESIGN §22.6) is a MELEE blocker like any other
const isBlocker = (c) => !!c && basePositionClass(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';
@@ -946,7 +949,8 @@ export function* planLayoutSteps(m, ps, pieces, params = LAYOUT_PARAMS, { occupi
let bestV = -Infinity;
// a "只能部署在召唤者攻击范围内" summon (伺夜's 狼群, 缪尔赛思's 流形): only the tiles of its owner's range
const within = p.kind === 'token' && typeof ps.summonRange === 'function' ? ps.summonRange(p) : null;
for (const [r, c] of legalTiles(map, positionClass(r0))) {
// the record's own position class: a 钩索师 / 推击手 (placement 'all') stays on the ground tiles, where it blocks
for (const [r, c] of legalTiles(map, basePositionClass(r0))) {
const k = tileKey(r, c);
if (taken.has(k) || (within && !within.has(k))) continue;
const noise = m.rngBots() * 1e-6;