feedback5 WE2: 重构体 only on the ground inside Mon3tr's range, 战术锚点 only on a 高台 outside 凯尔希·思衡托's (#26)
Mon3tr T1 "可以在攻击范围内的地面使用…重构体"; PRTS 战术锚点 特性 "仅可以部署在凯尔希·思衡托攻击 范围外的远程位". tools/build-data.mjs summonRecord: ownerRange also from an owner talent naming the token, ownerRangeOutside / rangedTilesOnly from the token text. Regenerated offline: 3 paths (重构体 ownerRange true; 战术锚点 ownerRangeOutside, rangedTilesOnly). Match: board.js placement class 'high'; PlayerState.summonExcluded in _legal, the re-orientation lift (+ toast), _roomForOutOfRange, the invariants and the bot; the client mirror (piecePosition HIGH, summonExcluded, reasons; English strings added). Golden: no scenario moved.
This commit is contained in:
1 parent
708719f382
commit
53ee0a1c6f
15 files changed
+236
-45
No files matched your search
+11
-6
@@ -27,11 +27,13 @@
|
||||
// 「可以放置于远程位」 — 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).
|
||||
// Tokens follow their own `position` (ALL ⇒ any deployable tile, MELEE ⇒ melee tiles, RANGED ⇒ any deployable), except
|
||||
// a token marked `rangedTilesOnly` ("仅可以部署在…远程位": 凯尔希·思衡托's 战术锚点) — class 'high', the ranged tiles only.
|
||||
// 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
|
||||
// grid (loadout-resolved, shared/loadoutRecord.js attackRangeGrid) rotated by its facing around its board tile
|
||||
// (PlayerState._legal; player report #9 after 0.1.0).
|
||||
// 缪尔赛思's 流形; PRTS 狼群 特性; Mon3tr's 重构体 by her talent) also needs a tile of its owner's attack range:
|
||||
// `ownerRangeKeys` = the owner's range grid (loadout-resolved, shared/loadoutRecord.js attackRangeGrid) rotated by its
|
||||
// facing around its board tile (PlayerState._legal; player report #9 after 0.1.0); one marked `ownerRangeOutside`
|
||||
// ("…攻击范围外": the 战术锚点) a tile outside that range.
|
||||
// Facing: board pieces carry `dir` ∈ UP|RIGHT|DOWN|LEFT (server/sim/dir.js); `pieceDir` reads it (absent ⇒ RIGHT),
|
||||
// `parseDir` validates an intent's optional direction.
|
||||
|
||||
@@ -135,12 +137,14 @@ export function buildDeployMap(stage, { deviceOverrides = {}, tileOverrides = {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Placement class of a chess / token record: 'melee' | 'ranged' | 'all'. A MELEE chess whose trait reads
|
||||
* Placement class of a chess / token record: 'melee' | 'ranged' | 'all' | 'high'. 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.
|
||||
* stand on the ranged (高台) tiles. Every other MELEE record stays 'melee'. A token marked `rangedTilesOnly` (tokens
|
||||
* "仅可以部署在…远程位": 战术锚点) is 'high': the ranged tiles only. `chess.json` has no `placement` field.
|
||||
* @param {object|null} rec
|
||||
*/
|
||||
export function positionClass(rec) {
|
||||
if (rec && rec.rangedTilesOnly === true) return 'high';
|
||||
return meleeOnHighGround(rec) ? 'all' : basePositionClass(rec);
|
||||
}
|
||||
|
||||
@@ -183,6 +187,7 @@ export function canPlace(map, pos, r, c) {
|
||||
const cls = map.get(tileKey(r, c));
|
||||
if (!cls) return false;
|
||||
if (pos === 'melee') return cls === 'melee';
|
||||
if (pos === 'high') return cls === 'ranged';
|
||||
return true; // ranged / all: melee tiles and ranged tiles
|
||||
}
|
||||
|
||||
|
||||
+4
-2
@@ -956,16 +956,18 @@ 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;
|
||||
// and an outside-bound one (凯尔希·思衡托's 战术锚点): never a tile of its owner's range
|
||||
const without = p.kind === 'token' && typeof ps.summonExcluded === 'function' ? ps.summonExcluded(p) : null;
|
||||
// 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 cls = p.kind === 'token' ? positionClass(r0) : placeClass(ps, pgd.chess(p.id) || r0);
|
||||
const preferHigh = cls === 'all' && basePositionClass(r0) === 'melee';
|
||||
let bestHigh = null;
|
||||
let bestHighV = -Infinity;
|
||||
for (const [r, c] of legalTiles(map, cls)) {
|
||||
const k = tileKey(r, c);
|
||||
if (taken.has(k) || (within && !within.has(k))) continue;
|
||||
if (taken.has(k) || (within && !within.has(k)) || (without && without.has(k))) continue;
|
||||
const noise = m.rngBots() * 1e-6;
|
||||
const seen = new Set();
|
||||
for (const dir of PLAN_DIRS) {
|
||||
|
||||
@@ -128,6 +128,9 @@ export function collectViolations(m, { limit = 25 } = {}) {
|
||||
// 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`);
|
||||
// an outside-bound summon (战术锚点, PlayerState.summonExcluded) outside it
|
||||
const out = p.kind === 'token' && typeof ps.summonExcluded === 'function' ? ps.summonExcluded(p) : null;
|
||||
if (out && out.has(k)) fail(`${id}: ${p.id} on ${k}, inside its owner's attack range`);
|
||||
if (p.kind === 'chess') deployed++;
|
||||
}
|
||||
if (deployed > ps.deployCap) fail(`${id}: ${deployed} chess deployed > cap ${ps.deployCap}`);
|
||||
|
||||
@@ -44,14 +44,18 @@ export class PlayerPlacement {
|
||||
|
||||
/**
|
||||
* Where piece may stand on (r, c): the deploy map of its position class (board.js canPlace) and, for a summon whose
|
||||
* text reads "只能部署在召唤者攻击范围内" (tokens.json `ownerRange`: 伺夜's 狼群, 缪尔赛思's 流形), a tile of its owner's
|
||||
* attack range (summonRange). `owner` = the owner's position after the move being checked ({ key, piece, dir }: a
|
||||
* summon swapped with its own owner).
|
||||
* text reads "只能部署在召唤者攻击范围内" (tokens.json `ownerRange`: 伺夜's 狼群, 缪尔赛思's 流形, Mon3tr's 重构体), a tile of
|
||||
* its owner's attack range (summonRange); for one marked `ownerRangeOutside` (凯尔希·思衡托's 战术锚点), a tile outside it
|
||||
* (summonExcluded). `owner` = the owner's position after the move being checked ({ key, piece, dir }: a summon swapped
|
||||
* with its own owner).
|
||||
*/
|
||||
_legal(piece, r, c, owner = null) {
|
||||
if (!canPlace(this.deployMap(), this._placementOf(piece), r, c)) return false;
|
||||
const k = tileKey(r, c);
|
||||
const range = this.summonRange(piece, owner);
|
||||
return !range || range.has(tileKey(r, c));
|
||||
if (range && !range.has(k)) return false;
|
||||
const out = this.summonExcluded(piece, owner);
|
||||
return !out || !out.has(k);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,6 +70,24 @@ export class PlayerPlacement {
|
||||
*/
|
||||
summonRange(piece, owner = null) {
|
||||
if (!piece || piece.kind !== 'token' || this.gd.token(piece.id)?.ownerRange !== true) return null;
|
||||
return this._ownerRangeOf(piece, owner);
|
||||
}
|
||||
|
||||
/**
|
||||
* The 'r,c' keys an outside-bound summon may NOT stand on — its owner's attack range (tokens `ownerRangeOutside`: PRTS
|
||||
* 战术锚点 "仅可以部署在凯尔希·思衡托攻击范围外的远程位"; 0.2.0 WE2) — or null (not outside-bound, or the owner is off the
|
||||
* board). Same range as summonRange.
|
||||
* @param {any} piece
|
||||
* @param {{ key: string, piece: any, dir: string } | null} [owner]
|
||||
* @returns {Set<string> | null}
|
||||
*/
|
||||
summonExcluded(piece, owner = null) {
|
||||
if (!piece || piece.kind !== 'token' || this.gd.token(piece.id)?.ownerRangeOutside !== true) return null;
|
||||
return this._ownerRangeOf(piece, owner);
|
||||
}
|
||||
|
||||
/** The owner's attack-range keys of a summon piece (summonRange / summonExcluded), or null when its owner is off the board. */
|
||||
_ownerRangeOf(piece, owner = null) {
|
||||
let at = owner;
|
||||
if (!at) for (const [key, p] of this.board) if (p.uid === piece.ownerUid && p.kind === 'chess') { at = { key, piece: p, dir: pieceDir(p) }; break; }
|
||||
const chess = at && this.gd.chess(at.piece.id);
|
||||
@@ -86,17 +108,23 @@ export class PlayerPlacement {
|
||||
* battle. Returns the number taken off the board; a toast names them.
|
||||
*/
|
||||
_liftOutOfRange() {
|
||||
const back = [], gone = [];
|
||||
const back = [], gone = [], backOut = [], goneOut = [];
|
||||
for (const [k, p] of [...this.board]) {
|
||||
if (p.kind !== 'token') continue;
|
||||
const range = this.summonRange(p);
|
||||
if (!range || range.has(k)) continue;
|
||||
const out = this.summonExcluded(p);
|
||||
const inside = !!(out && out.has(k));
|
||||
if (!inside && (!range || range.has(k))) continue;
|
||||
this.board.delete(k);
|
||||
(this._returnToken(p, null, { allowTemp: true }) ? back : gone).push(this.gd.token(p.id)?.name || p.id);
|
||||
const ok = this._returnToken(p, null, { allowTemp: true });
|
||||
(inside ? (ok ? backOut : goneOut) : (ok ? back : gone)).push(this.gd.token(p.id)?.name || p.id);
|
||||
}
|
||||
if (back.length) this.m.toast(this, 'warn', msg('{names}只能部署在召唤者攻击范围内,已退回整备区', { names: back.map(dn) }));
|
||||
if (gone.length) this.m.toast(this, 'warn', msg('{names}只能部署在召唤者攻击范围内,整备区已满,下回合返还', { names: gone.map(dn) }));
|
||||
return back.length + gone.length;
|
||||
// an outside-bound summon (战术锚点) its owner's new range now covers
|
||||
if (backOut.length) this.m.toast(this, 'warn', msg('{names}只能部署在召唤者攻击范围外,已退回整备区', { names: backOut.map(dn) }));
|
||||
if (goneOut.length) this.m.toast(this, 'warn', msg('{names}只能部署在召唤者攻击范围外,整备区已满,下回合返还', { names: goneOut.map(dn) }));
|
||||
return back.length + gone.length + backOut.length + goneOut.length;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -111,7 +139,8 @@ export class PlayerPlacement {
|
||||
for (const [k, p] of this.board) {
|
||||
if (p.kind !== 'token' || p.ownerUid !== owner.uid || stacks.some((s) => s.id === p.id)) continue;
|
||||
const range = this.summonRange(p, at);
|
||||
if (range && !range.has(k)) needSlot.add(p.id);
|
||||
const out = this.summonExcluded(p, at);
|
||||
if ((range && !range.has(k)) || (out && out.has(k))) needSlot.add(p.id);
|
||||
}
|
||||
return needSlot.size <= [...this.hand, ...this.temp].filter((p) => p == null).length;
|
||||
}
|
||||
|
||||
@@ -68,8 +68,8 @@
|
||||
// the move be refused, she comes back on her own tile [ASSUMED; officially she is knocked out].
|
||||
// At the skill's end any anchor of hers on the field is withdrawn and the stock emptied (token_ctrl). The flight's begin
|
||||
// clip and its 生命回复速度 ×0 are not modelled [ASSUMED].
|
||||
// - 战术锚点 (token_10068_kalts2_mtship, placed in prep — "仅可以部署在凯尔希·思衡托攻击范围外的远程位" is the match's
|
||||
// placement rule): 无敌 and never targeted ("不会受到攻击"), no attack, blocks nothing; its range (hers) is only shown.
|
||||
// - 战术锚点 (token_10068_kalts2_mtship, placed in prep on a ranged tile outside her attack range — "仅可以部署在凯尔希·思衡托
|
||||
// 攻击范围外的远程位": the data's `ownerRangeOutside` / `rangedTilesOnly`, enforced by the match, 0.2.0 WE2): 无敌 and never targeted ("不会受到攻击"), no attack, blocks nothing; its range (hers) is only shown.
|
||||
// A placed piece is docked by content/tokens.js (a skill's summon: deployed for free at the battle start, then only when
|
||||
// her S3 gives one and its 70 s redeploy time has passed, on its own tile).
|
||||
|
||||
|
||||
@@ -34,9 +34,9 @@
|
||||
// stage 3: the next attack@chain.extra_cnt jumps after that neither). Her heals pass 禁疗 ("治疗效果无视禁疗"); her selection
|
||||
// takes her own 重构体 through its 禁疗 (`healThrough`), no other 禁疗 unit [ASSUMED: the jumps too].
|
||||
// - Module XAH-X 记忆存档: the trait's 0.85 per jump (traitOverride); stage 3 changes T1 (below); ATK / DEF from the attributes.
|
||||
// - T1 自我修复 (cnt 1): her 重构体 is a hand piece the player places (the talent's summon). "可以在攻击范围内的地面使用" — a
|
||||
// ground tile of her attack range — is not in the token data (`ownerRange` false), so the match does not hold the piece to
|
||||
// it and the battle takes it where it stands [open question]. Its kit: 禁疗 (only her heals reach it), blocks nothing, taunt
|
||||
// - T1 自我修复 (cnt 1): her 重构体 is a hand piece the player places (the talent's summon) on a ground tile of her attack
|
||||
// range — "可以在攻击范围内的地面使用": the data's `ownerRange` from that talent text (tools/build-data.mjs summonRecord,
|
||||
// 0.2.0 WE2) and its MELEE position, enforced by the match. Its kit: 禁疗 (only her heals reach it), blocks nothing, taunt
|
||||
// −1, loses 8 HP every 0.1 s from 1 s after it lands (流失, Battle.loseHp — PRTS "每0.1s流失8生命值"; trait damage_per_second
|
||||
// 80); every ally of its
|
||||
// 3×3 but itself (no 孤立 one) ATK +atk (0.15; XAH-X stage 3 0.25; 同名效果取最高). Gone (destroyed or withdrawn) ⇒ back on its
|
||||
|
||||
Reference in new issue
Block a user