0.2.6-dev: 魂灵之影 only on a board of its field (#557, PR #558 by IceCodeNew adapted) — shadowTile requires Battle.onFieldBoard: a teammate's half counts, a boss field's hand / 临时整备区 rows and a solo boss field's empty half do not, no summon when nothing fits (the PR cut the bench rows by number, which left an empty half legal); the PR's test plus a solo / pair case; DESIGN §3, §31.10, SIM; golden: unchanged

This commit is contained in:
sganggs committed 2026-10-13 16:04:32 +08:00
1 parent 1df530f2b9
commit 347343d739
6 files changed
+71 -3

No files matched your search

+3
View File
@@ -9,6 +9,9 @@
- 流量统计:`/healthz?traffic=1` 显示服务器发出的各类流量(只对本机和局域网的访问显示);「问题反馈」的诊断信息增加本页下载量。
- 部署指南:固定访问地址、反代开 HTTP/2、Cloudflare 缓存规则。
### 规则(按官方)
- 维什戴尔的魂灵之影只召唤到场上的部署格(含队友的半场):首领战中不会再落到整备区,单人首领战也不会落到空着的另一半场;范围内没有空位时不召唤(#557、PR #558,感谢 @IceCodeNew)。
### 界面
- 官方战场场景的光照改回提取出的官方值:直射光和烘焙光不再被压暗,场景不再明显偏暗(#549,PR #554,感谢 @xcdoge)。
- 最后一个还没选策略的玩家不再看到可点的「跳过」按钮:已经选完的玩家仍显示在顺序里,但不算可以让过去的人(PR #546,感谢 @Xinglan233)。
+4
View File
@@ -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.
+1 -1
View File
@@ -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).
+23
View File
@@ -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.
+4 -2
View File
@@ -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]; }
}
+36
View File
@@ -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);