feedback3 WJ: 高台 only for elite 歌蕾蒂娅 with HOK-Y

The trait line is on every hookmaster and is not trusted. Placement is the loadout at place time: golden 歌蕾蒂娅 carrying uniequip_003_glady may use a ranged tile; everyone else stays on the ground.
This commit is contained in:
sganggs committed 2026-10-04 20:31:46 +08:00
1 parent d5b945f14b
commit 5ef4fac137
23 files changed
+251 -174

No files matched your search

+3 -2
View File
@@ -72,7 +72,7 @@
import { ERR, GEO, PHASE, layerGainRoom } from '../../shared/constants.js';
import { checkLoadout, resolveLoadout } from '../../shared/protocol.js';
import { FIELD, tileKey, parseKey, inField, canPlace, positionClass, boardOrder, freeSlot, pieceDir, parseDir, mergeTile, ownerRangeKeys } from './board.js';
import { FIELD, tileKey, parseKey, inField, canPlace, placeClass, boardOrder, freeSlot, pieceDir, parseDir, mergeTile, ownerRangeKeys } from './board.js';
import { attackRangeGrid, loadoutRecord, resolveRecordLoadout } from '../../shared/loadoutRecord.js';
import { offsetTile } from '../sim/dir.js';
import { computeBonds, bondList, bondSnapshot, activatedLayers, bondsWithGains } from './bondsMeta.js';
@@ -1005,7 +1005,8 @@ export class PlayerState {
_placementOf(piece) {
const rec = piece.kind === 'token' ? this.gd.token(piece.id) : this.gd.chess(piece.id);
return positionClass(rec);
// elite 歌蕾蒂娅 + HOK-Y may use a 高台; the module is this player's loadout (owner's decision 2026-10-04)
return placeClass(this, rec);
}
/**
+2 -2
View File
@@ -42,7 +42,7 @@
import { PHASE } from '../../shared/constants.js';
import { collectViolations } from './invariants.js';
import { mergeTile, pieceDir, canPlace, positionClass } from './board.js';
import { mergeTile, pieceDir, canPlace, placeClass } from './board.js';
import { pairPlayers, bossPoolHp, hiddenEligible } from './finalAssault.js';
import { helperOrder } from './unite.js';
import { BAND_TURN_SECONDS } from './Match.js';
@@ -243,7 +243,7 @@ export function attachAudit(m, { invariants = true, limit = 200 } = {}) {
if (ps.deployCount > deployed0) fail(`${id}: a merge of ${baseId} grew the deploy count ${deployed0} → ${ps.deployCount}`);
// a pure read of the deploy field (Match.deployMapFor, as invariants.js): the audit must not refresh the cache
const dmap = typeof m.deployMapFor === 'function' ? m.deployMapFor(ps) : ps.deployMap();
const pos = positionClass(gd.chess(elite.id));
const pos = placeClass(ps, gd.chess(elite.id));
const want = mergeTile([...tiles.keys()].map((key) => ({ key })), (r, c) => canPlace(dmap, pos, r, c));
if (want) {
if (loc.area !== 'board' || loc.key !== want.key) fail(`${id}: the elite of ${baseId} went to ${loc.area} ${loc.key || ''}, expected the deployed copy's tile ${want.key}`);
+31 -10
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 the
// 钩索师 / 推击手, placement 'all' below).
// 'ranged' — HIGH tile with buildable ALL/RANGED, or a tile under an active platform (射击台): ranged only (and elite
// 歌蕾蒂娅 carrying HOK-Y, placeClass 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,9 +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 a MELEE operator whose branch
// trait reads "可以放置于远程位" (钩索师, 推击手: chess.json `placement` 'all' ⇒ any deployable tile, `positionClass`;
// GitHub issue #32 item 4, DESIGN §22.6 [ASSUMED]: the branch trait read as no 部署效果, which the mode would switch off).
// 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.
// 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
@@ -35,6 +36,7 @@
// `parseDir` validates an intent's optional direction.
import { GEO } from '../../shared/constants.js';
import { meleeOnHighGround } from '../../shared/highGround.js';
import { DEFAULT_DIR, isDir, rotateOffset } from '../sim/dir.js';
import { BOSS_ROW_OFFSET, COLS } from '../sim/constants.js';
@@ -133,12 +135,31 @@ export function buildDeployMap(stage, { deviceOverrides = {}, tileOverrides = {}
}
/**
* 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.
* 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.
* @param {object|null} rec
* @param {string|null} [moduleId] equipped module; ignored for tokens and non-golden records
*/
export function positionClass(rec) {
return rec && rec.placement === 'all' ? 'all' : basePositionClass(rec);
export function positionClass(rec, moduleId = null) {
return meleeOnHighGround(rec, moduleId) ? '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.
* @param {{ loadoutFor?: (rec: object) => { moduleId?: string|null }|null }|null} ps
*/
export function placeClass(ps, rec) {
if (!rec || !rec.chessId || !ps || typeof ps.loadoutFor !== 'function') return positionClass(rec);
let moduleId = null;
try {
const lo = ps.loadoutFor(rec);
moduleId = lo && typeof lo.moduleId === 'string' ? lo.moduleId : null;
} catch {
moduleId = null;
}
return positionClass(rec, moduleId);
}
/**
+8 -7
View File
@@ -33,9 +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 深水区; 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
// (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
// 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
@@ -77,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, basePositionClass, parseKey, tileKey, FIELD, pieceDir, boardTileOf, BOSS_MIRROR_COL } from './board.js';
import { freeSlot, countFree, legalTiles, canPlace, positionClass, placeClass, 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';
@@ -276,7 +276,7 @@ function tacticScore(m, ps, card) {
const chessRec = (m, id) => m.gd.chess(id);
const isHealer = (c) => !!c && (c.dmgType === 'heal' || c.attackKind === 'heal');
// the record's own position: a 钩索师 / 推击手 (placement 'all', DESIGN §22.6) is a MELEE blocker like any other
// the record's own position: a 钩索师 / 推击手 is a MELEE blocker like any other (a 高台 does not block)
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);
@@ -949,8 +949,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;
// 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))) {
// ground tiles for a MELEE blocker; elite 歌蕾蒂娅 with HOK-Y may also take a 高台 (everyone else must not)
const cls = p.kind === 'token' ? basePositionClass(r0) : placeClass(ps, m.gd.chess(p.id) || r0);
for (const [r, c] of legalTiles(map, cls)) {
const k = tileKey(r, c);
if (taken.has(k) || (within && !within.has(k))) continue;
const noise = m.rngBots() * 1e-6;
+3 -2
View File
@@ -17,7 +17,7 @@
// match phase known; teamLp / boss pool within range; combat fields match the alive players
import { PHASE, BOND_LAYER_CAP } from '../../shared/constants.js';
import { FIELD, canPlace, positionClass, parseKey } from './board.js';
import { FIELD, canPlace, placeClass, positionClass, parseKey } from './board.js';
import { computeBonds } from './bondsMeta.js';
const PHASES = new Set(Object.values(PHASE));
@@ -111,7 +111,8 @@ export function collectViolations(m, { limit = 25 } = {}) {
if (!(r >= FIELD.r0 && r <= FIELD.r1 && c >= FIELD.c0 && c <= FIELD.c1)) fail(`${id}: piece outside the board at ${k}`);
if (p.kind === 'item') { fail(`${id}: item ${p.id} stands on the board`); continue; }
const rec = p.kind === 'token' ? gd.token(p.id) : gd.chess(p.id);
if (rec && !canPlace(dmap, positionClass(rec), r, c)) fail(`${id}: ${p.id} on an illegal tile ${k}`);
const cls = p.kind === 'chess' ? placeClass(ps, rec) : positionClass(rec);
if (rec && !canPlace(dmap, cls, r, c)) fail(`${id}: ${p.id} on an illegal tile ${k}`);
// a "只能部署在召唤者攻击范围内" summon inside its owner's attack range (PlayerState.summonRange: a pure read)
const range = p.kind === 'token' && typeof ps.summonRange === 'function' ? ps.summonRange(p) : null;
if (range && !range.has(k)) fail(`${id}: ${p.id} on ${k}, outside its owner's attack range`);