feedback1f WU: units on fenced tiles block no ground enemy (F4: 深巡 on a 围墙 tile attacks as usual)
F4 "干员深巡放在围栏围着的地块里不会主动攻击" is not reproduced: 深巡 (哨戒铁卫, range 2-2) attacks every enemy on her range tiles from the fenced 围墙 tiles of every active stage, the Final Assault halves and 联防, through a real Match and in the browser's own battle. Locked by test/sim/feedback1f-fence.test.js. Nearby rule fixed: PRTS 围墙 / 围栏 地形机制 "部署在其中的单位,若当前阻挡类型为'地面阻挡'则无法阻挡敌人". A unit on a tile ground units cannot pass now blocks no ground enemy (Battle._blockerFor); before, an enemy pushed or dragged to the fence edge (0.5 from the unit) was blocked there - 薄绿 S2 on a fenced tile held the dragged enemies. Air blocking (blockFly against flyers) stays. Docs: SIM.md Blocking, PLAYING.md tip on the fenced tiles.
This commit is contained in:
1 parent
7f134737ce
commit
e01d24e0c5
4 files changed
+186
-9
No files matched your search
@@ -132,3 +132,4 @@
|
||||
- 进入隐秘核心看的是全队的**激活层数总和**:后期可以针对性地堆层数。
|
||||
- 层数到 999 就满了,多余的叠层没有用,可以把资源转去别的盟约。
|
||||
- 领袖战注意**限伤**:单次 30 万以上的伤害会被整次作废。盟约叠到 999 层后,再叠加额外的伤害倍率(例如 999 层谢拉格再点亮奥术)反而可能让大招打出 0 伤害;队友的奥术等减益也会放大你的伤害。
|
||||
- **橙色围栏围起的地块**(官方「围墙」)可以部署干员,敌人走不进去:站在里面的干员照常攻击自己攻击范围内的敌人(摆放时选好朝向,让攻击范围对着敌人走的路),但**挡不住地面敌人**(被推、拉到围栏边的敌人也不会被它阻挡),也很少挨打——重装干员(如深巡)的手动技能要受到伤害才释放,在里面可能一直不放;只打自己脚下一格的干员(如角峰)在里面打不到任何敌人。
|
||||
+11
-6
@@ -176,12 +176,17 @@ to a new pick. A `wait` leg keeps counting; when the fear ends the route re-plan
|
||||
user playtest #5 item 4): an unblocked, blockable enemy is blocked by an ally (or device) whose centre is within its
|
||||
block radius of the enemy's position — `constants.js BLOCK_RADIUS`: ground 0.7071 (compared as d² < 0.49999037, the
|
||||
tile's circumscribed circle), air 0.8944 (blockFly units against flyers), devices 0.4472 — while that blocker has free
|
||||
capacity for the enemy's `blockWeight` (data `blockCnt`). It is checked every tick for every unblocked enemy, moving or
|
||||
not: an enemy that overlaps an operator when its blocker dies / is withdrawn / is stunned, or when the operator's
|
||||
blocked enemy dies, is taken over at once; an enemy that finds no room walks on (pass-through). Several blockers in
|
||||
contact → the nearest [ASSUMED]. A head-on enemy therefore stops at contact, ~0.71 tile from the blocker's centre, on the
|
||||
tile in front of it (PRTS 作战机制: a blocked enemy's collider does not enter the blocker's tile; the official few
|
||||
hundredths of a tile of deceleration are not modelled), and **every blocker** — melee units (要塞 / 领主 / 哨戒铁卫
|
||||
capacity for the enemy's `blockWeight` (data `blockCnt`). A unit standing on a tile ground units cannot pass — the
|
||||
fenced 围墙 / 围栏 tiles (`b`: low, deployable, flyers only) — blocks no ground enemy (PRTS 围墙 / 围栏 地形机制 "部署在
|
||||
其中的单位,若当前阻挡类型为'地面阻挡'则无法阻挡敌人"; `Battle._blockerFor`): nothing walks onto those tiles, but a push or
|
||||
pull leaves an enemy at the fence edge, 0.5 from the unit — it walks on (community report F4 after 0.1.0: 薄绿 S2 on a
|
||||
fenced tile held the dragged enemies there; test/sim/feedback1f-fence.test.js); air blocking (blockFly against flyers)
|
||||
stays; a unit on a fenced tile still attacks whatever stands on its range tiles. It is checked every tick for every
|
||||
unblocked enemy, moving or not: an enemy that overlaps an operator when its blocker dies / is withdrawn / is stunned,
|
||||
or when the operator's blocked enemy dies, is taken over at once; an enemy that finds no room walks on (pass-through).
|
||||
Several blockers in contact → the nearest [ASSUMED]. A head-on enemy therefore stops at contact, ~0.71 tile from the
|
||||
blocker's centre, on the tile in front of it (PRTS 作战机制: a blocked enemy's collider does not enter the blocker's
|
||||
tile; the official few hundredths of a tile of deceleration are not modelled), and **every blocker** — melee units (要塞 / 领主 / 哨戒铁卫
|
||||
included), summons (流形's melee copy) and a ranged operator standing on a melee tile alike — may always target the
|
||||
enemies it blocks, in range or not, whatever its facing, and targets them first ("可以选择且优先选择阻挡单位", PRTS 选择器;
|
||||
the user's rule after playtest #6, "阻挡了就一定要能打到": officially the collision pushes a blocked enemy to its blocker's
|
||||
|
||||
+11
-3
@@ -1066,7 +1066,8 @@ export class Battle {
|
||||
* Try to block enemy `e` where it stands. Returns true if it is (now) blocked. Official contact rule (constants.js
|
||||
* BLOCK_RADIUS, PRTS 游戏数据基础 §阻挡半径): the enemy's position within the blocker's block radius of the blocker's
|
||||
* centre (ground 0.7071 — the tile's circumscribed circle, reaching 0.21 tile into the side neighbours; air 0.8944;
|
||||
* devices 0.4472), and the blocker has free capacity for the enemy's block weight. Checked every tick for every
|
||||
* devices 0.4472), and the blocker has free capacity for the enemy's block weight (and may block it at all: a unit on
|
||||
* a fenced tile blocks no ground enemy, _blockerFor). Checked every tick for every
|
||||
* unblocked enemy, moving or not, so an enemy overlapping an operator is taken over as soon as its blocker is gone or
|
||||
* the operator's capacity frees up (user playtest #5 item 4). Several blockers in contact → the nearest [ASSUMED],
|
||||
* ties → the first in row-then-column scan order.
|
||||
@@ -1113,11 +1114,18 @@ export class Battle {
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Can ally/device `u` block enemy `e` (block weight `w`) right now — everything but the contact distance. */
|
||||
/**
|
||||
* Can ally/device `u` block enemy `e` (block weight `w`) right now — everything but the contact distance. A unit on a
|
||||
* tile ground units cannot pass blocks no ground enemy: the fenced tiles (围墙 tile_fence_bound / 围栏 tile_fence —
|
||||
* low ground, deployable, passable to flyers only) — PRTS 围墙 / 围栏 地形机制 "部署在其中的单位,若当前阻挡类型为'地面
|
||||
* 阻挡'则无法阻挡敌人". Air blocking (blockFly against flyers) is not ground blocking and stays. Nothing walks onto such
|
||||
* a tile, so it matters for an enemy pushed or pulled against the fence (Battle.displace stops it at the tile edge,
|
||||
* 0.5 from the fenced unit — inside the ground block radius).
|
||||
*/
|
||||
_blockerFor(u, e, w) {
|
||||
if (!u.alive || !u.deployed || u.hidden || u.s.flags.noBlock || u.s.flags.sleep) return false;
|
||||
if (e.isFlying && !(u.s.flags.blockFly || (u.profile && u.profile.blockFly))) return false;
|
||||
if (!e.isFlying && !u.ground) return false;
|
||||
if (!e.isFlying && (!u.ground || this.grid.tile(u.tileR, u.tileC).pass !== 'ALL')) return false;
|
||||
const cap = u.s.blockCnt;
|
||||
if (cap <= 0) return false;
|
||||
let used = 0;
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
// Community report F4 after 0.1.0 — "干员深巡放在围栏围着的地块里不会主动攻击" (深巡, 哨戒铁卫, on a fenced tile).
|
||||
//
|
||||
// The fenced tiles are the official 围墙 (tile_fence_bound; gamedata stage_table tileInfo "可放置近战单位,敌方无法进入"):
|
||||
// low ground, deployable, passable to flyers only, drawn with an orange railing around each fenced region (the normal
|
||||
// fields of act1 m01–m04 / act2 m02–m04, the Final Assault halves, act2 m01's 1×3 pipe tiles).
|
||||
//
|
||||
// Not reproduced: 深巡's trait "能够阻挡三个敌人,可以进行远程攻击" (character_table char_4137_udflow, range 2-2 = her tile and
|
||||
// the two in front) — she attacks every enemy on those tiles from a fenced tile like from any other: the sim targets by
|
||||
// range tiles (no line-of-sight, path or blocking requirement for an attack). Locked below on every deployable fenced
|
||||
// tile of every active stage, every direction that looks at walkable ground, normal and elite.
|
||||
//
|
||||
// Nearby rule fixed: PRTS 围墙 / 围栏 地形机制 "部署在其中的单位,若当前阻挡类型为'地面阻挡'则无法阻挡敌人" — a unit on a fenced
|
||||
// tile blocks no ground enemy (Battle._blockerFor). Nothing walks onto such a tile, but a push / pull stops an enemy at
|
||||
// the tile edge, 0.5 from the fenced unit — inside the ground block radius 0.7071 — and 0.1.0 let the fenced unit block
|
||||
// it there. Air blocking (blockFly against flyers) is not ground blocking and stays.
|
||||
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import { makeBattle } from '../helpers/battleHarness.js';
|
||||
import { canTargetEnemy } from '../../server/sim/targeting.js';
|
||||
import { bodyInKeys } from '../../server/sim/body.js';
|
||||
import { effectiveProfile } from '../../server/sim/ai.js';
|
||||
import { rotateOffset } from '../../server/sim/dir.js';
|
||||
|
||||
const STAGES = JSON.parse(fs.readFileSync(new URL('../../data/stages.json', import.meta.url), 'utf8'));
|
||||
const ACTIVE = Object.keys(STAGES).filter((id) => STAGES[id].active && STAGES[id].weight > 0);
|
||||
const UDFLOW = 'chess_char_1_04_a', UDFLOW_E = 'chess_char_1_04_b';
|
||||
const DIRS = ['RIGHT', 'UP', 'LEFT', 'DOWN'];
|
||||
const glyph = (st, r, c) => st.rows[r]?.[c];
|
||||
const walkable = (st, r, c) => r >= 9 && r <= 12 && c >= 0 && c <= 10 && !!st.tiles[glyph(st, r, c)]?.groundPassable;
|
||||
|
||||
/** Deployable fenced tiles of a stage's normal field (board = field coordinates there). */
|
||||
const fencedTiles = (st) => st.deployTiles.normal.melee.filter(([r, c]) => glyph(st, r, c) === 'b');
|
||||
|
||||
/** Directions whose 2-2 line (her tile + two ahead) reaches walkable ground from (r, c). */
|
||||
const laneDirs = (st, r, c) => DIRS.filter((d) => [[0, 1], [0, 2]].some(([dr, dc]) => { const [a, b] = rotateOffset(dr, dc, d); return walkable(st, r + a, c + b); }));
|
||||
|
||||
/**
|
||||
* One battle: the operator alone on (r, c) facing `dir` against a real round template. Returns its attacks, the ticks a
|
||||
* targetable enemy stood on its range tiles, the longest wait for an attack while one did and it could act (seconds
|
||||
* beyond its attack interval) and the ticks it blocked anything.
|
||||
*/
|
||||
function watch(stageId, chessId, r, c, dir, { template = 'act1autochess_02', seconds = 45 } = {}) {
|
||||
const h = makeBattle({ stageId, waveTemplate: template, units: [{ chessId, row: r, col: c, dir }], seed: 5 });
|
||||
h.step(1);
|
||||
const u = h.unit(chessId);
|
||||
assert.ok(u && u.deployed && u.tileR === r && u.tileC === c, `${stageId} (${r},${c}) deployed`);
|
||||
let attacks = 0, inRange = 0, blocked = 0, waitFrom = null, worst = 0;
|
||||
h.b.on('attack', (ctx) => { if (ctx.attacker === u) { attacks++; waitFrom = null; } });
|
||||
for (let i = 0; i < seconds * 30 && !h.b.finished; i++) {
|
||||
h.step(1);
|
||||
if (u.blocking.length) blocked++;
|
||||
if (!u.alive || !u.deployed || !u.canAct || u.s.flags.disarm) { waitFrom = null; continue; }
|
||||
const prof = effectiveProfile(u);
|
||||
const set = u.rangeKeySet || new Set(u.rangeKeys);
|
||||
const target = h.b.enemies.some((e) => e.alive && e.deployed && canTargetEnemy(u, e, prof) && bodyInKeys(e, set));
|
||||
if (!target) { waitFrom = null; continue; }
|
||||
inRange++;
|
||||
if (waitFrom == null) waitFrom = h.b.time;
|
||||
worst = Math.max(worst, h.b.time - waitFrom - u.s.interval);
|
||||
}
|
||||
return { attacks, inRange, blocked, worst };
|
||||
}
|
||||
|
||||
test('F4 (not reproduced): 深巡 on every deployable fenced tile of every active stage attacks the enemies on her range tiles', () => {
|
||||
let cases = 0, withTargets = 0;
|
||||
for (const id of ACTIVE) {
|
||||
const st = STAGES[id];
|
||||
for (const [r, c] of fencedTiles(st)) {
|
||||
for (const dir of laneDirs(st, r, c)) {
|
||||
for (const chessId of [UDFLOW, UDFLOW_E]) {
|
||||
const w = watch(id, chessId, r, c, dir);
|
||||
const where = `${chessId} ${id} (${r},${c}) ${dir}: ${JSON.stringify(w)}`;
|
||||
cases++;
|
||||
// never idle with a target: an attack at the latest one interval (+ 0.2 s) after an enemy is on her tiles
|
||||
assert.ok(w.worst <= 0.2, `idle with a target in range — ${where}`);
|
||||
if (w.inRange > 0) { withTargets++; assert.ok(w.attacks > 0, `no attack — ${where}`); }
|
||||
// nothing walks onto a fenced tile or within her reach there
|
||||
assert.equal(w.blocked, 0, `blocked from a fenced tile — ${where}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.ok(cases >= 150, `${cases} placements`);
|
||||
assert.ok(withTargets >= cases * 0.8, `${withTargets} of ${cases} placements saw enemies on her range tiles`);
|
||||
});
|
||||
|
||||
test('F4: the Final Assault fenced tiles (act2 m01 boss (3–5, 8), act1 m01 boss (4,7)) — 深巡 attacks the leaders and their escorts', () => {
|
||||
const W = JSON.parse(fs.readFileSync(new URL('../../data/waves.json', import.meta.url), 'utf8'));
|
||||
const run = (stageId, tpl, r, c, dir) => {
|
||||
const h = makeBattle({ kind: W[tpl].kind, stageId, waveTemplate: tpl, timeLimit: 20, autoFinish: false,
|
||||
sharedBoss: { hp: 1e9, maxHp: 1e9, damage() {} },
|
||||
players: [{ playerId: 'p1', seat: 0, side: 'L', coords: 'field', colOffset: 0, units: [{ uid: 1, kind: 'chess', chessId: UDFLOW, row: r, col: c, dir }], bonds: {} }] });
|
||||
h.step(1);
|
||||
const u = h.unit(UDFLOW);
|
||||
let n = 0;
|
||||
h.b.on('attack', (ctx) => { if (ctx.attacker === u) n++; });
|
||||
h.run(20);
|
||||
return n;
|
||||
};
|
||||
for (const r of [3, 4, 5]) assert.ok(run('act2autochess_m01', 'act1autochess_h07_01', r, 8, 'RIGHT') > 0, `act2 m01 (${r},8): the huge leader on her line`);
|
||||
assert.equal(glyph(STAGES.act1autochess_m01, 4, 7), 'b');
|
||||
assert.ok(run('act1autochess_m01', 'act1autochess_h07_03', 4, 7, 'RIGHT') > 0, 'act1 m01 boss (4,7)');
|
||||
});
|
||||
|
||||
test('fix: a unit on a fenced tile (围墙) blocks no ground enemy pushed against the fence; on a road tile it does', () => {
|
||||
const push = (stage) => {
|
||||
const h = makeBattle({ stage, units: [{ chessId: UDFLOW, row: 11, col: 7, dir: 'RIGHT' }], autoFinish: false, timeLimit: 60, seed: 3 });
|
||||
h.step(1);
|
||||
const u = h.unit(UDFLOW);
|
||||
const e = h.spawn('enemy_1007_slime', { pos: [11, 8], routeIndex: 0 });
|
||||
h.step(2);
|
||||
assert.equal(e.blockedBy, null, 'one tile away: not in contact');
|
||||
const moved = h.b.displace(e, { x: -1, y: 0 }, 0.6);
|
||||
assert.ok(Math.hypot(e.x - 7, e.y - 11) < 0.7071, `within the ground block radius (${e.x.toFixed(3)})`);
|
||||
h.step(1);
|
||||
return { h, u, e, moved };
|
||||
};
|
||||
// act1 m01: (11,7) is a 围墙 tile of the fenced block, (11,8) the col-8 road beside it
|
||||
const fenced = STAGES.act1autochess_m01;
|
||||
assert.equal(glyph(fenced, 11, 7), 'b');
|
||||
assert.equal(fenced.tiles.b.tileKey, 'tile_fence_bound');
|
||||
const a = push(fenced);
|
||||
assert.ok(a.moved > 0.4 && a.moved < 0.6 && Math.round(a.e.x) === 8, `stopped at the fence edge (moved ${a.moved})`);
|
||||
assert.equal(a.e.blockedBy, null, 'the enemy against the fence is not blocked by the unit on the fenced tile');
|
||||
assert.equal(a.u.blocking.length, 0);
|
||||
a.h.step(30);
|
||||
assert.ok(a.e.alive && a.e.blockedBy === null && Math.hypot(a.e.x - 7.5, a.e.y - 11.05) > 0.2, 'it walks on');
|
||||
// control: the same tile made a road — the pushed enemy is blocked as before
|
||||
const road = { ...fenced, rows: fenced.rows.map((row, r) => (r === 11 ? `${row.slice(0, 7)}r${row.slice(8)}` : row)) };
|
||||
const b = push(road);
|
||||
assert.equal(b.e.blockedBy, b.u, 'on a road tile the same contact blocks');
|
||||
});
|
||||
|
||||
test('fix, real kit: 薄绿 on a 围墙 tile drags enemies against the fence and blocks none of them (0.1.0: held them there)', () => {
|
||||
// 薄绿 S2 聚能涡旋 drags enemies towards her (Battle.push inward); from act1 m01's fenced (11,7) facing the col-8 road the
|
||||
// dragged enemies end at the fence edge, in contact with her — 0.1.0 blocked them there for most of the round
|
||||
const BOLV = 'chess_char_3_08_a';
|
||||
const h = makeBattle({ stageId: 'act1autochess_m01', waveTemplate: 'act1autochess_02', units: [{ chessId: BOLV, row: 11, col: 7, dir: 'RIGHT' }], seed: 9 });
|
||||
h.step(1);
|
||||
const u = h.unit(BOLV);
|
||||
let casts = 0, contact = 0, blocked = 0;
|
||||
h.b.on('skillStart', (ctx) => { if (ctx.unit === u) casts++; });
|
||||
for (let i = 0; i < 45 * 30 && !h.b.finished; i++) {
|
||||
h.step(1);
|
||||
if (h.b.enemies.some((e) => e.alive && !e.isFlying && Math.hypot(e.x - 7, e.y - 11) < 0.7071)) contact++;
|
||||
if (u.blocking.length) blocked++;
|
||||
}
|
||||
assert.ok(casts > 0 && contact > 30, `the drag brought ground enemies into contact (${casts} casts, ${contact} ticks)`);
|
||||
assert.equal(blocked, 0, 'she blocks nothing from the fenced tile');
|
||||
});
|
||||
|
||||
test('fix: air blocking stays — a blockFly unit on a fenced tile blocks a flyer over the fence', () => {
|
||||
const h = makeBattle({ stageId: 'act1autochess_m01', units: [{ chessId: UDFLOW, row: 11, col: 7, dir: 'RIGHT' }], autoFinish: false, timeLimit: 60, seed: 3 });
|
||||
h.step(1);
|
||||
const u = h.unit(UDFLOW);
|
||||
h.b.addBuff(u, { key: 'test:blockFly', duration: Infinity, flags: { blockFly: true } });
|
||||
const e = h.spawn('enemy_1005_yokai', { pos: [11, 7], routeIndex: 2 });
|
||||
h.step(2);
|
||||
assert.ok(e.isFlying);
|
||||
assert.equal(e.blockedBy, u, 'the flyer over the fenced tile is blocked by the air blocker standing on it');
|
||||
});
|
||||
Reference in new issue
Block a user