diff --git a/CHANGELOG.md b/CHANGELOG.md index 46d8f28f..90a052d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ - 流量统计:`/healthz?traffic=1` 显示服务器发出的各类流量(只对本机和局域网的访问显示);「问题反馈」的诊断信息增加本页下载量。 - 部署指南:固定访问地址、反代开 HTTP/2、Cloudflare 缓存规则。 +### 规则(按官方) +- 维什戴尔的魂灵之影只召唤到场上的部署格(含队友的半场):首领战中不会再落到整备区,单人首领战也不会落到空着的另一半场;范围内没有空位时不召唤(#557、PR #558,感谢 @IceCodeNew)。 + ### 界面 - 官方战场场景的光照改回提取出的官方值:直射光和烘焙光不再被压暗,场景不再明显偏暗(#549,PR #554,感谢 @xcdoge)。 - 最后一个还没选策略的玩家不再看到可点的「跳过」按钮:已经选完的玩家仍显示在顺序里,但不算可以让过去的人(PR #546,感谢 @Xinglan233)。 diff --git a/docs/SIM.md b/docs/SIM.md index 11c06075..757c5d6d 100644 --- a/docs/SIM.md +++ b/docs/SIM.md @@ -1617,3 +1617,7 @@ general hook guard. The queue is emptied in `finally`; it introduces no timer or - Healing skill activation includes elemental-only injury when the operator's heal profile can cure it. Pinecone S2 growth is deployment-local. Skadi S1 and Stead preserve redirect ancestry and take both shares from the pre-redirect hit, so the total damage is conserved. `champagneHold` includes the internal `isolated` ally-selection flag (§29.11): device-type champagne bombs are excluded from ordinary allied auras/inspiration in both creation paths. Other unhealable summons retain their own selection behavior. + +维什戴尔's `shadowTile` requires `Battle.onFieldBoard` before ranking summon candidates: a board of this field (a +teammate's half), never a boss field's hand / 临时整备区 rows or an empty half (§31.10). `grid.inRect` alone also admits +those bench rows. See [DESIGN §3](design/geometry-time.md#3-coordinates-fields-and-geometry) for the deployment boundary. diff --git a/docs/design/geometry-time.md b/docs/design/geometry-time.md index e209be2c..f092e3a7 100644 --- a/docs/design/geometry-time.md +++ b/docs/design/geometry-time.md @@ -9,7 +9,7 @@ Part of [DESIGN.md](../DESIGN.md) (the index; section numbers are global). - **Normal field** (own board): rows 9–12, cols 2–10. Enemy gates `S` at (9,10) and (12,10) (R1–3 only the lower one); protection objective `E` at (9,2). Col 9 lane is not deployable (tile type decides). - **Hand (整备区)**: row 7, cols 0–9 (10 regular slots, index = col). **Temp slots**: row 8, cols 4–8 (5 slots). Hand/temp tiles are never part of a battle. - **Partner display** cols 11–18 of rows 9–12 (the unite field uses them). - - **Boss field**: rows 1–5; left half cols 2–10, right half cols 10–18. + - **Boss field**: combat deployment rows 2–5; left half cols 2–10, right half cols 10–18. The mapped hand / temp rows are 0 / 1, inside the simulation rect but not deployment tiles (`Battle.onFieldBoard` excludes them): 维什戴尔's 魂灵之影 goes only on a board of the field — a teammate's half, never those rows or an empty half — and without a free one in range no shadow is summoned (§31.10). - Enemy preview pen rows 14–18 × cols 7–13 (row 16 empty): during prep the next round's enemies idle there (upper-gate enemies rows 17–18, lower-gate rows 14–15, ≤ 50 models, spawn-time order) — research 08 §4 / 09 §2. - Tile semantics come from `data/stages.json` legend: height (LOW/HIGH), buildable (ALL/MELEE/RANGED/NONE — the **effective** deploy type: the 深水区 `tile_deepsea` refuses deployment, NONE with the level's `buildableType` ALL kept, PRTS 深水区 地形信息 "拒绝部署(待补充)"; an active 特制水上平台 makes its tile deployable in the prep deploy map only, §21.3), ground-passable, fly-only, special terrain (mire, smog, deepsea, infection). The fenced tiles (围墙 `tile_fence_bound` / 围栏 `tile_fence`, legend `b`: LOW, buildable ALL, passable to flyers only) take units, which attack normally from there but block no ground enemy (PRTS 地形机制, §21.23). Melee chess may stand only on LOW tiles with buildable ALL/MELEE — except a melee chess whose trait reads 「可以放置于远程位」 (the 钩索师 / 推击手 branch trait: 歌蕾蒂娅, 崖心, 见行者, normal and elite, with any module or none), which may use the ranged tiles too (`shared/highGround.js`; the owner's decision of 2026-10-05, following PRTS — it reverses the 2026-10-04 one that allowed only elite 歌蕾蒂娅 with HOK-Y; on a 高台 it attacks and blocks nothing); ranged chess on ALL/RANGED **and** on LOW ALL/MELEE tiles ("所有行动内远程干员可部署在近战位"). Tokens follow their own `position`; a summon marked `ownerRange` (狼群, 流形: "只能部署在召唤者攻击范围内"; Mon3tr's 重构体 by her talent) goes only on a tile of its owner's attack range — the loadout grid rotated by the owner's facing around its tile (`PlayerState.summonRange`, §21.3) —, one marked `ownerRangeOutside` / `rangedTilesOnly` (凯尔希·思衡托's 战术锚点: "仅可以部署在凯尔希·思衡托攻击范围外的远程位") only on a 高台 outside it (`summonExcluded`, placement class `high`; 0.2.0). - **Facing (corrected, see research 09 §1.2):** like standard Arknights, every board piece has a direction `dir ∈ UP|RIGHT|DOWN|LEFT` chosen with the 4-direction deploy wheel after dropping it on a tile (also re-orientable in place); default `RIGHT`; persists across rounds. Range grid entries are `[dRow, dCol]` relative to facing RIGHT and are rotated: RIGHT `(dr,dc)`, UP `(dc,−dr)`, LEFT `(−dr,−dc)`, DOWN `(−dc,dr)` (row 0 = bottom); `rangeExtend` applies along +dCol before rotating — to a running skill's range too, unless that skill ignores 攻击距离 (`targeting.noRangeExtend`, 信仰搅拌机 S3, §21.16). The right-side Final Assault player is mirrored col `c→20−c` with RIGHT↔LEFT (UP/DOWN unchanged). diff --git a/docs/history/0.2.6.md b/docs/history/0.2.6.md index 2308a216..fa121f64 100644 --- a/docs/history/0.2.6.md +++ b/docs/history/0.2.6.md @@ -290,3 +290,26 @@ inlined the readout into `_updateHud`; here it is a method like the purse's (`_u spending, refills, capacity changes and dense magazines, hidden at the skill's end, in the prep view, after death and for enemies, the live toggle), `test/ui/ammo-count.test.js` (normalisation, the toggle and its hint in every language pack, the view's live setting, the diagnostics line), `test/ui/gameLogic.test.js` (sanitizeSettings). Golden: unchanged. + +### 31.10 魂灵之影 stays on the field's boards (#557, PR #558) + +Issue #557; PR #558 by @IceCodeNew, adapted. + +**Seen.** The boss / hidden simulation rect includes rows 0–1, the mapped hand and 临时整备区. `shadowTile` accepted any +standable tile of her range there, so a bottom-row 维什戴尔 summoned 魂灵之影 onto the bench through her deployment talent +or S3 when her nearer tiles were taken. + +**Now.** A candidate must also be on a board of this field (`Battle.onFieldBoard`, the "deployable" of the other summons +and of the 突袭 landing): a teammate's half on a pair field counts; a boss field's hand / 临时整备区 rows and the empty half +of a solo boss field do not. With no free candidate the summon is skipped. Range, occupancy and the order (nearest to her, +then the lower row, then the left column) are unchanged. The PR cut the bench rows by number; that left a standable tile +of an empty half legal, so the port uses the field's boards instead (the Raid-only empty-half exception, §29.13, stays +Raid's). + +Sources: [PRTS 维什戴尔](https://prts.wiki/w/维什戴尔), the talent's nearest / lower-row / left-column deployable-tile rule; +[issue #557](https://github.com/sganggs/Stronghold-Protocol/issues/557). No new [ASSUMED] rule. + +**Tests.** `test/content/op_wisdel.test.js`: the PR's case (boss and hidden fields, both sides, a full range and one free +combat tile, through the deployment talent and an actual S3 activation), and a solo / pair case — her own free tiles +filled, the empty half takes no shadow and the teammate's half does. Normative: DESIGN §3 +([geometry-time.md](../design/geometry-time.md)); API note: [SIM.md](../SIM.md). Golden: unchanged. diff --git a/server/sim/content/kits/ops/op-wisdel.js b/server/sim/content/kits/ops/op-wisdel.js index 01220201..ed6da457 100644 --- a/server/sim/content/kits/ops/op-wisdel.js +++ b/server/sim/content/kits/ops/op-wisdel.js @@ -133,12 +133,14 @@ function shocks({ count, atkScale, shockScale, radius, prob, stun, mainScale, bo }; } -/** The free deployable tile of her range a shadow takes (PRTS: nearest to her > lower row > left column), or null. */ +/** The free deployable tile of her range a shadow takes (PRTS: nearest to her > lower row > left column), or null. + * Deployable = on a board of this field (Battle.onFieldBoard): a teammate's half counts, a boss field's hand / + * 临时整备区 rows and an empty half do not (#557, PR #558). */ function shadowTile(battle, host) { let best = null, bd = Infinity; for (const k of host.rangeKeys ?? []) { const r = Math.floor(k / COLS), c = k % COLS; - if (!battle.grid.inRect(r, c) || !battle.grid.canStand(r, c, { ranged: true }) || battle.isReservedTile(r, c)) continue; + if (!battle.grid.inRect(r, c) || !battle.onFieldBoard(r, c) || !battle.grid.canStand(r, c, { ranged: true }) || battle.isReservedTile(r, c)) continue; const d = hypot(r - host.tileR, c - host.tileC); if (d < bd - 1e-9 || (Math.abs(d - bd) <= 1e-9 && (r < best[0] || (r === best[0] && c < best[1])))) { bd = d; best = [r, c]; } } diff --git a/test/content/op_wisdel.test.js b/test/content/op_wisdel.test.js index f1fc2046..8d18d800 100644 --- a/test/content/op_wisdel.test.js +++ b/test/content/op_wisdel.test.js @@ -184,6 +184,42 @@ test('T2 死魂灵的余息: a 魂灵之影 (the form\'s stats, 禁疗, no attac done(h); }); +test('user #557 (PR #558 by IceCodeNew): Given a bottom-row boss lineup, When the talent and S3 summon, Then shadows skip the bench and use only free combat tiles', () => { + for (const kind of ['boss', 'hidden']) for (const side of ['L', 'R']) for (const free of [false, true]) { + // Board (9, 7) becomes field (2, 7)/(2, 13). Row 1 is the temporary bench, inside BOSS_RECT. + const units = [{ uid: 1, chessId: SLOT[5], diy: { charId: WD, skillIndex: 2 }, row: 9, col: 7, dir: 'DOWN' }]; + for (let col = 5; col <= 9; col++) { + if (col === 7 || free && col === 8) continue; + units.push({ uid: col, chessId: 'chess_char_1_01_a', row: 9, col }); + } + const h = makeBattle({ kind, stageId: 'act2autochess_m02', players: [{ playerId: 'p1', side, units }], autoFinish: false }); + h.step(); + const u = h.unit(1); + const expected = free ? [[2, side === 'L' ? 8 : 12]] : []; + assert.deepEqual(shadowsOf(h, u).map(t => [t.tileR, t.tileC]), expected, `${kind}/${side}/${free}: deployment talent`); + assert.ok(h.runUntil(() => u.skill.activations === 1, 40), 'S3 actually activates without an enemy'); + assert.deepEqual(shadowsOf(h, u).map(t => [t.tileR, t.tileC]), expected, `${kind}/${side}/${free}: S3 cannot spill into the bench`); + done(h); + } +}); + +test('#557: a shadow may stand on the teammate\'s half of a pair field (Battle.onFieldBoard), never on the empty half of a solo boss field — no summon when nothing else fits', () => { + // board (10, 8) facing RIGHT = field (3, 8) on the left half / (3, 12) mirrored; her own free tiles of range filled, + // the bench tile (1, 8 / 12) and the other half's (2, 11 / 9) and (3, 12 / 8) stay in range + for (const kind of ['boss', 'hidden']) for (const side of ['L', 'R']) for (const pair of [false, true]) { + const units = [{ uid: 1, chessId: SLOT[6], diy: { charId: WD, skillIndex: 0 }, row: 10, col: 8, dir: 'RIGHT' }, + ...[[9, 8], [11, 8], [12, 8], [9, 9]].map(([row, col], i) => ({ uid: 2 + i, chessId: 'chess_char_1_01_a', row, col }))]; + const players = [{ playerId: 'p1', side, units }]; + if (pair) players.push({ playerId: 'p2', side: side === 'L' ? 'R' : 'L', units: [{ uid: 11, chessId: 'chess_char_1_01_a', row: 12, col: 2 }] }); + const h = makeBattle({ kind, stageId: 'act2autochess_m02', players, autoFinish: false }); + h.step(); + const u = h.unit(1); + const expected = pair ? [[2, side === 'L' ? 11 : 9]] : []; + assert.deepEqual(shadowsOf(h, u).map(t => [t.tileR, t.tileC]), expected, `${kind}/${side}/${pair ? 'pair' : 'solo'}`); + done(h); + } +}); + test('魂灵之影\'s skill: every 5 SP (time; +0–2 after each cast) a ground enemy of 维什戴尔\'s range — unmarked first — takes its ATK as arts damage, 停顿 1 s and her 残影; never an air unit', () => { for (const [tier, elite] of [[5, false], [6, true]]) { const v = shadowOf(tier, elite);