feedback5: merge W5M — the nine 0.1.4 PRs (#150 #66 #146 #74 #72 #103 #149 #109 #160) and W4A (高台 per the trait, 阿戈尔 revive slots, 联防 devour) ported into the split layout

Conflicts: server/match/bot.js (W4A's comment + W5K's player data view pgd), test/golden/diy.json (regenerated). data/ equals an offline build of the merged tools/build-data.mjs. Golden regenerated on the merged code: against feedback5 the moved roster / bonds / fields / matches / standins scenarios are exactly W5M's per-PR attribution (#160 skill events, #109 deploy-timed lifecycle, #66 灵巧 sources and hidden layers, 83fc408 歌蕾蒂娅 placement), plus coop2-NORMAL-70-diy (fields operators those PRs touch) and diy-051 / diy-052 (events.skill only: Misery's deploy passives, #160); against fb5-merge4 only feedback5's own additions (the diy kits, coop2-NORMAL-70-diy).
This commit is contained in:
sganggs committed 2026-10-06 00:33:15 +08:00
commit 9ce5435373
74 files changed
+2966 -1338

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
@@ -954,8 +956,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, pgd.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);
}