feedback5 WM: merge feedback4 at 13808e3 (W4A) — 高台 for every 「可以放置于远程位」 trait holder, the 5-阿戈尔 revive slots, the 联防 devour of whoever stands in front

Ported by hand into the split layout:
- 83fc408 board.js placeClass: the player's loadout no longer counts, as in 83fc408. feedback5's 补位 rule stays: a
  chess the player fields as its stand-in is placed by the stand-in's record (ps.fieldRecord). positionClass(rec)
  takes no module.
- 83fc408 client piecePosition → public/js/ui/gameLogic/placement.js: meleeOnHighGround(rec) on the stand-in or
  chess record, no module. The gameLogic.js shim header and deployedModuleId's comment follow; deployedModuleId is
  kept, still exported and tested.
- 83fc408 PlayerState._placementOf comment → server/match/player/placement.js.
- 83fc408 tools/golden.mjs layout: positionClass without the module, on feedback5's stand-in body (recOf) and in
  diyLayout.
- bonds/core.js (6ab9a06, 8a2e0c7) applied cleanly; it equals 13808e3's plus feedback5's i18n toast. shared/highGround.js
  equals 13808e3's.
- data/: only bonds.json (the 阿戈尔 spec text). node tools/build-data.mjs --offline on the merged tree rewrites all
  15 files byte-identical.

Golden regenerated: 16 scenarios moved, all by 83fc408. Each fields 歌蕾蒂娅 (normal, or elite with HOK-X / no
module), whom tools/golden.mjs now lays out beside the enemy path like a ranged operator instead of on the busiest
melee tile: roster-010, roster-012, roster-029, roster-040, roster-041; bond-deputShip-high, bond-indomShip-low,
bond-preciShip-low, bond-preciShip-high, bond-suntShip-low, bond-swiftShip-high; boss-boss_2-pair, boss-boss_4-pair,
boss-boss_6-pair, hidden-boss_8-pair, hidden-boss_10-pair. This is the list in 83fc408's own message. With the
阿戈尔 commits reverted the digests are the same, so those two move nothing in this corpus. The standins, diy and
matches families are unchanged: no stand-in or 自选 piece there is 歌蕾蒂娅, 崖心 or 见行者.
This commit is contained in:
sganggs committed 2026-10-06 00:06:41 +08:00
commit c5c52eed34
26 files changed
+1095 -871

No files matched your search

