@@ -1,156 +1,142 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* 自测 Unity 绘制排序(与 UnityDrawSort.ts 对齐)
|
||||
* 自测等距 frontKey 排序(与 UnityDrawSort.ts 对齐)
|
||||
* 运行: node tools/verify-unity-draw-sort.mjs
|
||||
*/
|
||||
|
||||
const UNITY_SORTING_ORDER = { nProp: 1, tilemap: 2, vehicle: 2, prop: 3, player: 4 };
|
||||
const ORDER_SCALE = 10_000;
|
||||
const DEPTH_SCALE = 100;
|
||||
const TILE_ELEVATION_Y_THRESHOLD = 12;
|
||||
const HALF_W = 50;
|
||||
const HALF_H = 25;
|
||||
const CELL = 100;
|
||||
const RANK_FLOOR = 0;
|
||||
const RANK_PICKABLE = 20;
|
||||
const RANK_VEHICLE = 30;
|
||||
const RANK_PLAYER = 35;
|
||||
const RANK_WALL = 40;
|
||||
|
||||
function cellToWorldCenter(x, y) {
|
||||
const wx = (x - y) * HALF_W;
|
||||
const wy = (x + y) * HALF_H + HALF_H;
|
||||
return { x: wx, y: wy };
|
||||
}
|
||||
|
||||
function unityTileIndividualSortingOrder(cellX, cellY) {
|
||||
return UNITY_SORTING_ORDER.tilemap - cellX - cellY;
|
||||
}
|
||||
|
||||
function compareTileCellDepth(ax, ay, bx, by) {
|
||||
if (ay !== by) return by - ay;
|
||||
return bx - ax;
|
||||
}
|
||||
|
||||
function compareUnityTileToTile(a, b) {
|
||||
const cellCmp = compareTileCellDepth(a.cellX, a.cellY, b.cellX, b.cellY);
|
||||
if (cellCmp !== 0) return cellCmp;
|
||||
const yCmp = Math.round(a.depthY * DEPTH_SCALE) - Math.round(b.depthY * DEPTH_SCALE);
|
||||
if (Math.abs(yCmp) >= TILE_ELEVATION_Y_THRESHOLD) return yCmp;
|
||||
return unityTileIndividualSortingOrder(a.cellX, a.cellY)
|
||||
- unityTileIndividualSortingOrder(b.cellX, b.cellY);
|
||||
}
|
||||
|
||||
function tileInFrontOfTile(front, back, depthFront = 0, depthBack = 0) {
|
||||
const a = { cellX: front.x, cellY: front.y, depthY: depthFront };
|
||||
const b = { cellX: back.x, cellY: back.y, depthY: depthBack };
|
||||
return compareUnityTileToTile(a, b) > 0;
|
||||
}
|
||||
|
||||
function compareIsoDrawOrder(ax, ay, bx, by) {
|
||||
const ka = ax + ay;
|
||||
const kb = bx + by;
|
||||
if (ka !== kb) return kb - ka;
|
||||
return ax - bx;
|
||||
}
|
||||
|
||||
function wallFaceSamples(wallX, wallY) {
|
||||
return [{ x: wallX, y: wallY - 1 }, { x: wallX - 1, y: wallY }];
|
||||
}
|
||||
|
||||
function compareWallActorIso(actor, wallX, wallY) {
|
||||
let actorAhead = 0;
|
||||
for (const face of wallFaceSamples(wallX, wallY)) {
|
||||
const iso = compareIsoDrawOrder(actor.x, actor.y, face.x, face.y);
|
||||
if (iso < 0) return iso;
|
||||
if (iso > actorAhead) actorAhead = iso;
|
||||
}
|
||||
return actorAhead;
|
||||
}
|
||||
|
||||
function compareUnityEntityToWall(entity, tile) {
|
||||
const cell = { x: entity.cellX, y: entity.cellY };
|
||||
for (const face of wallFaceSamples(tile.cellX, tile.cellY)) {
|
||||
if (face.x === cell.x && face.y === cell.y) return 1;
|
||||
}
|
||||
const tileOrder = unityTileIndividualSortingOrder(tile.cellX, tile.cellY);
|
||||
if (tileOrder > entity.sortingOrder) return -1;
|
||||
const wallIso = compareWallActorIso(cell, tile.cellX, tile.cellY);
|
||||
if (wallIso < 0) {
|
||||
if (tile.cellX === cell.x && tile.cellY === cell.y - 1) return -1;
|
||||
if (tile.cellY < cell.y) return 1;
|
||||
return -1;
|
||||
}
|
||||
const tileAhead = (tile.cellX + tile.cellY) - (cell.x + cell.y);
|
||||
if (wallIso > 0 && tileAhead > 0) return -1;
|
||||
if (wallIso > 0) return wallIso;
|
||||
const yCmp = Math.round(entity.depthY * DEPTH_SCALE) - Math.round(tile.depthY * DEPTH_SCALE);
|
||||
if (yCmp !== 0) return yCmp;
|
||||
if (tileAhead !== 0) return -tileAhead;
|
||||
return compareIsoDrawOrder(tile.cellX, tile.cellY, cell.x, cell.y);
|
||||
}
|
||||
|
||||
function compareUnityEntityToTile(entity, tile) {
|
||||
if (tile.tileName !== 'WallBlock') return 1;
|
||||
return compareUnityEntityToWall(entity, tile);
|
||||
function frontKey(x, y, rank) {
|
||||
return -(x + y) * CELL + rank;
|
||||
}
|
||||
|
||||
function entityInFrontOfTile(entity, tile) {
|
||||
return compareUnityEntityToTile(entity, tile) > 0;
|
||||
const eRank = entity.isPickable ? RANK_PICKABLE : (entity.isPlayer ? RANK_PLAYER : RANK_VEHICLE);
|
||||
let eKey = frontKey(entity.cellX, entity.cellY, eRank);
|
||||
// 贴墙立面抬升
|
||||
const faces = [
|
||||
{ x: tile.cellX, y: tile.cellY - 1 },
|
||||
{ x: tile.cellX - 1, y: tile.cellY },
|
||||
];
|
||||
const onFace = faces.some((f) => f.x === entity.cellX && f.y === entity.cellY);
|
||||
const tKey = frontKey(tile.cellX, tile.cellY, tile.tileName === 'WallBlock' ? RANK_WALL : RANK_FLOOR);
|
||||
if (tile.tileName === 'WallBlock' && onFace) eKey = Math.max(eKey, tKey + 1);
|
||||
return eKey > tKey;
|
||||
}
|
||||
|
||||
function walkableUnderPlayer(playerCell, tileCell) {
|
||||
return entityInFrontOfTile(
|
||||
{ sortingOrder: 4, cellX: playerCell.x, cellY: playerCell.y, centerY: 0, depthY: 0 },
|
||||
{ cellX: tileCell.x, cellY: tileCell.y, tileName: 'Baseblock', centerY: 0, depthY: 0 },
|
||||
);
|
||||
}
|
||||
|
||||
function playerBottomY(cellX, cellY) {
|
||||
return cellToWorldCenter(cellX, cellY).y - 0.35 * 90;
|
||||
}
|
||||
|
||||
function wallFrontY(cellX, cellY) {
|
||||
const c = cellToWorldCenter(cellX, cellY);
|
||||
return Math.max(c.y, c.y + (1 - 0.67) * 115);
|
||||
function tileInFrontOfTile(a, b) {
|
||||
return frontKey(a.x, a.y, RANK_FLOOR) > frontKey(b.x, b.y, RANK_FLOOR);
|
||||
}
|
||||
|
||||
const cases = [
|
||||
{
|
||||
name: '砖↔砖:斜向路径 (0,-2)→(0,-1)→(0,0)→(1,0) 下方逐格遮挡',
|
||||
pass: tileInFrontOfTile({ x: 0, y: -2 }, { x: 0, y: -1 })
|
||||
&& tileInFrontOfTile({ x: 0, y: -1 }, { x: 0, y: 0 })
|
||||
&& tileInFrontOfTile({ x: 0, y: 0 }, { x: 1, y: 0 }),
|
||||
name: '砖:南 (0,-2) 挡北 (0,-1)',
|
||||
pass: tileInFrontOfTile({ x: 0, y: -2 }, { x: 0, y: -1 }),
|
||||
},
|
||||
{
|
||||
name: '砖↔砖:对角路径 (0,-3)→(1,-2)→(2,-1)→(3,0) 下方逐格遮挡',
|
||||
pass: tileInFrontOfTile({ x: 0, y: -3 }, { x: 1, y: -2 })
|
||||
&& tileInFrontOfTile({ x: 1, y: -2 }, { x: 2, y: -1 })
|
||||
&& tileInFrontOfTile({ x: 2, y: -1 }, { x: 3, y: 0 }),
|
||||
},
|
||||
{
|
||||
name: '砖↔砖:南侧 (0,-1) 挡住北侧 (0,0)',
|
||||
pass: tileInFrontOfTile({ x: 0, y: -1 }, { x: 0, y: 0 }),
|
||||
},
|
||||
{
|
||||
name: '砖↔砖:东南 (2,-2) 挡住西北 (0,-1)',
|
||||
pass: tileInFrontOfTile({ x: 2, y: -2 }, { x: 0, y: -1 }),
|
||||
},
|
||||
{
|
||||
name: '91614 出生 (0,-1) 不被北侧墙 (0,0) 挡住',
|
||||
pass: entityInFrontOfTile(
|
||||
{ sortingOrder: 4, cellX: 0, cellY: -1, centerY: 0, depthY: 0 },
|
||||
{ cellX: 0, cellY: 0, tileName: 'WallBlock', centerY: 0, depthY: 0 },
|
||||
name: '北墙 (-1,1) 挡住中心角色 (0,0)',
|
||||
pass: !entityInFrontOfTile(
|
||||
{ cellX: 0, cellY: 0 },
|
||||
{ cellX: -1, cellY: 1, tileName: 'WallBlock' },
|
||||
),
|
||||
},
|
||||
{
|
||||
name: '跨格路径砖:南侧 (0,-2) 不挡北侧角色 (0,-1)',
|
||||
pass: entityInFrontOfTile(
|
||||
{ sortingOrder: 4, cellX: 0, cellY: -1, centerY: 0, depthY: 0 },
|
||||
{ cellX: 0, cellY: -2, tileName: 'Baseblock', centerY: 0, depthY: 100 },
|
||||
name: '东墙 (1,-1) 挡住中心角色 (0,0)(同对角立面)',
|
||||
pass: !entityInFrontOfTile(
|
||||
{ cellX: 0, cellY: 0 },
|
||||
{ cellX: 1, cellY: -1, tileName: 'WallBlock' },
|
||||
),
|
||||
},
|
||||
{
|
||||
name: '91614 真实Y:出生 (0,-1) 在 (0,0) 墙前',
|
||||
pass: entityInFrontOfTile(
|
||||
{ sortingOrder: 4, cellX: 0, cellY: -1, centerY: 0, depthY: playerBottomY(0, -1) },
|
||||
{ cellX: 0, cellY: 0, tileName: 'WallBlock', centerY: 0, depthY: wallFrontY(0, 0) },
|
||||
name: '南 tip 墙 (-1,-3) 挡住金币 (0,-3)',
|
||||
pass: !entityInFrontOfTile(
|
||||
{ cellX: 0, cellY: -3, isPickable: true },
|
||||
{ cellX: -1, cellY: -3, tileName: 'WallBlock' },
|
||||
),
|
||||
},
|
||||
{
|
||||
name: '金币 (0,-3) 在东后墙 (1,-3) 之前(屏上金币更靠前)',
|
||||
pass: entityInFrontOfTile(
|
||||
{ cellX: 0, cellY: -3, isPickable: true },
|
||||
{ cellX: -1, cellY: -2, tileName: 'Baseblock' },
|
||||
) && entityInFrontOfTile(
|
||||
{ cellX: 0, cellY: -3, isPickable: true },
|
||||
{ cellX: 1, cellY: -3, tileName: 'WallBlock' },
|
||||
),
|
||||
},
|
||||
{
|
||||
name: '贴南墙立面 (0,-1) 露在墙 (0,0) 前',
|
||||
pass: entityInFrontOfTile(
|
||||
{ cellX: 0, cellY: -1 },
|
||||
{ cellX: 0, cellY: 0, tileName: 'WallBlock' },
|
||||
),
|
||||
},
|
||||
{
|
||||
name: '南墙 (0,-1) 挡住角色 (0,0)',
|
||||
pass: !entityInFrontOfTile(
|
||||
{ cellX: 0, cellY: 0 },
|
||||
{ cellX: 0, cellY: -1, tileName: 'WallBlock' },
|
||||
),
|
||||
},
|
||||
{
|
||||
name: '左墙 (-6,-2) 挡住角色 (-4,-3)',
|
||||
pass: !entityInFrontOfTile(
|
||||
{ cellX: -4, cellY: -3 },
|
||||
{ cellX: -6, cellY: -2, tileName: 'WallBlock' },
|
||||
),
|
||||
},
|
||||
{
|
||||
name: '载具朝前钉起点:相对南侧地砖仍被挡',
|
||||
pass: !entityInFrontOfTile(
|
||||
{ cellX: 0, cellY: 0, isPlayer: false },
|
||||
{ cellX: 0, cellY: -1, tileName: 'Baseblock' },
|
||||
),
|
||||
},
|
||||
{
|
||||
name: '载具朝后用落点:从(0,0)→(1,0) 比钉起点更靠后(继续被下方砖挡)',
|
||||
pass: (() => {
|
||||
const floor = { cellX: 0, cellY: -1, tileName: 'Baseblock' };
|
||||
// 起点更靠前,相对该南侧砖「不挡得那么死」;落点更靠后应仍被挡住
|
||||
const startKeyWins = entityInFrontOfTile(
|
||||
{ cellX: 0, cellY: 0, isPlayer: false },
|
||||
floor,
|
||||
);
|
||||
const landOccluded = !entityInFrontOfTile(
|
||||
{ cellX: 1, cellY: 0, isPlayer: false },
|
||||
floor,
|
||||
);
|
||||
return !startKeyWins && landOccluded;
|
||||
})(),
|
||||
},
|
||||
{
|
||||
name: '角色朝前用落点:从(0,1)→(0,0) 途中露前',
|
||||
pass: (() => {
|
||||
const from = { cellX: 0, cellY: 1 };
|
||||
const to = { cellX: 0, cellY: 0 };
|
||||
const wall = { cellX: 1, cellY: 0, tileName: 'WallBlock' };
|
||||
return !entityInFrontOfTile(from, wall) && entityInFrontOfTile(to, wall);
|
||||
})(),
|
||||
},
|
||||
{
|
||||
name: '同格:角色永远盖过载具',
|
||||
pass: frontKey(0, 0, RANK_PLAYER) > frontKey(0, 0, RANK_VEHICLE),
|
||||
},
|
||||
{
|
||||
name: '南侧地砖可挡住同侧偏后的实体(下方地砖应遮挡载具)',
|
||||
pass: !entityInFrontOfTile(
|
||||
{ cellX: 0, cellY: 0 },
|
||||
{ cellX: 0, cellY: -1, tileName: 'Baseblock' },
|
||||
),
|
||||
},
|
||||
{
|
||||
name: '人物与载具同格 frontKey 条带一致(主题无关)',
|
||||
pass: Math.floor(frontKey(2, -1, RANK_PLAYER) / 100) === Math.floor(frontKey(2, -1, RANK_VEHICLE) / 100),
|
||||
},
|
||||
];
|
||||
|
||||
let failed = 0;
|
||||
@@ -159,7 +145,6 @@ for (const c of cases) {
|
||||
console.log(`${ok ? '✓' : '✗'} ${c.name}`);
|
||||
if (!ok) failed++;
|
||||
}
|
||||
|
||||
if (failed > 0) {
|
||||
console.error(`\n${failed} case(s) failed`);
|
||||
process.exit(1);
|
||||
|
||||
Reference in New Issue
Block a user