feedback2 WK: a ranged enemy's attack stand holds only its walking — route WAIT legs keep running and DISAPPEAR / APPEAR legs still happen, hiding ends the stand; the [ASSUMED] form-clip note names 扎罗 and 特战术师; 暴鸰's hover comment (review of GitHub #58)
This commit is contained in:
1 parent
7757db0779
commit
130266553d
4 files changed
+70
-10
No files matched your search
+2
-1
@@ -279,7 +279,8 @@ halving — PRTS: a map effect —, 【国度】, a chimera's 源石污染区 au
|
|||||||
and, unblocked, stand for each attack's clip — `attackStand`: data/enemies.json `attackAnim`, through its wind-up
|
and, unblocked, stand for each attack's clip — `attackStand`: data/enemies.json `attackAnim`, through its wind-up
|
||||||
(cooldown ≤ the strike frame, a target in range) and the rest of the clip after the strike, shortened when the attacks
|
(cooldown ≤ the strike frame, a target in range) and the rest of the clip after the strike, shortened when the attacks
|
||||||
come quicker than the clip; `ATTACK_PAUSE` (0.35 s) after the strike when no clip is known; an `attackMoves`
|
come quicker than the clip; `ATTACK_PAUSE` (0.35 s) after the strike when no clip is known; an `attackMoves`
|
||||||
(「不停止移动」) enemy never stops; a stun ends the stand; GitHub #58 — then walk on; `fear`/`disarm` stop attacks; `dmgType 'none'` enemies
|
(「不停止移动」) enemy never stops; a stun ends the stand; only the walking waits — a route WAIT keeps running and
|
||||||
|
DISAPPEAR / APPEAR legs still happen, hiding ends the stand; GitHub #58 — then walk on; `fear`/`disarm` stop attacks; `dmgType 'none'` enemies
|
||||||
never attack — unless content arms them through `enemy.profile` (`noAttack: false`, `melee`, `dmgType`, `maxTargets`:
|
never attack — unless content arms them through `enemy.profile` (`noAttack: false`, `melee`, `dmgType`, `maxTargets`:
|
||||||
转译基底·α's 寻仇者 / 特战术师 forms, which then attack like any enemy); `dmgType 'heal'` enemies heal the lowest-HP% enemy in their radius instead. A `noMove` enemy stands (not `moving`, drawn idle). Content can take over an
|
转译基底·α's 寻仇者 / 特战术师 forms, which then attack like any enemy); `dmgType 'heal'` enemies heal the lowest-HP% enemy in their radius instead. A `noMove` enemy stands (not `moving`, drawn idle). Content can take over an
|
||||||
enemy's attack: `enemy.profile.deferHit` = the engine makes the attack (target, timing, the `'atk'` event) but deals no
|
enemy's attack: `enemy.profile.deferHit` = the engine makes the attack (target, timing, the `'atk'` event) but deals no
|
||||||
|
|||||||
+17
-7
@@ -465,17 +465,18 @@ export function updateEnemy(b, e, dt) {
|
|||||||
}
|
}
|
||||||
if (!e.hidden && b._checkBlock(e)) return;
|
if (!e.hidden && b._checkBlock(e)) return;
|
||||||
if (b.time < e.pauseUntil) return;
|
if (b.time < e.pauseUntil) return;
|
||||||
// standing for an attack clip (attackStand, GitHub #58): drawn idle — the client plays the clip, then Move again; a
|
// standing for an attack clip (attackStand, GitHub #58): only the walking waits — a checkpoint's WAIT keeps running
|
||||||
// 恐惧 runs at once (it cannot attack)
|
// and DISAPPEAR / APPEAR legs still happen (advanceRoute); drawn idle (the client plays the clip, then Move again);
|
||||||
if (winding || (b.time < e.atkStandUntil && !e.s.flags.fear)) { e.moving = false; return; }
|
// a 恐惧 runs at once (it cannot attack)
|
||||||
|
const standing = winding || (b.time < e.atkStandUntil && !e.s.flags.fear);
|
||||||
if (e.s.flags.noMove) { e.moving = false; return; } // standing (a 重生, a form change): drawn idle, not walking
|
if (e.s.flags.noMove) { e.moving = false; return; } // standing (a 重生, a form change): drawn idle, not walking
|
||||||
// 恐惧 (ba.fear "无法被阻挡并四散逃跑"; PRTS 诱发移动: 恐惧 outranks 诱导): runs to random tiles of the fan away from
|
// 恐惧 (ba.fear "无法被阻挡并四散逃跑"; PRTS 诱发移动: 恐惧 outranks 诱导): runs to random tiles of the fan away from
|
||||||
// its source — a self-inflicted fear flutters inside its own tile (fear.js); the route re-plans once it ends
|
// its source — a self-inflicted fear flutters inside its own tile (fear.js); the route re-plans once it ends
|
||||||
if (e.s.flags.fear && !e.hidden) { moveFeared(b, e, dt); return; }
|
if (e.s.flags.fear && !e.hidden) { moveFeared(b, e, dt); return; }
|
||||||
if (e.mem.fearMove) endFear(e);
|
if (e.mem.fearMove) endFear(e);
|
||||||
// 诱导 (ba.attract "无法被阻挡并向目标位置移动"): walks to the attract point instead of following its route
|
// 诱导 (ba.attract "无法被阻挡并向目标位置移动"): walks to the attract point instead of following its route
|
||||||
if (e.s.flags.attract) { moveAttracted(b, e, dt); return; }
|
if (e.s.flags.attract) { if (standing) e.moving = false; else moveAttracted(b, e, dt); return; }
|
||||||
advanceRoute(b, e, dt, R);
|
advanceRoute(b, e, dt, R, standing);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -520,7 +521,11 @@ function moveAttracted(b, e, dt) {
|
|||||||
if (moved && e.route) e.route.pts = null;
|
if (moved && e.route) e.route.pts = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function advanceRoute(b, e, dt, R) {
|
/**
|
||||||
|
* One tick along the route legs. `standing` (an attack clip, attackStand): the time-based legs go on — a WAIT runs
|
||||||
|
* down, DISAPPEAR / APPEAR happen (leaving the field ends the clip) — and a MOVE leg holds (GitHub #58 review).
|
||||||
|
*/
|
||||||
|
function advanceRoute(b, e, dt, R, standing = false) {
|
||||||
let budget = dt;
|
let budget = dt;
|
||||||
let guard = 16;
|
let guard = 16;
|
||||||
while (budget > 1e-9 && guard-- > 0 && e.alive) {
|
while (budget > 1e-9 && guard-- > 0 && e.alive) {
|
||||||
@@ -537,6 +542,7 @@ function advanceRoute(b, e, dt, R) {
|
|||||||
}
|
}
|
||||||
if (leg.t === 'disappear') {
|
if (leg.t === 'disappear') {
|
||||||
b._setHidden(e, true);
|
b._setHidden(e, true);
|
||||||
|
e.atkStandUntil = -Infinity; standing = false; // off the field: its attack clip is over
|
||||||
R.legIdx++; R.pts = null;
|
R.legIdx++; R.pts = null;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -547,6 +553,7 @@ function advanceRoute(b, e, dt, R) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// move
|
// move
|
||||||
|
if (standing) { e.moving = false; return; } // the walking waits for the attack clip
|
||||||
if (!R.pts || R.version !== b.grid.version) planLeg(b, e, leg);
|
if (!R.pts || R.version !== b.grid.version) planLeg(b, e, leg);
|
||||||
const speed = e.s.moveSpeed * MOVE_SCALE;
|
const speed = e.s.moveSpeed * MOVE_SCALE;
|
||||||
if (speed <= 0) { e.moving = false; return; }
|
if (speed <= 0) { e.moving = false; return; }
|
||||||
@@ -587,7 +594,10 @@ function advanceRoute(b, e, dt, R) {
|
|||||||
* plays for its attacks and its strike frame (tools/build-data.mjs, from the asset manifest) —, `hit` of it before
|
* plays for its attacks and its strike frame (tools/build-data.mjs, from the asset manifest) —, `hit` of it before
|
||||||
* the strike (the wind-up, while a target is in range) and the rest after it, both shortened when the attacks come
|
* the strike (the wind-up, while a target is in range) and the rest after it, both shortened when the attacks come
|
||||||
* quicker than the clip (it then plays faster: render/spine.js, the same rule) — no source gives a length. An enemy in
|
* quicker than the clip (it then plays faster: render/spine.js, the same rule) — no source gives a length. An enemy in
|
||||||
* another form (掠海漂移体's crawl, 转译基底's forms, 杰斯顿's second form) stands for its base clip [ASSUMED].
|
* another form (掠海漂移体's crawl, 转译基底's forms, 杰斯顿's second form) stands for its base clip [ASSUMED]; known
|
||||||
|
* mismatches: 扎罗's second form stands 1.433 s (A_Attack) though its B_Attack lasts 2.0 s (2.5 s interval: ~0.4 s of
|
||||||
|
* walking during the recovery), 转译基底's 特战术师 stands 2.0 s (B_Attack) though its D_Attack lasts 1.667 s — a per-form
|
||||||
|
* clip needs the client's FORMS mapping (render/units.js) in shared data.
|
||||||
* Returns the wind-up and rest (s) into `out`: an enemy with no attack clip known stands ATTACK_PAUSE after the strike
|
* Returns the wind-up and rest (s) into `out`: an enemy with no attack clip known stands ATTACK_PAUSE after the strike
|
||||||
* (the old rule), a 「不停止移动」 one (`attackMoves`: data or content) never stops.
|
* (the old rule), a 「不停止移动」 one (`attackMoves`: data or content) never stops.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1742,8 +1742,9 @@ function kitRoar(ab) {
|
|||||||
* ×boomb.move_speed, and it flies on. A stun / freeze / sleep before the release interrupts the cast (Boomb
|
* ×boomb.move_speed, and it flies on. A stun / freeze / sleep before the release interrupts the cast (Boomb
|
||||||
* `_immuneStunWhenAffecting` 0, like the other enemy channels here): nothing leaves the drone, it keeps its bomb and casts
|
* `_immuneStunWhenAffecting` 0, like the other enemy channels here): nothing leaves the drone, it keeps its bomb and casts
|
||||||
* again after the skill's cooldown (1 s, counted from the interrupt) once it is free and an operator is in range.
|
* again after the skill's cooldown (1 s, counted from the interrupt) once it is free and an operator is in range.
|
||||||
* [ASSUMED] no drop when the drone is dead at the release; the ATK at the release; the hover through the cast (the
|
* [ASSUMED] no drop when the drone is dead at the release; the ATK at the release; the hover through the cast (its own
|
||||||
* engine stands a ranged enemy for its attack clip, ai.js attackStand; the drop is the drone's only attack-like cast); the
|
* hold, `pauseUntil`: the drone has no normal attack — `noAttack` —, so ai.js attackStand, the stand of a ranged enemy
|
||||||
|
* for its attack clip, never runs for it; the drop is its only attack-like cast); the
|
||||||
* bomb-less look from the release (the bomb leaves the drone on that frame of the Attack clip; at the cast end — 1 tick
|
* bomb-less look from the release (the bomb leaves the drone on that frame of the Attack clip; at the cast end — 1 tick
|
||||||
* before the clip ends — the client would draw the bomb back for a frame); an interrupted cast does not use up the one
|
* before the clip ends — the client would draw the bomb back for a frame); an interrupted cast does not use up the one
|
||||||
* trigger (`_maxTriggerTime` 1 — the bomb is still on the drone); a stun after the release does not stop the cast end.
|
* trigger (`_maxTriggerTime` 1 — the bomb is still on the drone); a stun after the release does not stop the cast end.
|
||||||
|
|||||||
@@ -126,3 +126,51 @@ test('no attack clip known: ATTACK_PAUSE after the strike and no wind-up stand;
|
|||||||
h.run(0.3);
|
h.run(0.3);
|
||||||
assert.notEqual(e.x, x0, 'walks once the stun is over');
|
assert.notEqual(e.x, x0, 'walks once the stun is over');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// review: a stand holds only the walking — a route's WAIT keeps running and DISAPPEAR / APPEAR still happen
|
||||||
|
// (data/waves.json has 122 WAIT and 99 DISAPPEAR checkpoints); it used to hold every leg, so a wait ran long by each
|
||||||
|
// clip and never ended for an enemy attacking quicker than its clip
|
||||||
|
function routeField(key, bat, dur, checkpoints, ally) {
|
||||||
|
const rec = { ...enemyRec({ key, hp: 1e9, atk: 1, range: 3, bat, speed: 1 }), attackAnim: { clip: 'Attack', dur, hit: dur / 2 } };
|
||||||
|
return makeBattle({
|
||||||
|
defs: { chess: { t_a: chessRec({ id: 't_a', stats: { maxHp: 1e9, atk: 0, def: 1e6 } }) }, enemies: { [key]: rec } },
|
||||||
|
units: ally ? [{ chessId: 't_a', row: 10, col: 7 }] : [], routes: [{ motion: 'WALK', start: [9, 8], end: [9, 2], checkpoints }],
|
||||||
|
enemies: [{ key, time: 0, route: 0 }], content: 'none', autoFinish: false, timeLimit: 60, hooks: ['attack'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test('a ranged enemy with a target in range on a WAIT checkpoint leaves when the wait (and its current clip) is over, as without a target', () => {
|
||||||
|
const leaves = (bat, dur, ally) => {
|
||||||
|
const h = routeField('enemy_waiter', bat, dur, [{ type: 'WAIT', time: 3 }], ally);
|
||||||
|
let waited = null, moved = null, x0 = null;
|
||||||
|
for (let i = 0; i < Math.round(8 / TICK); i++) {
|
||||||
|
h.step();
|
||||||
|
const e = h.enemies()[0];
|
||||||
|
x0 ??= e.x;
|
||||||
|
if (waited == null && e.route.legIdx >= 1) waited = h.b.time;
|
||||||
|
if (moved == null && Math.abs(e.x - x0) > 1e-6) moved = h.b.time;
|
||||||
|
}
|
||||||
|
return { waited, moved, attacks: h.hooksOf('attack').length };
|
||||||
|
};
|
||||||
|
const free = leaves(2.7, 1, false), sniper = leaves(2.7, 1, true), quick = leaves(1, 2, true);
|
||||||
|
assert.equal(free.attacks, 0);
|
||||||
|
assert.ok(sniper.attacks >= 2 && quick.attacks >= 5);
|
||||||
|
assert.ok(Math.abs(sniper.waited - free.waited) < 1e-9 && Math.abs(quick.waited - free.waited) < 1e-9, `the wait ends at ${free.waited.toFixed(2)} s for all three`);
|
||||||
|
// it walks on once the attack clip running at the end of the wait is over (the attack at 2.7 s: until 3.2 s)
|
||||||
|
assert.ok(sniper.moved > free.moved && sniper.moved <= 3.2 + 2 * TICK, `leaves at ${sniper.moved?.toFixed(2)} s (no target: ${free.moved.toFixed(2)} s)`);
|
||||||
|
assert.equal(quick.moved, null, 'an enemy attacking quicker than its clip stands while the target stays in range');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('DISAPPEAR / APPEAR legs proceed while an enemy stands for its attacks; leaving the field ends the stand', () => {
|
||||||
|
const h = routeField('enemy_ghost', 1, 2, [{ type: 'WAIT', time: 1 }, { type: 'DISAPPEAR' }, { type: 'WAIT', time: 1 }, { type: 'APPEAR', pos: [9, 4] }], true);
|
||||||
|
let hiddenAt = null, appeared = null;
|
||||||
|
for (let i = 0; i < Math.round(4 / TICK); i++) {
|
||||||
|
h.step();
|
||||||
|
const e = h.enemies()[0];
|
||||||
|
if (hiddenAt == null && e.hidden) { hiddenAt = h.b.time; assert.equal(e.atkStandUntil, -Infinity, 'no stand left once hidden'); }
|
||||||
|
if (appeared == null && hiddenAt != null && !e.hidden) appeared = { t: h.b.time, x: e.x, y: e.y };
|
||||||
|
}
|
||||||
|
assert.ok(h.hooksOf('attack').length >= 1, 'it attacked before leaving');
|
||||||
|
assert.ok(hiddenAt != null && Math.abs(hiddenAt - 1) < 2 * TICK, `disappears after its 1 s wait (${hiddenAt})`);
|
||||||
|
assert.ok(appeared && Math.abs(appeared.t - 2) < 2 * TICK && Math.abs(appeared.x - 4) < 0.05 && appeared.y === 9, `appears on (9,4) 1 s later (${JSON.stringify(appeared)})`);
|
||||||
|
});
|
||||||
Reference in new issue
Block a user