+26 -27
View File
@@ -4,8 +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 (and elite
// 歌蕾蒂娅 carrying HOK-Y, placeClass below).
// 'ranged' — HIGH tile with buildable ALL/RANGED, or a tile under an active platform (射击台): ranged only (and the
// melee chess whose trait reads 「可以放置于远程位」, positionClass 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
@@ -23,10 +23,10 @@
// (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 elite 歌蕾蒂娅 carrying module
// HOK-Y 淡金坠饰 (uniequip_003_glady: `placeClass` / shared/highGround.js ⇒ any deployable tile). The branch trait
// 「可以放置于远程位」 is not read (owner's decision 2026-10-04, reversing DESIGN §22.6): 崖心, 见行者, a normal record,
// any other module and no module are ground-only. The equipped module is the player's loadout at place time.
// Chess follow their `position` (MELEE ⇒ melee tiles, RANGED ⇒ any deployable), except a MELEE chess whose trait reads
// 「可以放置于远程位」 — the 钩索师 / 推击手 branch trait: 歌蕾蒂娅, 崖心, 见行者, normal and elite, any module or none
// (shared/highGround.js ⇒ any deployable tile; the owner's decision of 2026-10-05, following PRTS). The loadout does
// not change the class.
// 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
@@ -135,41 +135,40 @@ export function buildDeployMap(stage, { deviceOverrides = {}, tileOverrides = {}
}
/**
* Placement class of a chess / token record: 'melee' | 'ranged' | 'all'. Widened to 'all' only for elite 歌蕾蒂娅
* carrying HOK-Y (`moduleId` = uniequip_003_glady, shared/highGround.js): she may stand on the ranged (高台) tiles.
* Any other module, no module, and every other MELEE record stay 'melee'. `chess.json` has no `placement` field.
* Placement class of a chess / token record: 'melee' | 'ranged' | 'all'. A MELEE chess whose trait reads
* 「可以放置于远程位」 (shared/highGround.js: 歌蕾蒂娅, 崖心, 见行者, normal and elite) is widened to 'all': it may also
* stand on the ranged (高台) tiles. Every other MELEE record stays 'melee'. `chess.json` has no `placement` field.
* @param {object|null} rec
* @param {string|null} [moduleId] equipped module; ignored for tokens and non-golden records
*/
export function positionClass(rec, moduleId = null) {
return meleeOnHighGround(rec, moduleId) ? 'all' : basePositionClass(rec);
export function positionClass(rec) {
return meleeOnHighGround(rec) ? 'all' : basePositionClass(rec);
}
/**
* Placement class of a record under a player's loadout (the module `ps.loadoutFor` resolves, defaults included).
* Tokens and records without a chess id ignore the loadout. A chess the player fields as its stand-in (0.2.0 补位,
* `ps.fieldRecord`) is placed by the stand-in's position — the body that is deployed (圣约送葬人's 预备干员-先锋 stands on
* the ground, 塞雷娅's Touch anywhere) [ASSUMED: the official deploy check reads the fielded character].
* @param {{ loadoutFor?: (rec: object) => { moduleId?: string|null }|null, fieldRecord?: (rec: object) => object }|null} ps
* Placement class of a record on a player's board — the server's single entry point for pieces (g.move, swaps, merges,
* the audit, the invariants, the bot). Since the owner's decision of 2026-10-05 the player's loadout no longer changes
* it (any module, or none). A chess the player fields as its stand-in (0.2.0 补位, `ps.fieldRecord`) is placed by the
* stand-in's position — the body that is deployed (圣约送葬人's 预备干员-先锋 stands on the ground, 塞雷娅's Touch
* anywhere) [ASSUMED: the official deploy check reads the fielded character]; tokens and records without a chess id
* are their own body.
* @param {{ fieldRecord?: (rec: object) => object }|null} ps
* @param {object|null} rec
*/
export function placeClass(ps, rec) {
if (!rec || !rec.chessId || !ps || typeof ps.loadoutFor !== 'function') return positionClass(rec);
let moduleId = null;
let body = rec;
if (!rec || !rec.chessId || !ps || typeof ps.fieldRecord !== 'function') return positionClass(rec);
let body;
try {
const lo = ps.loadoutFor(rec);
moduleId = lo && typeof lo.moduleId === 'string' ? lo.moduleId : null;
if (typeof ps.fieldRecord === 'function') body = ps.fieldRecord(rec) || rec;
body = ps.fieldRecord(rec) || rec;
} catch {
moduleId = null;
body = rec;
}
return positionClass(body, moduleId);
return positionClass(body);
}
/**
* 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).
* plays (a MELEE operator still blocks on a ground tile; on a 高台 it blocks nothing): the bots' blocker test and their
* 高台 preference for a widened melee chess (bot.js isBlocker / planLayoutSteps).
*/
export function basePositionClass(rec) {
const p = rec && typeof rec.position === 'string' ? rec.position.toUpperCase() : 'ALL';
+7 -4
View File
@@ -34,8 +34,10 @@
// 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 深水区). A MELEE operator is planned on the ground
// tiles, where it blocks — except elite 歌蕾蒂娅 carrying HOK-Y, who may also take a 高台 (placeClass; owner's
// decision 2026-10-04). She stays a blocker in the lineup (basePositionClass). Each unit's range grid — the one it is
// tiles, where it blocks — except one whose trait reads 「可以放置于远程位」 (歌蕾蒂娅, 崖心, 见行者, any module:
// placeClass 'all', shared/highGround.js), which takes a 高台 whose range covers the enemy road, else the ground
// (owner 2026-10-04 for the 高台 preference, 2026-10-05 for who may use one). It stays a blocker in the lineup
// (basePositionClass). 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
@@ -951,8 +953,9 @@ 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;
// ground tiles for a MELEE blocker. placeClass 'all' is only elite 歌蕾蒂娅 + HOK-Y: she may stand on a 高台,
// and when one of those tiles covers the enemy road she is planned there (owner 2026-10-04: the bot uses the 高台).
// ground tiles for a MELEE blocker. placeClass 'all' on a MELEE record is a chess whose trait reads 「可以放置于远程位」
// (歌蕾蒂娅, 崖心, 见行者, any module): it may stand on a 高台, and when one of those tiles covers the enemy road it is
// planned there even with the ground free (owner 2026-10-04: the bot uses the 高台; 2026-10-05: every such chess).
const cls = p.kind === 'token' ? basePositionClass(r0) : placeClass(ps, m.gd.chess(p.id) || r0);
const preferHigh = cls === 'all' && basePositionClass(r0) === 'melee';
let bestHigh = null;
+2 -1
View File
@@ -37,7 +37,8 @@ export class PlayerPlacement {
_placementOf(piece) {
const rec = piece.kind === 'token' ? this.gd.token(piece.id) : this.gd.chess(piece.id);
// elite 歌蕾蒂娅 + HOK-Y may use a 高台; the module is this player's loadout (owner's decision 2026-10-04)
// a melee chess whose trait reads 「可以放置于远程位」 may use a 高台, whatever its module (shared/highGround.js); a
// stand-in is placed by its own record (placeClass)
return placeClass(this, rec);
}