Hit tiles: positions round half to even (PRTS 作战机制); enemies held just inside the field edge
This commit is contained in:
1 parent
bc50cac01f
commit
119a04888a
13 files changed
+113
-24
No files matched your search
+1
-1
@@ -541,7 +541,7 @@ returns their current `weight` (including modifiers). The same tick's count and
|
|||||||
list and budget (DESIGN §28.27); this is local to the kit, not a change to general displacement.
|
list and budget (DESIGN §28.27); this is local to the kit, not a change to general displacement.
|
||||||
|
|
||||||
**Hit areas (`body.js`, user playtest #5 item 10).** A regular enemy is a point: in a grid range when the tile of its
|
**Hit areas (`body.js`, user playtest #5 item 10).** A regular enemy is a point: in a grid range when the tile of its
|
||||||
position (`round(y)`, `round(x)`) is a range tile, in a radius when its position is (DESIGN §3). A huge enemy (巨型单位:
|
position (`roundHalfEven(y)`, `roundHalfEven(x)` — PRTS 作战机制 "四舍六入五成双"; `Math.round` would take 2.5 to 3) is a range tile, in a radius when its position is (DESIGN §3). A huge enemy (巨型单位:
|
||||||
`enemy.hitArea` from data/enemies.json `hitArea` — 假想敌:胄 / 管 / 盐风主教昆图斯 / 阿利斯泰尔 / “萨米的意志”, PRTS
|
`enemy.hitArea` from data/enemies.json `hitArea` — 假想敌:胄 / 管 / 盐风主教昆图斯 / 阿利斯泰尔 / “萨米的意志”, PRTS
|
||||||
"受击判定区域为长4.95、宽2.95的长方形,向上偏移1.0") is hit anywhere on a rectangle `w` × `h` tiles centred on its position
|
"受击判定区域为长4.95、宽2.95的长方形,向上偏移1.0") is hit anywhere on a rectangle `w` × `h` tiles centred on its position
|
||||||
moved `dx` columns right / `dy` rows up: it occupies every tile the rectangle overlaps (`bodyKeys` — 5 × 3 tiles; PRTS
|
moved `dx` columns right / `dy` rows up: it occupies every tile the rectangle overlaps (`bodyKeys` — 5 × 3 tiles; PRTS
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
import { ROWS, COLS } from '../constants.js';
|
import { ROWS, COLS } from '../constants.js';
|
||||||
import { absoluteRangeKeys, canTargetEnemy, extendedGrid, enemyStealthed } from '../targeting.js';
|
import { absoluteRangeKeys, canTargetEnemy, extendedGrid, enemyStealthed } from '../targeting.js';
|
||||||
import { bodyKeys, bodyInKeys, bodyInRadius } from '../body.js';
|
import { bodyKeys, bodyInKeys, bodyInRadius } from '../body.js';
|
||||||
|
import { roundHalfEven } from '../detmath.js';
|
||||||
|
|
||||||
export class BattleQueries {
|
export class BattleQueries {
|
||||||
/**
|
/**
|
||||||
@@ -21,7 +22,7 @@ export class BattleQueries {
|
|||||||
for (const k of bodyKeys(e)) { const b = this._eb[k]; if (!b.length) this._ebUsed.push(k); b.push(e); }
|
for (const k of bodyKeys(e)) { const b = this._eb[k]; if (!b.length) this._ebUsed.push(k); b.push(e); }
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const r = Math.round(e.y), c = Math.round(e.x);
|
const r = roundHalfEven(e.y), c = roundHalfEven(e.x); // the tile body.js gives a point enemy
|
||||||
if (r < 0 || r >= ROWS || c < 0 || c >= COLS) continue;
|
if (r < 0 || r >= ROWS || c < 0 || c >= COLS) continue;
|
||||||
const k = r * COLS + c;
|
const k = r * COLS + c;
|
||||||
const b = this._eb[k];
|
const b = this._eb[k];
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
// and the list compaction.
|
// and the list compaction.
|
||||||
// Installed on Battle.prototype by server/sim/Battle.js (a method container: never instantiated; `this` is the battle).
|
// Installed on Battle.prototype by server/sim/Battle.js (a method container: never instantiated; `this` is the battle).
|
||||||
|
|
||||||
import { MAX_ALIVE_ENEMIES } from '../constants.js';
|
import { MAX_ALIVE_ENEMIES, FIELD_EDGE_INSET } from '../constants.js';
|
||||||
import { Unit } from '../units.js';
|
import { Unit } from '../units.js';
|
||||||
import { compileRoute } from '../ai.js';
|
import { compileRoute } from '../ai.js';
|
||||||
import { unitInfo } from '../snapshot.js';
|
import { unitInfo } from '../snapshot.js';
|
||||||
@@ -201,11 +201,11 @@ export class BattleSpawns {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_clampPos(e) {
|
_clampPos(e) {
|
||||||
const R = this.rect;
|
const R = this.rect, m = 0.5 - FIELD_EDGE_INSET; // inside the edge: on the edge tile, never halfway (body.js)
|
||||||
if (e.x < R.c0 - 0.5) e.x = R.c0 - 0.5;
|
if (e.x < R.c0 - m) e.x = R.c0 - m;
|
||||||
if (e.x > R.c1 + 0.5) e.x = R.c1 + 0.5;
|
if (e.x > R.c1 + m) e.x = R.c1 + m;
|
||||||
if (e.y < R.r0 - 0.5) e.y = R.r0 - 0.5;
|
if (e.y < R.r0 - m) e.y = R.r0 - m;
|
||||||
if (e.y > R.r1 + 0.5) e.y = R.r1 + 0.5;
|
if (e.y > R.r1 + m) e.y = R.r1 + m;
|
||||||
}
|
}
|
||||||
|
|
||||||
_compactEnemies() {
|
_compactEnemies() {
|
||||||
|
|||||||
+7
-6
@@ -1,8 +1,9 @@
|
|||||||
// server/sim/body.js — where a unit can be hit: the ONE helper behind every range test that targets enemies (user
|
// server/sim/body.js — where a unit can be hit: the ONE helper behind every range test that targets enemies (user
|
||||||
// playtest #5 item 10: "大体型的关底boss实际受击判定范围很小…原版是很大的一片区域").
|
// playtest #5 item 10: "大体型的关底boss实际受击判定范围很小…原版是很大的一片区域").
|
||||||
//
|
//
|
||||||
// Regular units are a point: in a grid range when the tile containing their position (round(y), round(x)) is a range
|
// Regular units are a point: in a grid range when the tile containing their position is a range tile — the position
|
||||||
// tile, in a radius when their position is (DESIGN §3). Huge units (巨型单位 — enemy data `hitArea`, tools/build-data.mjs
|
// rounded half to even (detmath roundHalfEven; PRTS 作战机制 "受击方坐标经round函数取整(四舍六入五成双)得到其所属格
|
||||||
|
// 坐标") —, in a radius when their position is (DESIGN §3). Huge units (巨型单位 — enemy data `hitArea`, tools/build-data.mjs
|
||||||
// HIT_AREAS, from PRTS "巨型单位:受击判定区域为长4.95、宽2.95的长方形,向上偏移1.0") are hit anywhere on a rectangle
|
// HIT_AREAS, from PRTS "巨型单位:受击判定区域为长4.95、宽2.95的长方形,向上偏移1.0") are hit anywhere on a rectangle
|
||||||
// `w` tiles along the columns × `h` along the rows, centred on their position moved `dx` columns right and `dy` rows
|
// `w` tiles along the columns × `h` along the rows, centred on their position moved `dx` columns right and `dy` rows
|
||||||
// up:
|
// up:
|
||||||
@@ -24,7 +25,7 @@
|
|||||||
// `hitRect`).
|
// `hitRect`).
|
||||||
|
|
||||||
import { ROWS, COLS } from './constants.js';
|
import { ROWS, COLS } from './constants.js';
|
||||||
import { hypot } from './detmath.js';
|
import { hypot, roundHalfEven } from './detmath.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validated hit area of a data record (`{ w, h, dx, dy }`, tiles) or null for a regular (point) unit.
|
* Validated hit area of a data record (`{ w, h, dx, dy }`, tiles) or null for a regular (point) unit.
|
||||||
@@ -47,7 +48,7 @@ export function hitRect(u) {
|
|||||||
|
|
||||||
/** Tile key of a unit's position (-1 off the grid). */
|
/** Tile key of a unit's position (-1 off the grid). */
|
||||||
function posKey(u) {
|
function posKey(u) {
|
||||||
const r = Math.round(u.y), c = Math.round(u.x);
|
const r = roundHalfEven(u.y), c = roundHalfEven(u.x);
|
||||||
return r < 0 || r >= ROWS || c < 0 || c >= COLS ? -1 : r * COLS + c;
|
return r < 0 || r >= ROWS || c < 0 || c >= COLS ? -1 : r * COLS + c;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,7 +69,7 @@ export function bodyKeys(u) {
|
|||||||
/** Does the unit occupy tile (r, c)? */
|
/** Does the unit occupy tile (r, c)? */
|
||||||
export function bodyOnTile(u, r, c) {
|
export function bodyOnTile(u, r, c) {
|
||||||
const R = hitRect(u);
|
const R = hitRect(u);
|
||||||
if (!R) return Math.round(u.y) === r && Math.round(u.x) === c;
|
if (!R) return roundHalfEven(u.y) === r && roundHalfEven(u.x) === c;
|
||||||
return c + 0.5 > R.x0 + 1e-9 && c - 0.5 < R.x1 - 1e-9 && r + 0.5 > R.y0 + 1e-9 && r - 0.5 < R.y1 - 1e-9;
|
return c + 0.5 > R.x0 + 1e-9 && c - 0.5 < R.x1 - 1e-9 && r + 0.5 > R.y0 + 1e-9 && r - 0.5 < R.y1 - 1e-9;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,7 +101,7 @@ export function bodyInRadius(u, x, y, r) {
|
|||||||
* around it; …): "周围八格"-style square areas centred on a tile.
|
* around it; …): "周围八格"-style square areas centred on a tile.
|
||||||
*/
|
*/
|
||||||
export function bodyTileReach(u, r, c) {
|
export function bodyTileReach(u, r, c) {
|
||||||
if (!u.hitArea) return Math.max(Math.abs(Math.round(u.y) - r), Math.abs(Math.round(u.x) - c));
|
if (!u.hitArea) return Math.max(Math.abs(roundHalfEven(u.y) - r), Math.abs(roundHalfEven(u.x) - c));
|
||||||
let best = Infinity;
|
let best = Infinity;
|
||||||
for (const k of bodyKeys(u)) {
|
for (const k of bodyKeys(u)) {
|
||||||
const kr = Math.floor(k / COLS), kc = k % COLS;
|
const kr = Math.floor(k / COLS), kc = k % COLS;
|
||||||
|
|||||||
@@ -220,6 +220,15 @@ export const MAX_ALIVE_ENEMIES = 600;
|
|||||||
/** Heal-over-time / regen events are aggregated and only emitted when they reach this amount. */
|
/** Heal-over-time / regen events are aggregated and only emitted when they reach this amount. */
|
||||||
export const REGEN_EVENT_MIN = 1;
|
export const REGEN_EVENT_MIN = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How far inside the field rectangle an enemy is held (tiles; Battle._clampPos). The rectangle's edge lies exactly
|
||||||
|
* halfway between two tiles, where the tile of a position — rounded half to even (body.js; PRTS 作战机制 四舍六入五成双)
|
||||||
|
* — would depend on whether the row / column number is even. Held this little inside, an enemy at the edge is always on
|
||||||
|
* the edge tile. 2^-20 (about a millionth of a tile) is exact in binary and far above the rounding error of a position,
|
||||||
|
* yet too small to change a slide of 0.1-tile steps against the edge.
|
||||||
|
*/
|
||||||
|
export const FIELD_EDGE_INSET = 1 / 1048576;
|
||||||
|
|
||||||
/** Row offset mapping board rows (9..12) onto boss-field rows (2..5). */
|
/** Row offset mapping board rows (9..12) onto boss-field rows (2..5). */
|
||||||
export const BOSS_ROW_OFFSET = -7;
|
export const BOSS_ROW_OFFSET = -7;
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,17 @@ export function hypot(x, y) {
|
|||||||
return Math.sqrt(x * x + y * y);
|
return Math.sqrt(x * x + y * y);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* x rounded to an integer, ties to even (2.5 → 2, 3.5 → 4, −2.5 → −2): the rule PRTS 作战机制 gives for the tile of a
|
||||||
|
* target — 受击方坐标经round函数取整(四舍六入五成双)得到其所属格坐标. Math.round rounds ties up (2.5 → 3). Exact; never −0.
|
||||||
|
* @param {number} x
|
||||||
|
*/
|
||||||
|
export function roundHalfEven(x) {
|
||||||
|
const f = Math.floor(x);
|
||||||
|
if (x - f !== 0.5) return Math.round(x) + 0;
|
||||||
|
return (f % 2 === 0 ? f : f + 1) + 0;
|
||||||
|
}
|
||||||
|
|
||||||
// ---- integer powers ----------------------------------------------------------------------------------------------
|
// ---- integer powers ----------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
const SPLIT = 134217729; // 2^27 + 1 (Dekker)
|
const SPLIT = 134217729; // 2^27 + 1 (Dekker)
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
import { COLS, ROWS } from './constants.js';
|
import { COLS, ROWS } from './constants.js';
|
||||||
import { normDir, rotateOffset } from './dir.js';
|
import { normDir, rotateOffset } from './dir.js';
|
||||||
import { bodyDist } from './body.js';
|
import { bodyDist } from './body.js';
|
||||||
|
import { roundHalfEven } from './detmath.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build the absolute tile-key list of a grid for a unit standing at (r,c) facing `dir` ('UP'|'RIGHT'|'DOWN'|'LEFT';
|
* Build the absolute tile-key list of a grid for a unit standing at (r,c) facing `dir` ('UP'|'RIGHT'|'DOWN'|'LEFT';
|
||||||
@@ -50,9 +51,9 @@ export function absoluteRangeKeys(grid, r, c, dir, extend = 0) {
|
|||||||
// record range (shared/loadoutRecord.js attackRangeGrid), re-exported here for the battle (Battle._refreshRange).
|
// record range (shared/loadoutRecord.js attackRangeGrid), re-exported here for the battle (Battle._refreshRange).
|
||||||
export { extendedGrid } from '../../shared/loadoutRecord.js';
|
export { extendedGrid } from '../../shared/loadoutRecord.js';
|
||||||
|
|
||||||
/** Tile key of a unit's current position. */
|
/** Tile key of a unit's current position (rounded half to even, as body.js). */
|
||||||
export function tileKeyOf(u) {
|
export function tileKeyOf(u) {
|
||||||
const r = Math.round(u.y), c = Math.round(u.x);
|
const r = roundHalfEven(u.y), c = roundHalfEven(u.x);
|
||||||
if (r < 0 || r >= ROWS || c < 0 || c >= COLS) return -1;
|
if (r < 0 || r >= ROWS || c < 0 || c >= COLS) return -1;
|
||||||
return r * COLS + c;
|
return r * COLS + c;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,7 +50,9 @@ test('#2 麦哲伦 S1: an enemy on a standing drone\'s range (air units too) cas
|
|||||||
const { h, u, ps } = field(MG, 0, [[DRONE_F, 12, 9]]);
|
const { h, u, ps } = field(MG, 0, [[DRONE_F, 12, 9]]);
|
||||||
assert.equal(u.skill.rule, 'DEFAULT');
|
assert.equal(u.skill.rule, 'DEFAULT');
|
||||||
assert.ok(ps[0].alive && ps[0].rangeKeySet.has(12 * COLS + 10) && outOfHerRange(u, 12, 10), 'the setup');
|
assert.ok(ps[0].alive && ps[0].rangeKeySet.has(12 * COLS + 10) && outOfHerRange(u, 12, 10), 'the setup');
|
||||||
h.spawn('enemy_dummy', { pos: [11, 13] }); // outside every area
|
// on the field (rows 9–12, cols 0–10) and outside every area: (11, 13) would be held at the field's edge, on (11, 10) —
|
||||||
|
// the drone's area
|
||||||
|
h.spawn('enemy_dummy', { pos: [11, 6] });
|
||||||
assert.ok(!casts(h, u), 'nobody in her range or a drone\'s: no cast');
|
assert.ok(!casts(h, u), 'nobody in her range or a drone\'s: no cast');
|
||||||
h.spawn('enemy_dummy', { pos: [12, 10] });
|
h.spawn('enemy_dummy', { pos: [12, 10] });
|
||||||
assert.ok(h.runUntil(() => u.skill.activations === 1, 1), 'an enemy on the drone\'s range casts S1');
|
assert.ok(h.runUntil(() => u.skill.activations === 1, 1), 'an enemy on the drone\'s range casts S1');
|
||||||
|
|||||||
@@ -1119,8 +1119,9 @@ test('异客: the S3 storm is range x-1 — the 13-tile diamond — around the t
|
|||||||
};
|
};
|
||||||
// in the diamond (|Δrow| + |Δcol| ≤ 2 by tile): (2, 0) / (0, −2) / (1.95, 0.4) lay outside the old 1.5 circle
|
// in the diamond (|Δrow| + |Δcol| ≤ 2 by tile): (2, 0) / (0, −2) / (1.95, 0.4) lay outside the old 1.5 circle
|
||||||
for (const off of [[0, 0], [2, 0], [0, -2], [-2, 0], [1.45, 1.45], [1.95, 0.4]]) assert.equal(strikes(off), 8, `(${off}) struck by all 8 strikes`);
|
for (const off of [[0, 0], [2, 0], [0, -2], [-2, 0], [1.45, 1.45], [1.95, 0.4]]) assert.equal(strikes(off), 8, `(${off}) struck by all 8 strikes`);
|
||||||
// outside it: tile (+2, +1), and tile (+3, 0) — (1.55, 1.05) was inside the old circle around the target's position
|
// outside it: tile (+2, +1), and tile (+3, 0) — (1.55, 1.05) was inside the old circle around the target's position;
|
||||||
for (const off of [[2, 1], [1.55, 1.05], [0, 2.6]]) assert.equal(strikes(off), 0, `(${off}) never struck`);
|
// three tiles off along the columns, as the field has no row 13 (a probe there is held on row 12, inside the diamond)
|
||||||
|
for (const off of [[2, 1], [1.55, 1.05], [2.6, 0]]) assert.equal(strikes(off), 0, `(${off}) never struck`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('蕾缪安: 通缉 needs a continuous 8 s stay in a 拉特兰 range (leaving restarts the count)', () => {
|
test('蕾缪安: 通缉 needs a continuous 8 s stay in a 拉特兰 range (leaving restarts the count)', () => {
|
||||||
|
|||||||
@@ -330,7 +330,7 @@ test('S2 连星 (AUTO, SP_FULL): a triggered piece hits every enemy on its conne
|
|||||||
assert.deepEqual([sk.spCost, sk.bb['attack@atk_scale'], sk.bb['attack@move_speed'], sk.bb['attack@duration']], elite ? [16, 4.8, -0.4, 6] : [17, 4.2, -0.35, 6], `T${tier}`);
|
assert.deepEqual([sk.spCost, sk.bb['attack@atk_scale'], sk.bb['attack@move_speed'], sk.bb['attack@duration']], elite ? [16, 4.8, -0.4, 6] : [17, 4.2, -0.35, 6], `T${tier}`);
|
||||||
const { h, u } = field({ tier, elite, skill: 1, stones: [[10, 6], [10, 7]] });
|
const { h, u } = field({ tier, elite, skill: 1, stones: [[10, 6], [10, 7]] });
|
||||||
const onLine = h.spawn('enemy_fly', { pos: [10, 9] }); // 3 tiles right of (10,6)…(10,7)+2: on the horizontal line
|
const onLine = h.spawn('enemy_fly', { pos: [10, 9] }); // 3 tiles right of (10,6)…(10,7)+2: on the horizontal line
|
||||||
const beyond = h.spawn('enemy_dummy', { pos: [10, 11] }); // 4 tiles past (10,7)
|
const beyond = h.spawn('enemy_dummy', { pos: [10, 2] }); // 4 tiles before (10,6) (the field ends at column 10: no tile 4 past (10,7))
|
||||||
const vertical = h.spawn('enemy_dummy', { pos: [12, 7] }); // its battle-start 跟子 (11,7) is 上 of it: the vertical line is on
|
const vertical = h.spawn('enemy_dummy', { pos: [12, 7] }); // its battle-start 跟子 (11,7) is 上 of it: the vertical line is on
|
||||||
const off = h.spawn('enemy_dummy', { pos: [9, 4] }); // on neither line
|
const off = h.spawn('enemy_dummy', { pos: [9, 4] }); // on neither line
|
||||||
const trig = h.spawn('enemy_runner', { pos: [10, 7] });
|
const trig = h.spawn('enemy_runner', { pos: [10, 7] });
|
||||||
|
|||||||
@@ -238,10 +238,10 @@
|
|||||||
"enemy_1071_dftman": [63,63,0,455,243855],
|
"enemy_1071_dftman": [63,63,0,455,243855],
|
||||||
"enemy_1072_dlancer": [60,55,5,22144,476588]
|
"enemy_1072_dlancer": [60,55,5,22144,476588]
|
||||||
},
|
},
|
||||||
"hooks": [1,178,6234,1000,1000,2074,2072,74,158,168,32,32,0,36983,4066,67,158,10,1,160,2,0,11],
|
"hooks": [1,178,6234,1000,1000,2074,2072,74,158,168,32,32,0,36983,4064,67,158,10,1,160,2,0,11],
|
||||||
"events": {"atk":1142,"bounty":4,"deploy":18,"die":158,"dmg":2072,"engage":11,"fx":161,"heal":53,"layer":2,"leak":10,"skill":64,"spawn":172,"status":472},
|
"events": {"atk":1142,"bounty":4,"deploy":18,"die":158,"dmg":2072,"engage":11,"fx":161,"heal":53,"layer":2,"leak":10,"skill":64,"spawn":172,"status":472},
|
||||||
"fx": {"aoe":11,"dodge":14,"dp":38,"garrison":2,"revive":1,"slash":77,"sp":13,"spGain":3,"telegraph":2},
|
"fx": {"aoe":11,"dodge":14,"dp":38,"garrison":2,"revive":1,"slash":77,"sp":13,"spGain":3,"telegraph":2},
|
||||||
"snaps": "484fd662 19a9b252 827467e6 d8836b51 4e1a5c20 f5f33eaf 009d313f 487fac84 6e33c253 481fa961 eee28282 47fecc48 ec2c4a14 046a7402 d017b22b a2a8910b b53ce9db 2504d954 222d228d 78e5e524 2b44a10f"
|
"snaps": "484fd662 19a9b252 827467e6 5c638330 c6d0b532 c1538ba9 691c2e95 65eab1de e256c73c 337bd270 302985e3 18a84929 737e1169 1cc75867 71696cfe c0e56f12 25c880fa 10b7e655 a65f5fd8 02e1a041 602de50a"
|
||||||
},
|
},
|
||||||
"standin-05": {
|
"standin-05": {
|
||||||
"about": "normal band 6 as stand-ins: chess_char_6_17_a→char_608_acpion chess_char_6_13_a→char_615_acspec chess_char_6_19_a→char_615_acspec chess_char_6_12_a→char_612_accast chess_char_6_15_a→char_614_acsupo chess_char_6_18_a→char_612_accast chess_char_6_20_a→char_613_acmedc",
|
"about": "normal band 6 as stand-ins: chess_char_6_17_a→char_608_acpion chess_char_6_13_a→char_615_acspec chess_char_6_19_a→char_615_acspec chess_char_6_12_a→char_612_accast chess_char_6_15_a→char_614_acsupo chess_char_6_18_a→char_612_accast chess_char_6_20_a→char_613_acmedc",
|
||||||
|
|||||||
@@ -177,8 +177,8 @@ for (const kind of ['boss', 'hidden']) for (const side of ['L', 'R']) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
test('#475: even buildable hand, temporary-storage, side-margin and off-rect tiles remain invalid Raid landings', () => {
|
test('#475: even buildable hand, temporary-storage and side-margin tiles remain invalid Raid landings', () => {
|
||||||
for (const pos of [[0, 3], [1, 3], [0, 17], [1, 17], [3, 1], [3, 19], [6, 17]]) {
|
for (const pos of [[0, 3], [1, 3], [0, 17], [1, 17], [3, 1], [3, 19]]) {
|
||||||
const h = emptyHalfLanding({ pos, ranged: true });
|
const h = emptyHalfLanding({ pos, ranged: true });
|
||||||
const u = h.unit('probe'), home = [u.tileR, u.tileC];
|
const u = h.unit('probe'), home = [u.tileR, u.tileC];
|
||||||
h.run(11);
|
h.run(11);
|
||||||
@@ -187,6 +187,16 @@ test('#475: even buildable hand, temporary-storage, side-margin and off-rect til
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('#475: no Raid lands off the rectangle — an enemy spawned on (6,17) is held on the edge row and stands on (5,17)', () => {
|
||||||
|
// Battle._clampPos keeps an enemy FIELD_EDGE_INSET inside the field, so its tile is the edge tile (body.js); the Raid
|
||||||
|
// lands where it would for an enemy put on (5,17), never on row 6
|
||||||
|
const h = emptyHalfLanding({ pos: [6, 17], ranged: true });
|
||||||
|
const u = h.unit('probe');
|
||||||
|
h.run(11);
|
||||||
|
assert.deepEqual([u.tileR, u.tileC], [5, 17]);
|
||||||
|
checkInvariants(h.b);
|
||||||
|
});
|
||||||
|
|
||||||
test('#475: empty-half Raid still respects melee/high-ground, water and occupied/downed-unit reservations', () => {
|
test('#475: empty-half Raid still respects melee/high-ground, water and occupied/downed-unit reservations', () => {
|
||||||
for (const glyph of ['h', 'd']) {
|
for (const glyph of ['h', 'd']) {
|
||||||
const h = emptyHalfLanding({ pos: [3, 17], glyph });
|
const h = emptyHalfLanding({ pos: [3, 17], glyph });
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
// The tile of a target is its position rounded half to even — PRTS 作战机制 「受击方坐标经round函数取整(四舍六入五成双)
|
||||||
|
// 得到其所属格坐标,按是否属于该格判定」 — not Math.round, which rounds every tie up: an enemy exactly halfway between
|
||||||
|
// columns 10 and 11 (x = 10.5) is on column 10, one at x = 11.5 on column 12.
|
||||||
|
|
||||||
|
import { test } from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { makeBattle, chessRec, enemyRec } from '../helpers/battleHarness.js';
|
||||||
|
import { roundHalfEven } from '../../server/sim/detmath.js';
|
||||||
|
import { bodyKeys, bodyOnTile, bodyTileReach } from '../../server/sim/body.js';
|
||||||
|
import { tileKeyOf } from '../../server/sim/targeting.js';
|
||||||
|
import { COLS } from '../../server/sim/constants.js';
|
||||||
|
|
||||||
|
test('roundHalfEven: ties to the even integer, the rest to the nearest; never −0', () => {
|
||||||
|
const cases = [[0.5, 0], [1.5, 2], [2.5, 2], [3.5, 4], [10.5, 10], [11.5, 12], [-0.5, 0], [-1.5, -2], [-2.5, -2],
|
||||||
|
[2.4999999999999996, 2], [2.5000000000000004, 3], [-0.4, 0], [-0, 0], [7, 7], [4503599627370495.5, 4503599627370496]];
|
||||||
|
for (const [x, want] of cases) assert.ok(Object.is(roundHalfEven(x), want), `${x} → ${roundHalfEven(x)}, want ${want}`);
|
||||||
|
for (let i = -2000; i <= 2000; i++) {
|
||||||
|
const x = i / 8, v = roundHalfEven(x), d = Math.abs(x - v);
|
||||||
|
assert.ok(Number.isInteger(v) && (d < 0.5 || (d === 0.5 && v % 2 === 0)), `${x} → ${v}`);
|
||||||
|
if (d !== 0.5) assert.equal(v, Math.round(x) + 0, `${x}: away from a tie it is Math.round`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a point target exactly halfway is on the even tile: bodyKeys, bodyOnTile, bodyTileReach, tileKeyOf', () => {
|
||||||
|
const e = { x: 10.5, y: 3.5 };
|
||||||
|
assert.deepEqual(bodyKeys(e), [4 * COLS + 10]);
|
||||||
|
assert.ok(bodyOnTile(e, 4, 10) && !bodyOnTile(e, 4, 11) && !bodyOnTile(e, 3, 10));
|
||||||
|
assert.equal(bodyTileReach(e, 4, 10), 0);
|
||||||
|
assert.equal(bodyTileReach(e, 4, 12), 2);
|
||||||
|
assert.equal(tileKeyOf(e), 4 * COLS + 10);
|
||||||
|
assert.equal(tileKeyOf({ x: 11.5, y: 2.5 }), 2 * COLS + 12);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the battle\'s enemy index puts an enemy at x = 10.5 on column 10, not 11', () => {
|
||||||
|
const h = makeBattle({
|
||||||
|
kind: 'boss', content: 'none', autoFinish: false, timeLimit: 60,
|
||||||
|
defs: {
|
||||||
|
chess: { t_one: chessRec({ id: 't_one', stats: { atk: 100, blockCnt: 0, maxHp: 1e7 }, rangeGrid: [[0, 0]], skill: null }) },
|
||||||
|
enemies: { enemy_still: enemyRec({ key: 'enemy_still', hp: 1e8, speed: 0 }) },
|
||||||
|
},
|
||||||
|
players: [{ playerId: 'p1', seat: 0, side: 'L', coords: 'field', units: [{ uid: 1, kind: 'chess', chessId: 't_one', row: 1, col: 1 }], bonds: {} }],
|
||||||
|
});
|
||||||
|
h.step();
|
||||||
|
const e = h.spawn('enemy_still', { pos: [3, 10], routeIndex: 0 });
|
||||||
|
e.x = 10.5;
|
||||||
|
e.y = 3;
|
||||||
|
h.step();
|
||||||
|
e.x = 10.5;
|
||||||
|
e.y = 3;
|
||||||
|
const u = h.unit('t_one');
|
||||||
|
assert.deepEqual(h.b.enemiesInKeys([3 * COLS + 10], u, { canHitFly: true }).map((x) => x.id), [e.id]);
|
||||||
|
assert.deepEqual(h.b.enemiesInKeys([3 * COLS + 11], u, { canHitFly: true }).map((x) => x.id), []);
|
||||||
|
});
|
||||||
Reference in new issue
Block a user