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:
1 parent
d5b945f14b
commit
5ef4fac137
23 files changed
+251
-174
No files matched your search
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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`);
|
||||
|
||||
Reference in new issue
Block a user