From a1d118a01f437945ce0b0cae83912efaae46971e Mon Sep 17 00:00:00 2001 From: sganggs Date: Wed, 7 Oct 2026 02:18:36 +0800 Subject: [PATCH] =?UTF-8?q?feedback5=20CB1:=20the=20=E7=82=8E=E4=BD=91=20n?= =?UTF-8?q?o=20longer=20spawns=20on=20=E5=A4=96=E5=8B=A4=E5=8C=BB=E7=96=97?= =?UTF-8?q?'s=20map-character=20slot=20=E2=80=94=20the=20medic=20/=20Touch?= =?UTF-8?q?=20stands=20beside=20it=20(community=20report=20=E3=80=8CTouch?= =?UTF-8?q?=E7=AD=96=E7=95=A5=E7=BB=99=E7=9A=84=E5=8C=BB=E7=96=97=E5=B9=B2?= =?UTF-8?q?=E5=91=98=E4=BC=9A=E8=B7=9F=E7=82=8E=E7=9B=9F=E7=BA=A6=E7=9A=84?= =?UTF-8?q?=E7=82=8E=E7=A5=90=E5=86=B2=E7=AA=81=EF=BC=8C=E6=97=A0=E6=B3=95?= =?UTF-8?q?=E5=90=8C=E6=97=B6=E5=87=BA=E5=9C=BA=E3=80=8D,=20item=2023)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both spawn at battle start; tokens.js airTile (the 炎佑's tile: void tiles nearest the half's centre) did not know the stage's map-character positions (data/stages.json mapChars, the levels' predefined 预备干员-医疗 / Touch at (10, 2) …) and took (10, 2) on 战场#08 涨潮控制 (every time) and 战场#07 排气格栅 (the second 炎佑 of 9 炎); spawnMapChar then found its tile taken and the medic never stood (measured on every map: m04 6 / 9 炎 and m03 9 炎 — medic false). airTile now holds those positions (mapCharKeys). Test: test/content/feedback5-yanyou-touch.test.js (fails on the parent). Golden unchanged (no corpus battle had a 炎佑 on that tile). --- server/sim/content/tokens.js | 21 +++++++++- test/content/feedback5-yanyou-touch.test.js | 44 +++++++++++++++++++++ 2 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 test/content/feedback5-yanyou-touch.test.js diff --git a/server/sim/content/tokens.js b/server/sim/content/tokens.js index d9234261..a9392892 100644 --- a/server/sim/content/tokens.js +++ b/server/sim/content/tokens.js @@ -1286,17 +1286,34 @@ function yanyouKit(bb, raw) { }; } -/** Free field tile for a flyer: the player's half, void/non-deployable tiles first, nearest to the half's centre. */ +/** + * Stage positions held for the band map characters (data/stages.json `mapChars`: the levels' predefined 预备干员-医疗 / + * Touch of 外勤医疗, hidden until a player holds the strategy), as tile keys. + */ +function mapCharKeys(battle) { + const list = battle.stage?.raw?.mapChars ?? battle.stage?.mapChars ?? []; + const out = new Set(); + for (const m of Array.isArray(list) ? list : []) if (m && Array.isArray(m.pos)) out.add(m.pos[0] * COLS + m.pos[1]); + return out; +} + +/** + * Free field tile for a flyer: the player's half, void/non-deployable tiles first, nearest to the half's centre — never a + * map character's position: the 炎佑 spawns at the same battle start as 外勤医疗's medic and took its tile on the maps + * whose free void tiles nearest the centre include it (战场#08 涨潮控制 always, 战场#07 排气格栅 with 9 炎), so the medic + * never stood (community report of 2026-10-06 「Touch策略给的医疗干员会跟炎盟约的炎祐冲突,无法同时出场」). + */ function airTile(battle, playerId, taken) { const R = battle.rect; const ps = battle.getPlayer(playerId); let c0 = R.c0, c1 = R.c1; if (battle.players.length > 1 && ps) { if (ps.half === 'R') c0 = Math.max(c0, 11); else c1 = Math.min(c1, 10); } const cr = (R.r0 + R.r1) / 2, cc = (c0 + c1) / 2; + const held = mapCharKeys(battle); let best = null, bs = null; for (let r = R.r0; r <= R.r1; r++) { for (let c = c0; c <= c1; c++) { - if (taken.has(r * COLS + c) || !tileFree(battle, r, c)) continue; + if (taken.has(r * COLS + c) || held.has(r * COLS + c) || !tileFree(battle, r, c)) continue; const t = battle.grid.tile(r, c); const cls = t.build === 'NONE' && t.pass !== 'ALL' ? 0 : t.build === 'NONE' ? 1 : 2; const s = [cls, Math.hypot(r - cr, c - cc), r * COLS + c]; diff --git a/test/content/feedback5-yanyou-touch.test.js b/test/content/feedback5-yanyou-touch.test.js new file mode 100644 index 00000000..6813c9d8 --- /dev/null +++ b/test/content/feedback5-yanyou-touch.test.js @@ -0,0 +1,44 @@ +// Community report of 2026-10-06 「Touch策略给的医疗干员会跟炎盟约的炎祐冲突,无法同时出场」 (item 23): the 炎 bond's 炎佑 and the +// 外勤医疗 map character spawn at the same battle start; the 炎佑's free-tile search (tokens.js airTile: void tiles nearest the +// half's centre) took the medic's stage slot (data/stages.json mapChars, (10, 2)) on 战场#08 涨潮控制 (and 战场#07 with two +// 炎佑), so the medic found its tile taken and never stood. The map characters' positions are held for them now. + +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { makeBattle } from '../helpers/battleHarness.js'; +import { hasGeneratedData } from '../../server/sim/simdata.js'; + +const REAL = { skip: !hasGeneratedData() }; +const YAN = ['chess_char_1_03', 'chess_char_2_04', 'chess_char_3_03', 'chess_char_3_04', 'chess_char_4_15', 'chess_char_4_17', 'chess_char_5_03', 'chess_char_5_12', 'chess_char_6_03']; +const TILES = [[9, 3], [9, 4], [10, 3], [10, 4], [11, 3], [11, 4], [9, 5], [11, 5], [10, 5]]; + +function tokensAt(stageId, n, elite = false) { + const h = makeBattle({ + stageId, units: YAN.slice(0, n).map((id, i) => ({ chessId: `${id}_${elite && i < 2 ? 'b' : 'a'}`, row: TILES[i][0], col: TILES[i][1] })), + bonds: { yanShip: { count: n, layers: 1, active: true, tier: n >= 9 ? 3 : 2 } }, bandId: 'band_amedic', timeLimit: 10, seed: 1, autoFinish: false, + }); + h.step(3); + const toks = h.b.allyUnits.filter((u) => u.kind === 'token' && u.alive); + return { + yan: toks.filter((u) => u.defId === 'enemy_9012_acloon'), + medic: toks.find((u) => u.defId === 'char_605_cmedic' || u.defId === 'char_613_acmedc') ?? null, + }; +} + +test('战场#08 涨潮控制: the 炎佑 and 外勤医疗\'s medic both stand, the medic on its own slot', REAL, () => { + const t = tokensAt('act2autochess_m04', 6); + assert.equal(t.yan.length, 1); + assert.ok(t.medic, 'the medic stands (the parent: none — the 炎佑 sat on (10, 2))'); + assert.deepEqual([t.medic.tileR, t.medic.tileC], [10, 2]); + assert.ok(!(t.yan[0].tileR === 10 && t.yan[0].tileC === 2)); + const touch = tokensAt('act2autochess_m04', 6, true); + assert.equal(touch.medic?.defId, 'char_613_acmedc', 'two elites: Touch, on the same slot'); +}); + +test('9 炎: two 炎佑 and the medic on every season map', REAL, () => { + for (const stageId of ['act2autochess_m01', 'act2autochess_m02', 'act2autochess_m03', 'act2autochess_m04']) { + const t = tokensAt(stageId, 9); + assert.equal(t.yan.length, 2, stageId); + assert.ok(t.medic, `${stageId}: the medic stands`); + } +});