Adds spatial search functionality to find users/objects by name
with distance and direction indicator.
## Features
- Search by name with autocomplete
- Distance calculation (meters)
- Direction indicator (N/S/E/W/NE/SE/SW/NW)
- Pathfinding arrow on HUD
- Keyboard shortcut (Ctrl+F / Cmd+F)
## Files Added
- js/spatial-search.js — Main search module
- tests/test_spatial_search.js — 9 passing tests
## Usage
```javascript
const search = new SpatialSearch({ maxDistance: 1000 });
search.registerEntity('id', { name: 'Alice', type: 'user', position: {...} });
const results = search.searchEntities('ali', cameraPosition);
```
## Tests
All 9 tests passing:
✅ loads correctly
✅ can be instantiated
✅ can register entities
✅ can unregister entities
✅ can update entity position
✅ calculates distance correctly
✅ calculates direction correctly
✅ searches entities correctly
✅ gets status
Closes #1540
Closes #1639
99 lines
3.4 KiB
JavaScript
99 lines
3.4 KiB
JavaScript
// Test suite for SpatialSearch module
|
|
// Run: node --test tests/test_spatial_search.js
|
|
|
|
const { describe, it, beforeEach } = require('node:test');
|
|
const assert = require('node:assert');
|
|
|
|
// Mock DOM for Node.js environment
|
|
global.document = {
|
|
createElement: (tag) => ({
|
|
className: '',
|
|
style: {},
|
|
innerHTML: '',
|
|
textContent: '',
|
|
addEventListener: () => {},
|
|
appendChild: () => {},
|
|
querySelector: () => ({ style: {}, textContent: '' })
|
|
}),
|
|
body: { appendChild: () => {} },
|
|
addEventListener: () => {}
|
|
};
|
|
global.window = { camera: null };
|
|
|
|
// Load module
|
|
const SpatialSearch = require('../js/spatial-search.js');
|
|
|
|
describe('SpatialSearch', () => {
|
|
let search;
|
|
|
|
beforeEach(() => {
|
|
search = new SpatialSearch({ maxDistance: 1000 });
|
|
});
|
|
|
|
it('loads correctly', () => {
|
|
assert.ok(SpatialSearch);
|
|
});
|
|
|
|
it('can be instantiated', () => {
|
|
assert.ok(search instanceof SpatialSearch);
|
|
});
|
|
|
|
it('can register entities', () => {
|
|
search.registerEntity('user1', {
|
|
name: 'Alice',
|
|
type: 'user',
|
|
position: { x: 10, y: 0, z: 5 }
|
|
});
|
|
const status = search.getStatus();
|
|
assert.strictEqual(status.entityCount, 1);
|
|
});
|
|
|
|
it('can unregister entities', () => {
|
|
search.registerEntity('user1', { name: 'Alice', type: 'user', position: { x: 0, y: 0, z: 0 } });
|
|
search.unregisterEntity('user1');
|
|
assert.strictEqual(search.getStatus().entityCount, 0);
|
|
});
|
|
|
|
it('can update entity position', () => {
|
|
search.registerEntity('user1', { name: 'Alice', type: 'user', position: { x: 0, y: 0, z: 0 } });
|
|
search.updateEntityPosition('user1', { x: 10, y: 0, z: 10 });
|
|
// Verify by searching with camera position
|
|
const results = search.searchEntities('alice', { x: 0, y: 0, z: 0 });
|
|
assert.strictEqual(results.length, 1);
|
|
assert.ok(results[0].distance > 0);
|
|
});
|
|
|
|
it('calculates distance correctly', () => {
|
|
const from = { x: 0, y: 0, z: 0 };
|
|
const to = { x: 3, y: 0, z: 4 };
|
|
const distance = search._calculateDistance(from, to);
|
|
assert.strictEqual(distance, 5); // 3-4-5 triangle
|
|
});
|
|
|
|
it('calculates direction correctly', () => {
|
|
const from = { x: 0, y: 0, z: 0 };
|
|
assert.strictEqual(search._calculateDirection(from, { x: 0, y: 0, z: 10 }), 'N');
|
|
assert.strictEqual(search._calculateDirection(from, { x: 10, y: 0, z: 0 }), 'E');
|
|
assert.strictEqual(search._calculateDirection(from, { x: 0, y: 0, z: -10 }), 'S');
|
|
assert.strictEqual(search._calculateDirection(from, { x: -10, y: 0, z: 0 }), 'W');
|
|
});
|
|
|
|
it('searches entities correctly', () => {
|
|
search.registerEntity('1', { name: 'Alice', type: 'user', position: { x: 5, y: 0, z: 0 } });
|
|
search.registerEntity('2', { name: 'Bob', type: 'user', position: { x: 10, y: 0, z: 0 } });
|
|
search.registerEntity('3', { name: 'Alice Shop', type: 'object', position: { x: 20, y: 0, z: 0 } });
|
|
|
|
const results = search.searchEntities('ali', { x: 0, y: 0, z: 0 });
|
|
assert.strictEqual(results.length, 2);
|
|
assert.strictEqual(results[0].name, 'Alice'); // Closer
|
|
assert.strictEqual(results[1].name, 'Alice Shop');
|
|
});
|
|
|
|
it('gets status', () => {
|
|
search.registerEntity('1', { name: 'Test', type: 'object', position: { x: 0, y: 0, z: 0 } });
|
|
const status = search.getStatus();
|
|
assert.strictEqual(status.entityCount, 1);
|
|
assert.strictEqual(status.isOpen, false);
|
|
assert.strictEqual(status.resultCount, 0);
|
|
});
|
|
}); |