feedback5: merge CB4 — community reports (enemies / bosses): a 突袭 member lands only on its own player's board (never a boss field's hand / 临时整备区 rows nor the other half of a 联防 or boss field), a silenced 萨卡兹枯朽战士 leaves no 污染秽蚀, 萨卡兹穿刺手's acceleration ends with a stun or 束缚 and its first hit after a block adds move speed × 600 as its own instance, 自制投石机 ignores its block when it picks a target (no attack on the 排气格栅's hidden blocker), the briefing types' 7–9 waves, 新硎's 断刃, a freed prisoner in 联防, 鸭爵's 0–2 swaps and the moving leaders' routes pinned, four reports not reproduced; §25.18; golden: vs feedback5 CB4's own rules — roster-016, boss-boss_2/3/4/5/6-pair, hidden-boss_8/10-pair, standin-04/06/08, diy-028, diy-124, matches solo-NORMAL-1, solo-HARD-1, solo-ABYSS-1, coop2-HARD-4, coop4-ABYSS-7, coop2-NORMAL-8-serverrun; vs CB4 exactly what feedback5 changed since its base (CB2's roster 13 / bonds 7 / fields 6, CB5's 18 bot matches)
Merge fix: test/sim/facing-invariance.test.js — the 突袭 rotation check put its enemy on the board's top row (9, 9), so the facing-RIGHT landing tile it expected, (8, 8), lies above the player's board, where CB4's onOwnBoard rule (the official 再部署 goes where the player deploys; community reports 40 / 16.3) now refuses it; the member landed on (10, 8). The enemy stands at (10, 6), inside rows 9–12 / cols 2–10, so all four diagonal tiles are candidates in every direction and the test checks the facing tie-break as before. It failed on fb5-cb4 itself (the stream did not run the file). The §25 intro's stream lines joined into one sentence again. Golden moves outside CB4's commit list are its rules on feedback5's paths (instrumented): solo-HARD-1 and solo-ABYSS-1 now have silenced 枯朽战士 deaths (8 / 15) and 突袭 candidates the own-board filter refuses; coop2-ABYSS-10-boosted, coop2-NORMAL-3 and coop2-NORMAL-14-standins (in CB4's list) have no silenced 枯朽战士 death on this path, and their digests are identical with the own-board filter switched off (the refused tiles were never the chosen ones). Gate: non-browser suites 4968 / 4968, npm run golden ok, test/golden.test.js 9 / 9, lint 0 errors, check:imports ok, typecheck ok, docs-consistency + docs-paths 31 / 31.
This commit is contained in:
commit
6ed3aed335
23 files changed
+1364
-598
No files matched your search
@@ -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 its position may be deployed
|
||||
// next to the most advanced ground enemy it can reach: on a free tile of its own board (Battle.onOwnBoard:
|
||||
// never a boss field's hand / 临时整备区 rows, never the other half of a 联防 or boss field — community
|
||||
// reports of 2026-10-06, items 40 and 16.3) its position may be deployed
|
||||
// on from which its range covers that enemy (GitHub issue #51 [ASSUMED]: the first of the 8 most
|
||||
// advanced that has such a tile; none → it stays and the next poll looks again, never a jump that
|
||||
// hits nothing) — for a melee member that blocks, a tile where its block applies first (ground
|
||||
@@ -269,7 +271,11 @@ function raidReach(u) {
|
||||
/**
|
||||
* Landing tile [row, col] of a jump to enemy `e`, or null: a tile from which the member's range (`reach`, raidReach)
|
||||
* covers the enemy's body (a huge enemy: any tile it occupies — body.js), within RAID_SEARCH tiles (Chebyshev) of the
|
||||
* enemy, inside the field rect, that the member's position may be deployed on (grid.canStand: never the 深水区 —
|
||||
* enemy, inside the field rect and on the member's own board (Battle.onOwnBoard — "再部署" goes where its player deploys:
|
||||
* never a boss field's hand row 0 or 临时整备区 row 1, both inside BOSS_RECT and buildable high ground, nor the half of the
|
||||
* 联防 or boss field that is not its player's; community reports of 2026-10-06, items 40 and 16.3: a ranged member landed
|
||||
* on the 临时整备区 row of a solo leader round, and on the right half of the one-helper 联防 map), that the member's
|
||||
* position may be deployed on (grid.canStand: never the 深水区 —
|
||||
* player report #3 after 0.1.0 —, and for a melee member low ground only, never a 高台: GitHub #148) and that is free
|
||||
* (Battle.isReservedTile: no living unit, no knocked-out operator's body — player report F5 —, no waiting piece's tile).
|
||||
* A melee member that blocks takes a tile where its block applies first — ground units pass it (not a 围墙 / 围栏 tile,
|
||||
@@ -293,7 +299,7 @@ function raidTile(battle, u, e, reach) {
|
||||
for (let i = 0; i < reach.length; i += 2) {
|
||||
const r = br - reach[i], c = bc - reach[i + 1], dr = r - er, dc = c - ec;
|
||||
if (Math.abs(dr) > RAID_SEARCH || Math.abs(dc) > RAID_SEARCH) continue;
|
||||
if (!battle.grid.inRect(r, c) || !battle.grid.canStand(r, c, { ranged }) || battle.isReservedTile(r, c)) continue;
|
||||
if (!battle.grid.inRect(r, c) || !battle.onOwnBoard(u.player, r, c) || !battle.grid.canStand(r, c, { ranged }) || battle.isReservedTile(r, c)) continue;
|
||||
const t = r * COLS + c;
|
||||
const p = path && battle.grid.tile(r, c).pass === 'ALL' && (path.has(t) || body.includes(t)) ? 0 : 1; // its block applies
|
||||
const d = Math.max(Math.abs(dr), Math.abs(dc)) + 0.01 * (Math.abs(dr) + Math.abs(dc));
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
// there: an enemy gains 1 layer — 2 at 重量 ≥ heavyWeight (the device's `value`, 3) — of ASPD aspdPerStack
|
||||
// (fractions are ×100 ASPD) and move speed × (1 + moveMulPerStack × layers), an operator 1 layer of the
|
||||
// ASPD part only; at most maxStacks layers; cleared on leaving
|
||||
// 烟雾 g operators on it cannot be targeted by enemy ranged attacks (stealth flag: blocked enemies still hit them)
|
||||
// 烟雾 g operators on it cannot be targeted by enemy ranged attacks (stealth flag: blocked enemies still hit them —
|
||||
// not 自制投石机, whose 索敌不受阻挡影响: blocked by one with nobody else in range it does not attack)
|
||||
// 深水 d ground enemies on it: sea_drown[enemy].damage dmg/s (无来源 true 持续伤害, not 环境伤害: tags dot /
|
||||
// periodic / deepsea), ASPD attack_speed (×100), move × move_speed
|
||||
// 活性源石 i a unit on it (allies and ground enemies) gets a timed effect: damage true dmg/s, ATK + atk, ASPD +
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
// hidden from later 'kill' handlers) · death(c,…) · status(c,…) (statusApplied on e) · burst(c,…) (elementBurst on e)
|
||||
// · tick(b,e,a,dt) every `iv` s (every tick when iv is 0) · skill: `cd`/`left`(=icd)/`cond`/`fire` (cooldown runs
|
||||
// continuously; fires when ready, the enemy can act and `cond` holds). `sil: true` = the ability's handbook line
|
||||
// is flagged SILENCE (沉默 disables it: exactly the data's `format: "SILENCE"` lines); stun / freeze / sleep block
|
||||
// is flagged SILENCE (沉默 disables it: exactly the data's `format: "SILENCE"` lines — and 萨卡兹枯朽战士 / 组长's
|
||||
// 死亡爆炸, a NORMAL line whose client template checks 沉默: archetypes.js kitPolluted); stun / freeze / sleep block
|
||||
// skills. Enemy SP skills ("数次攻击后…"): SP +1 per attack, the (spCost+1)-th attack is the skill attack (official
|
||||
// 粉碎攻坚手 text: spCost 2 = "攻击2次后,下一次攻击").
|
||||
// Shared mechanics: reborn() — "首次被击倒后重生 / 第二形态" (the first KO is hidden from kill credit and bounty; a
|
||||
|
||||
@@ -630,7 +630,17 @@ const kitSelfFear = (ab) => [selfFear(ab)];
|
||||
* after a block ends, the warrior's own block that the knock-out releases included (Battle._stealthSwitch). */
|
||||
const kitEmber = (ab) => [husk({ hits: T(ab, 'Revive[Trigger].prop_max_hp'), delay: T(ab, 'Revive[Trigger].interval') })];
|
||||
|
||||
/**
|
||||
* 萨卡兹枯朽战士 / 组长 · death: 【污染秽蚀】 (PRTS 天赋 "{{特殊机制|死亡爆炸}}(释放半径2.0,持续8s的【污染秽蚀】)"). Silenced at its
|
||||
* death it releases nothing: PRTS 特殊机制 死亡爆炸 "如无特殊说明,此能力默认可沉默…若不处于沉默状态,将会…释放", and the
|
||||
* client agrees — its PollutedDie ability is a buff of template `projectile_on_killed` (battle prefab
|
||||
* enemy_1267_nhpbr / _2), whose ON_OWNER_KILLED first checks `CheckAbnormalFlag SILENCED` (unset) before it emits the
|
||||
* pollution projectile — the template of 高能源石虫's SILENCE-flagged blast too. Its handbook line alone is NORMAL, the one
|
||||
* exception to the handbook rule of content/enemies.js (community report of 2026-10-06, item 10: until 0.2.0 a
|
||||
* silenced one still poisoned its blockers).
|
||||
*/
|
||||
const kitPolluted = (ab) => [{
|
||||
sil: true,
|
||||
death(c, b, e) {
|
||||
if (c.reason !== 'killed') return;
|
||||
pollution(b, e, e.x, e.y, T(ab, 'PollutedDie.projectile_range') ?? 1, T(ab, 'PollutedDie.projectile_life_time') ?? 0,
|
||||
|
||||
@@ -126,23 +126,49 @@ function kitParrot(ab) {
|
||||
}];
|
||||
}
|
||||
|
||||
/**
|
||||
* 萨卡兹穿刺手 (PRTS 天赋; the battle prefab's Rush / FirstAttack abilities — templates dlancer_t_listener[a] / [b] / [c],
|
||||
* dlancer_t[trigger], dlancer_t_atk): every RUSH_LISTEN (0.1) s — 晕眩 (STUNNED) or 束缚 (UNMOVABLE) ends the acceleration
|
||||
* and its speed layers with it; not blocked and not accelerating starts it. While it accelerates, every
|
||||
* `rush.dlancer_t[trigger].interval` (0.5) s — the first one 0.5 s after the start — one more layer of move speed
|
||||
* +`move_speed` (50 %, MULTIPLIER, the layers add up), at most `trig_cnt` (25) tries — blocked or not, frozen, asleep or
|
||||
* slowed or not (only the two flags stop it). Every normal attack carries dlancer_t_atk: while it accelerates, the hit
|
||||
* adds 当前移动速度 × `firstattack.atk_scale` (600) physical damage — a second instance, melee, no 受击回复 (ignoreForSp) —
|
||||
* and ends the acceleration, so once blocked only its first hit has it. Until 0.2.0 it kept accelerating through a stun
|
||||
* and kept the speed after it, and the first hit after a block multiplied the attack instead (×(1 + 6 × layers / 25)),
|
||||
* a stun in between included (community report of 2026-10-06, item 36).
|
||||
*/
|
||||
const RUSH_LISTEN = 0.1;
|
||||
|
||||
function kitRush(ab) {
|
||||
const ms = T(ab, 'rush.dlancer_t[trigger].move_speed') ?? 0, iv = T(ab, 'rush.dlancer_t[trigger].interval') ?? 0.5;
|
||||
const max = T(ab, 'rush.dlancer_t[trigger].trig_cnt') ?? 0, first = T(ab, 'firstattack.atk_scale') ?? 0;
|
||||
const max = T(ab, 'rush.dlancer_t[trigger].trig_cnt') ?? 0, scale = T(ab, 'firstattack.atk_scale') ?? 0;
|
||||
// 晕眩 / 束缚 only (the client's STUNNED / UNMOVABLE flags: not 冻结, 沉睡 or 浮空, which hold flags.stun here too)
|
||||
const held = (e) => e.buffs.some((x) => x.status === 'stun' || x.status === 'bind');
|
||||
const stop = (b, e, a) => { a.on = false; a.n = 0; b.removeBuff(e, 'ab:rush'); };
|
||||
return [{
|
||||
iv,
|
||||
spawn(b, e, a) { a.n = 0; },
|
||||
tick(b, e, a) {
|
||||
if (!e.moving || e.blockedBy || a.n >= max) return;
|
||||
a.n++;
|
||||
b.addBuff(e, { key: 'ab:rush', mods: { moveMul: 1 + ms * a.n }, persist: true });
|
||||
iv: RUSH_LISTEN,
|
||||
// the listeners first look at once (waitFirstTriggerInterval 0): a walker spawned unblocked starts at t = 0
|
||||
spawn(b, e, a) { a.on = !e.blockedBy && !held(e); a.n = 0; a.tries = 0; a.acc = 0; },
|
||||
tick(b, e, a, dt) {
|
||||
if (held(e)) { if (a.on) stop(b, e, a); return; }
|
||||
if (!a.on) {
|
||||
if (!e.blockedBy) { a.on = true; a.tries = 0; a.acc = 0; }
|
||||
return;
|
||||
}
|
||||
if (a.tries >= max || !(iv > 0)) return;
|
||||
a.acc += dt;
|
||||
let grew = false;
|
||||
while (a.acc >= iv - 1e-9 && a.tries < max) { a.acc -= iv; a.tries++; a.n++; grew = true; }
|
||||
if (grew) b.addBuff(e, { key: 'ab:rush', mods: { moveMul: 1 + ms * a.n }, persist: true });
|
||||
},
|
||||
blocked(c, b, e, a) { a.charge = a.n; },
|
||||
hitOut(c, b, e, a) {
|
||||
if (!c.dmg.isAttack || !(a.charge > 0) || !(max > 0)) return;
|
||||
c.dmg.amount *= 1 + (first / 100) * (a.charge / max); // 被阻挡后的首次攻击: extra damage from the built-up speed
|
||||
dealt(c, b, e, a) {
|
||||
if (!a.on) return;
|
||||
const t = c.target;
|
||||
const amount = e.s.moveSpeed * scale;
|
||||
stop(b, e, a);
|
||||
if (t && t.alive && amount > 0) b.dealDamage(e, t, { amount, type: 'phys', canDodge: false, isSkill: true, noSp: true, tags: ['enemyAbility', 'rush'] });
|
||||
},
|
||||
attack(c, b, e, a) { if (a.charge > 0) { a.charge = 0; a.n = 0; b.removeBuff(e, 'ab:rush'); } },
|
||||
}];
|
||||
}
|
||||
|
||||
@@ -378,7 +404,11 @@ export const SPECIAL_KITS = Object.freeze({
|
||||
b.fx('explode', { x: e.x, y: e.y, r, kind: 'heal' });
|
||||
for (const o of b.enemiesInRadius(e.x, e.y, r)) if (o !== e) b.heal(e, o, amt);
|
||||
} }],
|
||||
enemy_10162_mnctpt: (ab) => [{ // 自制投石机 · 3-hit attacks with small splash
|
||||
enemy_10162_mnctpt: (ab) => [{ // 自制投石机 · 3-hit attacks with small splash; 索敌不受阻挡影响
|
||||
// PRTS 天赋 「索敌不受阻挡影响,且不会因丢失目标而结束攻击」: its target selection ignores its block — a 隐匿 blocker (an
|
||||
// operator on the 排气格栅) is no target, so with nobody else in range it does not attack (the official game, community
|
||||
// report of 2026-10-06, item 24); it picks by 仇恨值, not its blocker first
|
||||
spawn(b, e) { e.profile.blockFree = true; },
|
||||
dealt(c, b, e) {
|
||||
const n = T(ab, 'Attack.attack@times') ?? 1, r = T(ab, 'Attack.attack@projectile_range') ?? 0;
|
||||
const t = c.target;
|
||||
|
||||
Reference in new issue
Block a user