diff --git a/docs/research/02-bonds.md b/docs/research/02-bonds.md index 25f8fe5e..4bd8632d 100644 --- a/docs/research/02-bonds.md +++ b/docs/research/02-bonds.md @@ -922,7 +922,7 @@ Members (5 chess, 4 in current shop pool; by tier in shop: {'2': 1, '3': 1, '5': **Implementable spec** -- **[2 distinct]** If a 突袭 member has not attacked for 10 s, or its skill is ready, and no enemy is in its range: it is immediately redeployed (keeping SP) onto a tile next to a ground enemy; while in that redeployed state its ATK and maxHP x(1 + 0.25 + 0.01*L). A passive skill that is on counts as ready (GitHub #49: the reporter's footage shows 缄默德克萨斯 jumping within her passive's 10 s; the engine keeps a passive on for the whole deployment [ASSUMED]), and so does a deploy-timed skill while its window runs (#109 made 宴 S2, 斯卡蒂 S2, 伊内丝 S3, 缄默德克萨斯 S1–S3 and 耀骑士临光 S2 duration skills that start at each deployment; once the window has ended only the idle trigger is left); the landing tile must have the target in range (GitHub #51 [ASSUMED]). +- **[2 distinct]** If a 突袭 member has not attacked for 10 s, or its skill is ready, and no enemy is in its range: it is immediately redeployed (keeping SP) onto a tile next to a ground enemy; while in that redeployed state its ATK and maxHP x(1 + 0.25 + 0.01*L). A passive skill that is on counts as ready (GitHub #49: the reporter's footage shows 缄默德克萨斯 jumping within her passive's 10 s; the engine keeps a passive on for the whole deployment [ASSUMED]), and so does a deploy-timed skill while its window runs (#109 made 宴 S2, 斯卡蒂 S2, 伊内丝 S3, 缄默德克萨斯 S1–S3 and 耀骑士临光 S2 duration skills that start at each deployment; once the window has ended only the idle trigger is left); the landing tile must have the target in range (GitHub #51 [ASSUMED]). A 隐匿 (unrevealed) ground enemy is a target too, and one inside the member's range counts as an enemy in range (no jump) — the owner's decision of 2026-10-08 from the official game (GitHub #316); asleep, untargetable and flying enemies are not. - **[L >= 50]** ALL operators ASPD +50. - Formulas: `atkHpMultiplier = 1.25 + 0.01*L`; `idleSec = 10`; `aspdAll(L>=50) = 50` - How layers are gained: 史尔特尔 部署时 +8 (<=50/battle); 休谟斯 each 2 kills +1; 瑕光 +4/deploy; 斯卡蒂; 伊内丝 +5; 机变 "斯卡蒂的盟誓" +8, "德克萨斯的盟誓" +10. diff --git a/server/sim/content/bonds/addon/battle.js b/server/sim/content/bonds/addon/battle.js index 210a07d7..24a369a2 100644 --- a/server/sim/content/bonds/addon/battle.js +++ b/server/sim/content/bonds/addon/battle.js @@ -33,7 +33,9 @@ // 助力 deputShip all operators DEF +(base + per·L), redeploy time ×(1 + respawn_time) // 突袭 raidShip member idle ≥ no_attack_duration s (or skill ready — a passive skill that is on counts, GitHub // #49, and so does a deploy-timed skill while it runs, #109) with no enemy in range → "保留技力立即再部署" -// next to the most advanced ground enemy it can reach: on a free tile of a board of its field +// next to the most advanced ground enemy it can reach — a 隐匿 (unrevealed) ground enemy too, and one +// in its range counts for 「范围内没有敌人」 (the owner's decision of 2026-10-08 from the official game, +// GitHub #316; asleep, untargetable and flying ones stay out: raidGroundEnemy): on a free tile of a board of its field // (Battle.onFieldBoard: its own or a teammate's on the same field — the two-helper 联防 field and a // pair field open both halves, the owner's decision of 2026-10-07; never a boss field's hand / // 临时整备区 rows, never the empty other half of a lone 联防 helper or a solo boss field — community @@ -62,9 +64,9 @@ // Hooks registered only when a bond needs them. Priorities: `hit` −20 (坚守 redirect, after other modifiers had their // say), `death` 10 (不屈, the bond slot of the revive/redeploy convention); everything else 0. -import { canTargetEnemy, extendedGrid } from '../../../targeting.js'; +import { canTargetEnemy, enemyStealthed, extendedGrid } from '../../../targeting.js'; import { normDir, rotateOffset, localOrder, localBefore } from '../../../dir.js'; -import { bodyKeys } from '../../../body.js'; +import { bodyKeys, bodyInKeys } from '../../../body.js'; import { isHpLoss } from '../../../damage.js'; import { num, bondRecord, buffParams, bondTier, bondLayers, isMember, isElite, isGroundOp, onField, playerOps, passiveBuff, @@ -315,15 +317,39 @@ function raidTile(battle, u, e, reach) { } /** - * Jump candidates of player `pid`, in priority order: the ground enemies an operator may target (not flying, - * canTargetEnemy) — those of the player's own field (`ownerId`), the others only when it has none —, the most advanced - * first (least remaining path distance, then the earliest spawned) [ASSUMED, research 02 §3.18]. The same list for - * every member of the player (canTargetEnemy reads the enemy, not the attacker). + * A ground enemy 突袭 jumps toward, and that counts for 「范围内没有敌人」 while it stands in a member's range: one an + * operator may target (canTargetEnemy, ground), or a 隐匿 one that only its 隐匿 keeps out — the owner's decision of + * 2026-10-08 from the official game (GitHub #316 "Raid operator doesn't deploy to invisible enemy units, it does in + * official"; the bond text says 敌人 / 地面敌人). Asleep, untargetable and flying enemies stay out, as before. + */ +function raidGroundEnemy(u, e) { + if (!e || e.isFlying) return false; + if (canTargetEnemy(u, e, { canHitFly: false })) return true; + if (!e.alive || e.hidden || !e.deployed || !enemyStealthed(e)) return false; + const f = e.s.flags; + if (f.untargetable || f.sleep) return false; + const skip = u.profile && u.profile.skipEnemy; + return !(typeof skip === 'function' && skip(e)); +} + +/** A 隐匿 ground enemy (raidGroundEnemy) on `u`'s range: enemiesInKeys leaves 隐匿 out, so the poll checks this too. */ +function raidStealthInRange(battle, u) { + const keys = u.rangeKeySet || u.rangeKeys; + if (!keys) return false; + for (const e of battle.enemies) if (e.alive && enemyStealthed(e) && raidGroundEnemy(u, e) && bodyInKeys(e, keys)) return true; + return false; +} + +/** + * Jump candidates of player `pid`, in priority order: the ground enemies of raidGroundEnemy (visible, or 隐匿; not + * flying, asleep or untargetable) — those of the player's own field (`ownerId`), the others only when it has none —, + * the most advanced first (least remaining path distance, then the earliest spawned) [ASSUMED, research 02 §3.18]. + * The same list for every member of the player (the checks read the enemy, not the attacker). */ function raidTargets(battle, u, pid) { const own = [], other = []; for (const e of battle.enemies) { - if (e.isFlying || !canTargetEnemy(u, e, { canHitFly: false })) continue; + if (!raidGroundEnemy(u, e)) continue; (e.ownerId === pid ? own : other).push(e); } const list = own.length ? own : other; @@ -352,7 +378,8 @@ function raidPoll(battle, st) { const ready = !!(sk && !sk.noSkill && ((sk.ready && !(sk.active && sk.isTimed)) || deploySkillOn)); const idleOk = battle.time - since >= idle - 1e-9; if (!(ready || idleOk)) continue; - if (battle.enemiesInKeys(u.rangeKeys || [], u, u.profile).length) continue; + // 「若范围内没有敌人」: a targetable enemy, or a 隐匿 ground one (the owner's decision of 2026-10-08, raidGroundEnemy) + if (battle.enemiesInKeys(u.rangeKeys || [], u, u.profile).length || raidStealthInRange(battle, u)) continue; const list = (targets ??= raidTargets(battle, u, st.pid)); if (!list.length) continue; // either trigger: raidTile only offers tiles with the target in range (without that a ready skill that finds no diff --git a/test/golden/matches.json b/test/golden/matches.json index a21b367d..46ccfb36 100644 --- a/test/golden/matches.json +++ b/test/golden/matches.json @@ -1398,7 +1398,7 @@ "combat": "client", "boost": {"lp":400,"layers":300}, "setup": {"stageId":"act2autochess_m04","bossId":"boss_5","hiddenBossId":"boss_9","factions":["FLY","ELEMENT","INVISIBLE"]}, - "end": {"victory":true,"roundsPassed":15,"hiddenReached":true,"hiddenCleared":true,"reason":"victory","teamLp":793,"bossPoolLeft":0,"virtualMs":124650}, + "end": {"victory":true,"roundsPassed":15,"hiddenReached":true,"hiddenCleared":true,"reason":"victory","teamLp":795,"bossPoolLeft":0,"virtualMs":124650}, "rng": {"setup":25,"shop":750,"waves":0,"draft":61,"bots":22165,"meta":198}, "errors": {"engine":0,"logged":0,"sim":0,"dispatcher":0}, "rounds": [ @@ -1444,27 +1444,27 @@ }, { "round": 9, - "ai_0": [1,400,12,0,0,4,8,"cdec5dfa",2214,"arcaneShip 2/318, deputShip 0/304, egirShip 1/322, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/302, kjeragShip 0/302, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/310, raidShip 1/330, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 1/304, steadShip 1/330, suntShip 1/300, swiftShip 0/302, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",24,24,0,77], + "ai_0": [1,400,12,0,0,4,8,"cdec5dfa",2231,"arcaneShip 2/318, deputShip 0/304, egirShip 1/319, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/302, kjeragShip 0/302, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/310, raidShip 1/353, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 1/304, steadShip 1/327, suntShip 1/300, swiftShip 0/302, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",24,24,0,77], "ai_1": [1,400,12,0,3,6,8,"36a202d9",2210,"arcaneShip 0/302, deputShip 1/326, egirShip 0/300, emptyShip 0/308, indomShip 0/300, investShip 0/300, kazimierzShip 0/300, kjeragShip 0/300, lateranoShip 0/300, maniShip 0/300, miraShip 1/306, preciShip 0/309, raidShip 0/300, sargonShip 0/300, siracusaShip 1/342, skillfulShip 1/302, soloShip 1/316, steadShip 0/312, suntShip 1/302, swiftShip 1/316, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",29,25,0,114] }, { "round": 10, - "ai_0": [1,400,12,0,0,5,8,"582856d0",2234,"arcaneShip 2/318, deputShip 0/304, egirShip 1/330, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/302, kjeragShip 0/304, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/310, raidShip 1/338, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 1/304, steadShip 1/334, suntShip 1/300, swiftShip 0/302, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",12,12,0,89], + "ai_0": [1,400,12,0,0,5,8,"582856d0",2251,"arcaneShip 2/318, deputShip 0/304, egirShip 1/327, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/302, kjeragShip 0/304, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/310, raidShip 1/363, sargonShip 0/300, siracusaShip 0/300, skillfulShip 0/300, soloShip 1/304, steadShip 1/329, suntShip 1/300, swiftShip 0/302, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",12,12,0,89], "ai_1": [1,400,15,0,2,6,8,"3a43d0ba",2260,"arcaneShip 0/302, deputShip 1/328, egirShip 0/300, emptyShip 0/308, indomShip 0/300, investShip 0/300, kazimierzShip 0/300, kjeragShip 0/300, lateranoShip 0/300, maniShip 0/300, miraShip 1/314, preciShip 0/309, raidShip 0/300, sargonShip 0/300, siracusaShip 1/370, skillfulShip 1/306, soloShip 1/318, steadShip 0/312, suntShip 1/304, swiftShip 1/320, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",13,13,0,130] }, { "round": 11, - "ai_0": [1,400,12,0,1,5,8,"9ac6e043",2267,"arcaneShip 2/332, deputShip 0/304, egirShip 1/343, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/302, kjeragShip 0/308, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/315, raidShip 1/365, sargonShip 0/300, siracusaShip 0/300, skillfulShip 1/308, soloShip 1/304, steadShip 0/338, suntShip 1/300, swiftShip 0/302, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",38,34,0,102], + "ai_0": [1,400,12,0,1,5,8,"9ac6e043",2281,"arcaneShip 2/332, deputShip 0/304, egirShip 1/340, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/302, kjeragShip 0/308, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/315, raidShip 1/382, sargonShip 0/300, siracusaShip 0/300, skillfulShip 1/308, soloShip 1/304, steadShip 0/333, suntShip 1/300, swiftShip 0/302, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",38,34,0,102], "ai_1": [1,400,14,0,1,6,8,"120b46d6",2628,"arcaneShip 1/318, deputShip 1/330, egirShip 0/300, emptyShip 0/308, indomShip 0/300, investShip 0/300, kazimierzShip 0/300, kjeragShip 0/300, lateranoShip 0/300, maniShip 0/300, miraShip 1/322, preciShip 0/311, raidShip 0/300, sargonShip 0/300, siracusaShip 1/390, skillfulShip 1/318, soloShip 1/320, steadShip 0/312, suntShip 1/306, swiftShip 1/324, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",34,34,0,144] }, { "round": 12, - "ai_0": [1,400,13,0,0,6,8,"afebef50",2311,"arcaneShip 2/340, deputShip 0/304, egirShip 1/348, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/302, kjeragShip 0/308, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/320, raidShip 1/383, sargonShip 0/300, siracusaShip 0/300, skillfulShip 1/316, soloShip 1/304, steadShip 0/338, suntShip 1/300, swiftShip 0/302, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",6,14,8,115], + "ai_0": [1,400,13,0,0,6,8,"afebef50",2325,"arcaneShip 2/340, deputShip 0/304, egirShip 1/345, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/302, kjeragShip 0/308, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/320, raidShip 1/400, sargonShip 0/300, siracusaShip 0/300, skillfulShip 1/316, soloShip 1/304, steadShip 0/333, suntShip 1/300, swiftShip 0/302, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",6,14,8,115], "ai_1": [1,400,13,0,5,6,8,"9e462d87",2688,"arcaneShip 1/333, deputShip 1/332, egirShip 0/300, emptyShip 0/308, indomShip 0/300, investShip 0/300, kazimierzShip 0/300, kjeragShip 0/300, lateranoShip 0/300, maniShip 0/300, miraShip 1/328, preciShip 0/311, raidShip 0/300, sargonShip 0/300, siracusaShip 1/411, skillfulShip 1/326, soloShip 1/322, steadShip 0/312, suntShip 1/308, swiftShip 1/328, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",15,15,0,152] }, { "round": 13, - "ai_0": [1,400,12,0,2,6,8,"97689296",2364,"arcaneShip 2/349, deputShip 0/304, egirShip 1/361, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/302, kjeragShip 0/308, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/325, raidShip 1/403, sargonShip 0/300, siracusaShip 0/300, skillfulShip 1/322, soloShip 1/304, steadShip 0/338, suntShip 1/300, swiftShip 0/302, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",41,41,0,126], + "ai_0": [1,400,12,0,2,6,8,"97689296",2380,"arcaneShip 2/349, deputShip 0/304, egirShip 1/359, emptyShip 0/300, indomShip 0/300, investShip 0/300, kazimierzShip 0/302, kjeragShip 0/308, lateranoShip 0/300, maniShip 0/300, miraShip 0/300, preciShip 1/325, raidShip 1/421, sargonShip 0/300, siracusaShip 0/300, skillfulShip 1/322, soloShip 1/304, steadShip 0/333, suntShip 1/300, swiftShip 0/302, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",41,41,0,126], "ai_1": [1,400,17,0,0,6,8,"36a5d772",2717,"arcaneShip 1/345, deputShip 0/332, egirShip 0/300, emptyShip 0/308, indomShip 0/300, investShip 0/300, kazimierzShip 0/300, kjeragShip 0/300, lateranoShip 0/300, maniShip 0/300, miraShip 1/334, preciShip 1/311, raidShip 0/300, sargonShip 0/300, siracusaShip 1/429, skillfulShip 1/338, soloShip 1/322, steadShip 0/312, suntShip 1/308, swiftShip 1/330, victoriaShip 0/300, visiShip 0/300, yanShip 0/300",41,41,0,168] } ], @@ -1474,26 +1474,26 @@ "victory": true, "roundsPassed": 15, "eliminatedRound": null, - "lp": 397, + "lp": 398, "bandId": "band_sarkazb", "title": {"id":"comment_1","name":"卫戍之星","picId":"comment_icon_1","text":"对敌方领袖造成伤害最高(仅胜利时)"}, "trophies": 8, "reward": 425, - "stats": {"activatedLayers":3346,"bossDamage":7712110,"dmgDealt":20535636,"gold":154,"itemsEquipped":21,"kills":287,"leaks":8,"lpLost":0,"merges":7,"perfectRounds":12,"refreshes":21}, + "stats": {"activatedLayers":3362,"bossDamage":7735924,"dmgDealt":20519044,"gold":154,"itemsEquipped":21,"kills":287,"leaks":8,"lpLost":0,"merges":7,"perfectRounds":12,"refreshes":21}, "lineup": ["4_09_b@11,5[3_11_e_a+3_11_e_b]","6_11_a@11,8","3_13_b@10,3","3_05_b@10,8[1_01_e_a+6_09_e_a]","6_16_a@9,6","6_05_a@9,7[3_02_e_a]","4_14_b@9,8[2_01_e_a+1_02_e_b]","5_07_a@9,9[4_10_e_b]"], - "bonds": ["arcaneShip 1/365","deputShip 0/304","egirShip 1/364","emptyShip 0/300","indomShip 0/300","investShip 0/300","kazimierzShip 0/302","kjeragShip 0/308","lateranoShip 0/300","maniShip 1/300","miraShip 0/300","preciShip 1/345","raidShip 1/405","sargonShip 1/300","siracusaShip 0/300","skillfulShip 1/338","soloShip 1/307","steadShip 0/338","suntShip 1/300","swiftShip 1/322","victoriaShip 0/300","visiShip 0/300","yanShip 0/300"] + "bonds": ["arcaneShip 1/365","deputShip 0/304","egirShip 1/362","emptyShip 0/300","indomShip 0/300","investShip 0/300","kazimierzShip 0/302","kjeragShip 0/308","lateranoShip 0/300","maniShip 1/300","miraShip 0/300","preciShip 1/345","raidShip 1/423","sargonShip 1/300","siracusaShip 0/300","skillfulShip 1/338","soloShip 1/307","steadShip 0/333","suntShip 1/300","swiftShip 1/322","victoriaShip 0/300","visiShip 0/300","yanShip 0/300"] }, "ai_1": { "alive": true, "victory": true, "roundsPassed": 15, "eliminatedRound": null, - "lp": 396, + "lp": 397, "bandId": "band_doberm", "title": {"id":"comment_3","name":"坚若磐石","picId":"comment_icon_3","text":"目标生命值损失最少"}, "trophies": 8, "reward": 425, - "stats": {"activatedLayers":2801,"bossDamage":6180513,"dmgDealt":24059056,"gold":191,"itemsEquipped":32,"kills":287,"leaks":1,"lpLost":0,"merges":5,"perfectRounds":12,"refreshes":55}, + "stats": {"activatedLayers":2801,"bossDamage":6156699,"dmgDealt":23996575,"gold":191,"itemsEquipped":32,"kills":287,"leaks":1,"lpLost":0,"merges":5,"perfectRounds":12,"refreshes":55}, "lineup": ["4_10_a@11,5[3_05_e_a+4_03_e_a]","6_05_a@11,8[4_08_e_a+6_09_e_a]","6_16_a@10,3[3_11_e_a+1_05_e_b]","2_13_a@10,7[4_04_e_a+4_05_e_a]","1_08_b@10,8[4_07_e_b+5_07_e_a]","6_08_a@9,6[2_01_e_b+3_01_e_b]","5_20_a@9,7[1_05_e_a+3_04_e_a]","6_08_a@9,8[3_03_e_a+1_01_e_a]","3_19_b@9,9[2_04_e_a+3_07_e_b]"], "bonds": ["arcaneShip 1/371","deputShip 0/332","egirShip 0/300","emptyShip 0/308","indomShip 0/300","investShip 0/300","kazimierzShip 0/300","kjeragShip 0/300","lateranoShip 0/300","maniShip 0/300","miraShip 1/334","preciShip 1/311","raidShip 0/300","sargonShip 0/300","siracusaShip 1/465","skillfulShip 1/356","soloShip 1/322","steadShip 0/312","suntShip 1/308","swiftShip 1/334","victoriaShip 0/300","visiShip 0/300","yanShip 0/300"] } @@ -1509,7 +1509,7 @@ "combat": "client", "boost": null, "setup": {"stageId":"act1autochess_m01","bossId":"boss_5","hiddenBossId":"boss_10","factions":["DOT","INVISIBLE","REFLECTION"]}, - "end": {"victory":true,"roundsPassed":14,"hiddenReached":false,"hiddenCleared":false,"reason":"victory","teamLp":39,"bossPoolLeft":0,"virtualMs":121400}, + "end": {"victory":true,"roundsPassed":14,"hiddenReached":false,"hiddenCleared":false,"reason":"victory","teamLp":40,"bossPoolLeft":0,"virtualMs":121400}, "rng": {"setup":25,"shop":489,"waves":0,"draft":34,"bots":18039,"meta":11}, "errors": {"engine":0,"logged":0,"sim":0,"dispatcher":0}, "rounds": [ @@ -1565,17 +1565,17 @@ }, { "round": 11, - "h_0": [1,26,12,0,0,5,8,"7c35f14c",146,"arcaneShip 1/11, kazimierzShip 1/40, miraShip 1/4, preciShip 0/8, raidShip 1/76, skillfulShip 0/3, soloShip 1/0, suntShip 1/0, swiftShip 1/15, yanShip 0/1",22,25,3,100], + "h_0": [1,27,12,0,0,5,8,"7c35f14c",146,"arcaneShip 1/11, kazimierzShip 1/40, miraShip 1/4, preciShip 0/8, raidShip 1/76, skillfulShip 0/3, soloShip 1/0, suntShip 1/0, swiftShip 1/15, yanShip 0/1",23,25,2,100], "ai_0": [1,23,12,0,0,5,8,"0e29ce35",193,"arcaneShip 0/2, deputShip 0/16, kazimierzShip 0/8, lateranoShip 1/41, preciShip 2/40, sargonShip 0/2, skillfulShip 1/19, soloShip 1/5, suntShip 1/14, swiftShip 1/74",25,25,0,101] }, { "round": 12, - "h_0": [1,26,12,0,0,6,8,"c8e0476d",181,"arcaneShip 1/14, kazimierzShip 1/52, miraShip 1/8, preciShip 0/8, raidShip 1/88, skillfulShip 0/3, soloShip 1/0, suntShip 1/0, swiftShip 1/19, yanShip 0/1",27,31,4,112], + "h_0": [1,27,12,0,0,6,8,"c8e0476d",181,"arcaneShip 1/14, kazimierzShip 1/52, miraShip 1/8, preciShip 0/8, raidShip 1/88, skillfulShip 0/3, soloShip 1/0, suntShip 1/0, swiftShip 1/19, yanShip 0/1",27,31,4,112], "ai_0": [1,23,12,0,0,6,8,"55f8656e",229,"arcaneShip 0/2, deputShip 0/16, kazimierzShip 0/8, lateranoShip 1/47, preciShip 2/53, sargonShip 0/2, skillfulShip 1/24, soloShip 1/5, suntShip 1/14, swiftShip 1/86",31,31,0,112] }, { "round": 13, - "h_0": [1,16,12,0,0,6,8,"64fc1bc4",199,"arcaneShip 0/14, kazimierzShip 1/64, miraShip 1/12, preciShip 0/8, raidShip 1/100, skillfulShip 0/3, soloShip 1/0, suntShip 1/0, swiftShip 1/23, yanShip 0/1",28,57,29,124], + "h_0": [1,17,12,0,0,6,8,"b18b37b2",191,"arcaneShip 0/14, kazimierzShip 1/60, miraShip 1/12, preciShip 0/8, raidShip 1/96, skillfulShip 0/3, soloShip 1/0, suntShip 1/0, swiftShip 1/23, yanShip 0/1",17,57,40,124], "ai_0": [1,23,12,0,0,6,8,"dc15adde",265,"arcaneShip 0/2, deputShip 0/16, kazimierzShip 0/8, lateranoShip 1/53, preciShip 2/63, sargonShip 0/2, skillfulShip 1/29, soloShip 1/5, suntShip 1/14, swiftShip 1/101, visiShip 1/0",57,57,0,126] } ], @@ -1585,14 +1585,14 @@ "victory": true, "roundsPassed": 14, "eliminatedRound": null, - "lp": 16, + "lp": 17, "bandId": "band_amedic", "title": {"id":"comment_4","name":"精英云集","picId":"comment_icon_4","text":"晋升精锐次数最多"}, "trophies": 4, "reward": 400, - "stats": {"activatedLayers":206,"bossDamage":301759,"dmgDealt":2977771,"gold":136,"itemsEquipped":21,"kills":274,"leaks":37,"lpLost":12,"merges":3,"perfectRounds":9,"refreshes":21}, + "stats": {"activatedLayers":198,"bossDamage":380567,"dmgDealt":2997433,"gold":136,"itemsEquipped":21,"kills":268,"leaks":47,"lpLost":11,"merges":3,"perfectRounds":9,"refreshes":21}, "lineup": ["6_18_a@12,3","4_10_a@12,4[2_01_e_b+2_04_e_a]","1_17_b@11,3[4_03_e_a+1_01_e_a]","5_20_a@11,8[6_05_e_a+3_06_e_a]","3_17_b@10,3","3_12_a@10,4[1_02_e_a+3_11_e_a]","5_07_a@10,7","1_19_b@10,8[2_01_e_b+4_04_e_b]"], - "bonds": ["arcaneShip 2/18","kazimierzShip 1/64","miraShip 1/12","preciShip 0/8","raidShip 1/100","siracusaShip 1/12","skillfulShip 0/3","soloShip 1/0","suntShip 1/0","swiftShip 0/23","yanShip 0/1"] + "bonds": ["arcaneShip 2/18","kazimierzShip 1/60","miraShip 1/12","preciShip 0/8","raidShip 1/96","siracusaShip 1/12","skillfulShip 0/3","soloShip 1/0","suntShip 1/0","swiftShip 0/23","yanShip 0/1"] }, "ai_0": { "alive": true, @@ -1604,7 +1604,7 @@ "title": {"id":"comment_1","name":"卫戍之星","picId":"comment_icon_1","text":"对敌方领袖造成伤害最高(仅胜利时)"}, "trophies": 4, "reward": 400, - "stats": {"activatedLayers":296,"bossDamage":478241,"dmgDealt":3742103,"gold":140,"itemsEquipped":17,"kills":315,"leaks":0,"lpLost":0,"merges":3,"perfectRounds":13,"refreshes":22}, + "stats": {"activatedLayers":296,"bossDamage":399433,"dmgDealt":3904432,"gold":140,"itemsEquipped":17,"kills":327,"leaks":0,"lpLost":0,"merges":3,"perfectRounds":13,"refreshes":22}, "lineup": ["3_21_a@12,3[4_08_e_a+6_02_e_a]","4_25_a@12,4","2_01_a@12,5[3_01_e_a]","6_12_a@11,3[1_05_e_a+3_11_e_a]","4_14_b@11,4[1_05_e_b+4_07_e_a]","1_01_b@10,3[3_08_e_a]","5_01_a@10,4[4_05_e_a]","2_13_a@9,4[4_02_e_b]"], "bonds": ["arcaneShip 0/2","deputShip 0/16","emptyShip 1/0","kazimierzShip 0/8","lateranoShip 1/56","preciShip 2/76","sargonShip 0/2","skillfulShip 1/34","soloShip 0/5","suntShip 1/14","swiftShip 1/113","visiShip 1/3"] } diff --git a/test/sim/feedback7-raid-stealth.test.js b/test/sim/feedback7-raid-stealth.test.js new file mode 100644 index 00000000..f305a5ad --- /dev/null +++ b/test/sim/feedback7-raid-stealth.test.js @@ -0,0 +1,85 @@ +// test/sim/feedback7-raid-stealth.test.js — 突袭 and 隐匿 ground enemies: the owner's decision of 2026-10-08 (GitHub #316 +// "Raid operator doesn't deploy to invisible enemy units, it does in official", confirmed by the owner's own play): a 突袭 +// member jumps next to a 隐匿 (unrevealed) ground enemy, and one already in its range counts for 「若范围内没有敌人」 (no +// hop). Asleep, untargetable and flying enemies stay out, as before (bond text: 「再部署至一名地面敌人周围」). +// Run: node --test test/sim/feedback7-raid-stealth.test.js + +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { makeBattle, chessRec, enemyRec, checkInvariants } from '../helpers/battleHarness.js'; +import { enemyStealthed } from '../../server/sim/targeting.js'; + +const HOME = [12, 3]; +const BONDS = { raidShip: { count: 2, active: true, tier: 1, layers: 0 } }; +/** + * A field where (10,7) is the only tile a melee member may land on beside the enemy at (10,8): the rest of rows 9–12 is + * undeployable floor ('f'), its home (12,3) road. + */ +const ONE_SPOT = { rows: { + 9: '##Efffffff' + 'S' + 'fffffffS##', + 10: '##hffffrff' + 'f' + 'ffffffff##', + 11: '##hfffffff' + 'f' + 'ffffffff##', + 12: '##hrffffff' + 'S' + 'fffffffS##', +} }; + +/** `foe` at `pos` (with the buff flags `flags`), and optionally a visible `vis` at `visible`. */ +function field({ pos = [10, 8], flags = null, fly = false, visible = null, range = [[0, 0], [0, 1]] } = {}) { + const h = makeBattle({ + flat: ONE_SPOT, bonds: BONDS, autoFinish: false, timeLimit: 60, hooks: ['deploy'], + defs: { + chess: { raider: chessRec({ id: 'raider', profession: 'WARRIOR', position: 'MELEE', bonds: ['raidShip'], rangeGrid: range, skill: { spCost: 10, initSp: 10 } }) }, + enemies: { + foe: enemyRec({ key: 'foe', hp: 1e7, speed: 0, atk: 0, motion: fly ? 'FLY' : 'WALK' }), + vis: enemyRec({ key: 'vis', hp: 1e7, speed: 0, atk: 0 }), + }, + }, + units: [{ chessId: 'raider', row: HOME[0], col: HOME[1], dir: 'RIGHT' }], + enemies: [{ key: 'foe', pos }, ...(visible ? [{ key: 'vis', pos: visible }] : [])], + setup(b) { + if (flags) b.on('enemySpawn', (c) => { if (/foe$/.test(c.enemy.defId)) b.addBuff(c.enemy, { key: 'test:mark', persist: true, flags }); }); + }, + }); + return h; +} +const jumps = (h) => h.hooksOf('deploy').filter((c) => c.unit === h.unit('raider') && !c.initial).length; +const at = (h) => [h.unit('raider').tileR, h.unit('raider').tileC]; +const foe = (h) => h.b.enemies.find((e) => e.alive && /foe$/.test(e.defId)); + +test('a ready 突袭 member jumps to the only landing spot beside a 隐匿 ground enemy, and does not hop again', () => { + const h = field({ flags: { stealth: true } }); + h.run(1); + assert.equal(enemyStealthed(foe(h)), true, 'still 隐匿 (not blocked, not revealed)'); + assert.deepEqual(at(h), [10, 7], 'landed on the one tile beside it'); + assert.equal(jumps(h), 1); + h.run(3); + assert.equal(jumps(h), 1, 'the 隐匿 enemy in its range is an enemy in range: no hop'); + assert.deepEqual(at(h), [10, 7]); + checkInvariants(h.b); +}); + +test('a visible ground enemy: the same landing (control)', () => { + const h = field(); + h.run(1); + assert.deepEqual(at(h), [10, 7]); + assert.equal(jumps(h), 1); + checkInvariants(h.b); +}); + +test('a 隐匿 ground enemy already in its range: 「范围内没有敌人」 is false — it stays, though a visible one waits elsewhere', () => { + const h = field({ pos: [12, 4], flags: { stealth: true }, visible: [10, 8] }); + h.run(3); + assert.equal(enemyStealthed(foe(h)), true); + assert.deepEqual(at(h), HOME); + assert.equal(jumps(h), 0); + checkInvariants(h.b); +}); + +test('asleep or flying 隐匿 enemies are still no destination', () => { + for (const opts of [{ flags: { stealth: true, sleep: true } }, { flags: { stealth: true }, fly: true }]) { + const h = field(opts); + h.run(3); + assert.deepEqual(at(h), HOME, JSON.stringify(opts)); + assert.equal(jumps(h), 0); + checkInvariants(h.b); + } +});