'use strict';
const test = require('node:test');
const assert = require('node:assert');
const {
parseMarkers,
fillPolygonEvenOdd,
pointInPolygon,
extractPolygons,
parseColor,
buildMarkersUrl
} = require('../src/core/territoryDataService');
const chunkPos = require('../src/core/chunkPos');
/** 把 [x, z] 坐标对转换为 dynmap 的点对象形式 */
function points(coords) {
return coords.map(([x, z]) => ({ x, z }));
}
/** 构造一个 markers.json 的单个多边形 marker */
function claimMarker(name, polygons, color) {
const converted = polygons.map(points);
return {
popup: `
疆土归属: ${name}
`,
color: color || '#75bd20',
points: converted.length === 1 ? converted[0] : converted
};
}
test('pointInPolygon 经典射线法', () => {
const square = [[0, 0], [32, 0], [32, 32], [0, 32]];
assert.strictEqual(pointInPolygon(16, 16, square), true);
assert.strictEqual(pointInPolygon(-1, 16, square), false);
assert.strictEqual(pointInPolygon(33, 16, square), false);
});
test('extractPolygons 展平单个多边形与嵌套多边形数组', () => {
const single = points([[0, 0], [16, 0], [16, 16]]);
assert.strictEqual(extractPolygons(single).length, 1);
const nested = [points([[0, 0], [16, 0], [16, 16]]), points([[4, 4], [8, 4], [8, 8]])];
assert.strictEqual(extractPolygons(nested).length, 2);
// 少于 3 个点的多边形被丢弃
assert.strictEqual(extractPolygons(points([[0, 0], [1, 1]])).length, 0);
});
test('fillPolygonEvenOdd 用内圈孔洞挖空外圈', () => {
const outer = [[0, 0], [48, 0], [48, 48], [0, 48]];
const hole = [[16, 16], [32, 16], [32, 32], [16, 32]];
const out = new Set();
fillPolygonEvenOdd(out, [outer, hole]);
// 3×3 区块的外圈 = 9 格,挖掉中心 1 格 = 8 格
assert.strictEqual(out.size, 8);
assert.ok(!out.has(chunkPos.key(1, 1)), '孔洞中心区块不应被填充');
assert.ok(out.has(chunkPos.key(0, 0)));
assert.ok(out.has(chunkPos.key(2, 2)));
});
test('parseMarkers 解析邦国领土与王城', () => {
const json = JSON.stringify([
{
id: 'union_claims',
markers: [
claimMarker('测试国', [[[0, 0], [32, 0], [32, 32], [0, 32]]], '#ff0000'),
claimMarker('另一国', [[[64, 64], [96, 64], [96, 96], [64, 96]]], '#00ff00')
]
},
{
id: 'union_capitals',
markers: [
{ popup: '「测试国」的王城
', point: { x: 8, y: 70, z: 8 } }
]
}
]);
const model = parseMarkers(json);
assert.strictEqual(model.stateCount, 2);
const state = model.state('测试国');
assert.ok(state);
assert.strictEqual(state.color, 0xff0000);
assert.deepStrictEqual(state.capital, { x: 0, z: 0 }, '王城应由方块坐标 8,8 换算为区块 0,0');
assert.strictEqual(state.chunks.size, 4, '2×2 区块应被填充');
// 没有王城 marker 的邦国回退到第一个区块
const other = model.state('另一国');
assert.ok(other.capital);
});
test('parseMarkers 忽略弹窗中没有邦国名的 marker', () => {
const json = JSON.stringify([
{ id: 'union_claims', markers: [{ popup: '无归属
', points: points([[0, 0], [16, 0], [16, 16]]) }] },
{ id: 'other_layer', markers: [claimMarker('忽略我', [[[0, 0], [16, 0], [16, 16]]])] }
]);
const model = parseMarkers(json);
assert.strictEqual(model.stateCount, 0);
});
test('parseMarkers 顶层必须是数组', () => {
assert.throws(() => parseMarkers('{}'), /数组/);
});
test('parseColor 支持 #RRGGBB 与 #AARRGGBB', () => {
assert.strictEqual(parseColor('#ff0000'), 0xff0000);
assert.strictEqual(parseColor('#80ff0000'), 0xff0000);
assert.strictEqual(parseColor('75bd20'), 0x75bd20);
// 非法输入回退到默认邦国绿
assert.strictEqual(parseColor('#zzzzzz'), 0x75bd20);
assert.strictEqual(parseColor(''), 0x75bd20);
});
test('buildMarkersUrl 拼接地图站点与世界目录', () => {
assert.strictEqual(
buildMarkersUrl('http://bgjq.simpfun.cn', 'minecraft_overworld'),
'http://bgjq.simpfun.cn/tiles/minecraft_overworld/markers.json'
);
// 末尾斜杠应被去掉
assert.strictEqual(
buildMarkersUrl('https://map.example.com/', 'world'),
'https://map.example.com/tiles/world/markers.json'
);
// 缺省值
assert.strictEqual(buildMarkersUrl(), 'http://bgjq.simpfun.cn/tiles/minecraft_overworld/markers.json');
});