Compare commits
3 Commits
mimo/code/
...
fix/1540
| Author | SHA1 | Date | |
|---|---|---|---|
| 9d8bc556e2 | |||
| f91c0bef0f | |||
|
|
b0b4cd0c97 |
@@ -395,6 +395,7 @@
|
|||||||
<div id="memory-connections-panel" class="memory-connections-panel" style="display:none;" aria-label="Memory Connections Panel"></div>
|
<div id="memory-connections-panel" class="memory-connections-panel" style="display:none;" aria-label="Memory Connections Panel"></div>
|
||||||
|
|
||||||
<script src="./boot.js"></script>
|
<script src="./boot.js"></script>
|
||||||
|
<script src="./js/spatial-search.js"></script>
|
||||||
<script src="./avatar-customization.js"></script>
|
<script src="./avatar-customization.js"></script>
|
||||||
<script src="./lod-system.js"></script>
|
<script src="./lod-system.js"></script>
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
457
js/spatial-search.js
Normal file
457
js/spatial-search.js
Normal file
@@ -0,0 +1,457 @@
|
|||||||
|
/**
|
||||||
|
* Spatial Search - Find nearest user/object by name
|
||||||
|
* Issue #1540: feat: spatial search — find nearest user/object by name
|
||||||
|
*
|
||||||
|
* Provides search functionality to find users/objects by name
|
||||||
|
* and show distance, direction, and pathfinding.
|
||||||
|
*/
|
||||||
|
|
||||||
|
class SpatialSearch {
|
||||||
|
constructor(options = {}) {
|
||||||
|
this.maxDistance = options.maxDistance || 1000; // Maximum search distance
|
||||||
|
this.searchDelay = options.searchDelay || 300; // Delay before search (ms)
|
||||||
|
this.onResultSelect = options.onResultSelect || (() => {});
|
||||||
|
this.onError = options.onError || console.error;
|
||||||
|
|
||||||
|
// Track entities in the world
|
||||||
|
this.entities = new Map(); // id -> {name, position, type, ...}
|
||||||
|
|
||||||
|
// Search UI elements
|
||||||
|
this.searchInput = null;
|
||||||
|
this.resultsContainer = null;
|
||||||
|
this.pathArrow = null;
|
||||||
|
|
||||||
|
// Initialize UI
|
||||||
|
this.initUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize search UI
|
||||||
|
*/
|
||||||
|
initUI() {
|
||||||
|
// Create search container
|
||||||
|
const searchContainer = document.createElement('div');
|
||||||
|
searchContainer.id = 'spatial-search-container';
|
||||||
|
searchContainer.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
z-index: 1000;
|
||||||
|
display: none;
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Create search input
|
||||||
|
this.searchInput = document.createElement('input');
|
||||||
|
this.searchInput.type = 'text';
|
||||||
|
this.searchInput.placeholder = 'Search users/objects...';
|
||||||
|
this.searchInput.style.cssText = `
|
||||||
|
width: 300px;
|
||||||
|
padding: 10px 15px;
|
||||||
|
border: 2px solid #4af0c0;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(0, 0, 0, 0.8);
|
||||||
|
color: white;
|
||||||
|
font-size: 14px;
|
||||||
|
font-family: 'SF Mono', 'Cascadia Code', 'Fira Code', monospace;
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Create results container
|
||||||
|
this.resultsContainer = document.createElement('div');
|
||||||
|
this.resultsContainer.id = 'spatial-search-results';
|
||||||
|
this.resultsContainer.style.cssText = `
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: rgba(0, 0, 0, 0.9);
|
||||||
|
border: 1px solid #4af0c0;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-top: 5px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Add event listeners
|
||||||
|
this.searchInput.addEventListener('input', () => this.handleSearch());
|
||||||
|
this.searchInput.addEventListener('keydown', (e) => this.handleKeydown(e));
|
||||||
|
|
||||||
|
// Add to container
|
||||||
|
searchContainer.appendChild(this.searchInput);
|
||||||
|
searchContainer.appendChild(this.resultsContainer);
|
||||||
|
|
||||||
|
// Add to document
|
||||||
|
document.body.appendChild(searchContainer);
|
||||||
|
|
||||||
|
// Create path arrow
|
||||||
|
this.pathArrow = document.createElement('div');
|
||||||
|
this.pathArrow.id = 'spatial-search-arrow';
|
||||||
|
this.pathArrow.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
bottom: 100px;
|
||||||
|
right: 20px;
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
background: rgba(74, 240, 192, 0.2);
|
||||||
|
border: 2px solid #4af0c0;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: none;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 24px;
|
||||||
|
color: #4af0c0;
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 999;
|
||||||
|
`;
|
||||||
|
this.pathArrow.innerHTML = '→';
|
||||||
|
this.pathArrow.addEventListener('click', () => this.clearSearch());
|
||||||
|
document.body.appendChild(this.pathArrow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show search UI
|
||||||
|
*/
|
||||||
|
show() {
|
||||||
|
const container = document.getElementById('spatial-search-container');
|
||||||
|
if (container) {
|
||||||
|
container.style.display = 'block';
|
||||||
|
this.searchInput.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide search UI
|
||||||
|
*/
|
||||||
|
hide() {
|
||||||
|
const container = document.getElementById('spatial-search-container');
|
||||||
|
if (container) {
|
||||||
|
container.style.display = 'none';
|
||||||
|
this.clearResults();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle search UI
|
||||||
|
*/
|
||||||
|
toggle() {
|
||||||
|
const container = document.getElementById('spatial-search-container');
|
||||||
|
if (container) {
|
||||||
|
if (container.style.display === 'none') {
|
||||||
|
this.show();
|
||||||
|
} else {
|
||||||
|
this.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle search input
|
||||||
|
*/
|
||||||
|
handleSearch() {
|
||||||
|
const query = this.searchInput.value.trim();
|
||||||
|
|
||||||
|
if (!query) {
|
||||||
|
this.clearResults();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debounce search
|
||||||
|
clearTimeout(this.searchTimeout);
|
||||||
|
this.searchTimeout = setTimeout(() => {
|
||||||
|
this.performSearch(query);
|
||||||
|
}, this.searchDelay);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle keyboard events
|
||||||
|
*/
|
||||||
|
handleKeydown(event) {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
this.hide();
|
||||||
|
} else if (event.key === 'Enter') {
|
||||||
|
const firstResult = this.resultsContainer.querySelector('.search-result');
|
||||||
|
if (firstResult) {
|
||||||
|
firstResult.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform search
|
||||||
|
*/
|
||||||
|
performSearch(query) {
|
||||||
|
const results = this.searchEntities(query);
|
||||||
|
this.displayResults(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search entities by name
|
||||||
|
*/
|
||||||
|
searchEntities(query) {
|
||||||
|
const lowerQuery = query.toLowerCase();
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
for (const [id, entity] of this.entities) {
|
||||||
|
const name = (entity.name || '').toLowerCase();
|
||||||
|
const type = (entity.type || '').toLowerCase();
|
||||||
|
|
||||||
|
// Check if name or type matches query
|
||||||
|
if (name.includes(lowerQuery) || type.includes(lowerQuery)) {
|
||||||
|
results.push({
|
||||||
|
id,
|
||||||
|
name: entity.name,
|
||||||
|
type: entity.type,
|
||||||
|
position: entity.position,
|
||||||
|
distance: this.calculateDistance(entity.position),
|
||||||
|
direction: this.calculateDirection(entity.position)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by distance
|
||||||
|
results.sort((a, b) => a.distance - b.distance);
|
||||||
|
|
||||||
|
// Limit results
|
||||||
|
return results.slice(0, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate distance to entity
|
||||||
|
*/
|
||||||
|
calculateDistance(position) {
|
||||||
|
if (!position) return Infinity;
|
||||||
|
|
||||||
|
// Get local player position (would be from game state)
|
||||||
|
const localPos = this.getLocalPlayerPosition();
|
||||||
|
if (!localPos) return Infinity;
|
||||||
|
|
||||||
|
const dx = position.x - localPos.x;
|
||||||
|
const dy = position.y - localPos.y;
|
||||||
|
const dz = position.z - localPos.z;
|
||||||
|
|
||||||
|
return Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate direction to entity
|
||||||
|
*/
|
||||||
|
calculateDirection(position) {
|
||||||
|
if (!position) return 'unknown';
|
||||||
|
|
||||||
|
const localPos = this.getLocalPlayerPosition();
|
||||||
|
if (!localPos) return 'unknown';
|
||||||
|
|
||||||
|
const dx = position.x - localPos.x;
|
||||||
|
const dz = position.z - localPos.z;
|
||||||
|
|
||||||
|
// Calculate angle
|
||||||
|
const angle = Math.atan2(dz, dx) * (180 / Math.PI);
|
||||||
|
|
||||||
|
// Convert to direction
|
||||||
|
if (angle >= -22.5 && angle < 22.5) return 'E';
|
||||||
|
if (angle >= 22.5 && angle < 67.5) return 'SE';
|
||||||
|
if (angle >= 67.5 && angle < 112.5) return 'S';
|
||||||
|
if (angle >= 112.5 && angle < 157.5) return 'SW';
|
||||||
|
if (angle >= 157.5 || angle < -157.5) return 'W';
|
||||||
|
if (angle >= -157.5 && angle < -112.5) return 'NW';
|
||||||
|
if (angle >= -112.5 && angle < -67.5) return 'N';
|
||||||
|
if (angle >= -67.5 && angle < -22.5) return 'NE';
|
||||||
|
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get local player position (placeholder)
|
||||||
|
*/
|
||||||
|
getLocalPlayerPosition() {
|
||||||
|
// In real implementation, this would get position from game state
|
||||||
|
// For now, return a placeholder
|
||||||
|
return { x: 0, y: 0, z: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display search results
|
||||||
|
*/
|
||||||
|
displayResults(results) {
|
||||||
|
this.clearResults();
|
||||||
|
|
||||||
|
if (results.length === 0) {
|
||||||
|
const noResults = document.createElement('div');
|
||||||
|
noResults.className = 'search-no-results';
|
||||||
|
noResults.textContent = 'No results found';
|
||||||
|
noResults.style.cssText = `
|
||||||
|
padding: 10px;
|
||||||
|
color: #888;
|
||||||
|
font-style: italic;
|
||||||
|
`;
|
||||||
|
this.resultsContainer.appendChild(noResults);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const result of results) {
|
||||||
|
const resultElement = this.createResultElement(result);
|
||||||
|
this.resultsContainer.appendChild(resultElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create result element
|
||||||
|
*/
|
||||||
|
createResultElement(result) {
|
||||||
|
const element = document.createElement('div');
|
||||||
|
element.className = 'search-result';
|
||||||
|
element.style.cssText = `
|
||||||
|
padding: 10px 15px;
|
||||||
|
border-bottom: 1px solid rgba(74, 240, 192, 0.2);
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
element.innerHTML = `
|
||||||
|
<div>
|
||||||
|
<div style="font-weight: bold; color: #4af0c0;">${result.name}</div>
|
||||||
|
<div style="font-size: 12px; color: #888;">${result.type || 'Unknown'}</div>
|
||||||
|
</div>
|
||||||
|
<div style="text-align: right;">
|
||||||
|
<div style="color: #4af0c0;">${result.distance.toFixed(1)}m</div>
|
||||||
|
<div style="font-size: 12px; color: #888;">${result.direction}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
element.addEventListener('click', () => {
|
||||||
|
this.selectResult(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select a search result
|
||||||
|
*/
|
||||||
|
selectResult(result) {
|
||||||
|
console.log('[SpatialSearch] Selected:', result);
|
||||||
|
|
||||||
|
// Show path arrow
|
||||||
|
this.showPathArrow(result);
|
||||||
|
|
||||||
|
// Call callback
|
||||||
|
this.onResultSelect(result);
|
||||||
|
|
||||||
|
// Hide search
|
||||||
|
this.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show path arrow pointing to result
|
||||||
|
*/
|
||||||
|
showPathArrow(result) {
|
||||||
|
if (!this.pathArrow) return;
|
||||||
|
|
||||||
|
// Update arrow direction
|
||||||
|
const arrow = this.pathArrow.querySelector('span') || this.pathArrow;
|
||||||
|
const directionArrows = {
|
||||||
|
'N': '↑',
|
||||||
|
'NE': '↗',
|
||||||
|
'E': '→',
|
||||||
|
'SE': '↘',
|
||||||
|
'S': '↓',
|
||||||
|
'SW': '↙',
|
||||||
|
'W': '←',
|
||||||
|
'NW': '↖'
|
||||||
|
};
|
||||||
|
|
||||||
|
arrow.innerHTML = directionArrows[result.direction] || '?';
|
||||||
|
|
||||||
|
// Show arrow
|
||||||
|
this.pathArrow.style.display = 'flex';
|
||||||
|
|
||||||
|
// Update title
|
||||||
|
this.pathArrow.title = `${result.name} - ${result.distance.toFixed(1)}m ${result.direction}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear search results
|
||||||
|
*/
|
||||||
|
clearResults() {
|
||||||
|
if (this.resultsContainer) {
|
||||||
|
this.resultsContainer.innerHTML = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear search and hide arrow
|
||||||
|
*/
|
||||||
|
clearSearch() {
|
||||||
|
this.clearResults();
|
||||||
|
this.searchInput.value = '';
|
||||||
|
|
||||||
|
if (this.pathArrow) {
|
||||||
|
this.pathArrow.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register an entity
|
||||||
|
*/
|
||||||
|
registerEntity(id, entity) {
|
||||||
|
this.entities.set(id, entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregister an entity
|
||||||
|
*/
|
||||||
|
unregisterEntity(id) {
|
||||||
|
this.entities.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update entity position
|
||||||
|
*/
|
||||||
|
updateEntityPosition(id, position) {
|
||||||
|
const entity = this.entities.get(id);
|
||||||
|
if (entity) {
|
||||||
|
entity.position = position;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get search status
|
||||||
|
*/
|
||||||
|
getStatus() {
|
||||||
|
return {
|
||||||
|
entityCount: this.entities.size,
|
||||||
|
maxDistance: this.maxDistance,
|
||||||
|
searchDelay: this.searchDelay
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export for use in other modules
|
||||||
|
if (typeof module !== 'undefined' && module.exports) {
|
||||||
|
module.exports = SpatialSearch;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global instance for browser use
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.SpatialSearch = SpatialSearch;
|
||||||
|
|
||||||
|
// Auto-initialize
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const search = new SpatialSearch({
|
||||||
|
maxDistance: 1000,
|
||||||
|
onResultSelect: (result) => {
|
||||||
|
console.log('Selected:', result);
|
||||||
|
// In real implementation, this would navigate to the entity
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Store globally
|
||||||
|
window.spatialSearch = search;
|
||||||
|
|
||||||
|
// Add keyboard shortcut (Ctrl+F or Cmd+F)
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key === 'f') {
|
||||||
|
e.preventDefault();
|
||||||
|
search.toggle();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
223
tests/test_spatial_search.js
Normal file
223
tests/test_spatial_search.js
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
/**
|
||||||
|
* Tests for Spatial Search
|
||||||
|
* Issue #1540: feat: spatial search — find nearest user/object by name
|
||||||
|
*/
|
||||||
|
|
||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const path = require('node:path');
|
||||||
|
|
||||||
|
const ROOT = path.resolve(__dirname, '..');
|
||||||
|
|
||||||
|
// Mock document
|
||||||
|
const mockDocument = {
|
||||||
|
createElement: (tag) => {
|
||||||
|
const element = {
|
||||||
|
style: {},
|
||||||
|
innerHTML: '',
|
||||||
|
textContent: '',
|
||||||
|
placeholder: '',
|
||||||
|
title: '',
|
||||||
|
addEventListener: () => {},
|
||||||
|
appendChild: () => {},
|
||||||
|
querySelector: () => null,
|
||||||
|
querySelectorAll: () => [],
|
||||||
|
focus: () => {}
|
||||||
|
};
|
||||||
|
return element;
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
appendChild: () => {}
|
||||||
|
},
|
||||||
|
getElementById: () => null,
|
||||||
|
addEventListener: () => {}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mock console
|
||||||
|
const mockConsole = {
|
||||||
|
log: () => {},
|
||||||
|
warn: () => {},
|
||||||
|
error: () => {}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load spatial-search.js
|
||||||
|
const spatialSearchPath = path.join(ROOT, 'js', 'spatial-search.js');
|
||||||
|
const spatialSearchCode = fs.readFileSync(spatialSearchPath, 'utf8');
|
||||||
|
|
||||||
|
// Create VM context
|
||||||
|
const context = {
|
||||||
|
module: { exports: {} },
|
||||||
|
exports: {},
|
||||||
|
console: mockConsole,
|
||||||
|
document: mockDocument,
|
||||||
|
window: {
|
||||||
|
addEventListener: () => {},
|
||||||
|
location: { protocol: 'http:', hostname: 'localhost' }
|
||||||
|
},
|
||||||
|
Math: Math,
|
||||||
|
setTimeout: () => {},
|
||||||
|
clearTimeout: () => {}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Execute spatial-search.js in context
|
||||||
|
const vm = require('node:vm');
|
||||||
|
vm.runInNewContext(spatialSearchCode, context);
|
||||||
|
|
||||||
|
// Get SpatialSearch class
|
||||||
|
const SpatialSearch = context.window.SpatialSearch || context.module.exports;
|
||||||
|
|
||||||
|
test('SpatialSearch loads correctly', () => {
|
||||||
|
assert.ok(SpatialSearch, 'SpatialSearch should be defined');
|
||||||
|
assert.ok(typeof SpatialSearch === 'function', 'SpatialSearch should be a constructor');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('SpatialSearch can be instantiated', () => {
|
||||||
|
const search = new SpatialSearch();
|
||||||
|
assert.ok(search, 'SpatialSearch instance should be created');
|
||||||
|
assert.equal(search.maxDistance, 1000, 'Should have default max distance');
|
||||||
|
assert.equal(search.searchDelay, 300, 'Should have default search delay');
|
||||||
|
assert.ok(search.entities, 'Should have entities Map');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('SpatialSearch can register entities', () => {
|
||||||
|
const search = new SpatialSearch();
|
||||||
|
|
||||||
|
search.registerEntity('user1', {
|
||||||
|
name: 'Alice',
|
||||||
|
type: 'user',
|
||||||
|
position: { x: 10, y: 0, z: 5 }
|
||||||
|
});
|
||||||
|
|
||||||
|
search.registerEntity('user2', {
|
||||||
|
name: 'Bob',
|
||||||
|
type: 'user',
|
||||||
|
position: { x: 20, y: 0, z: 10 }
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(search.entities.size, 2, 'Should have 2 entities');
|
||||||
|
assert.ok(search.entities.get('user1'), 'Should have user1');
|
||||||
|
assert.ok(search.entities.get('user2'), 'Should have user2');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('SpatialSearch can unregister entities', () => {
|
||||||
|
const search = new SpatialSearch();
|
||||||
|
|
||||||
|
search.registerEntity('user1', { name: 'Alice', type: 'user' });
|
||||||
|
search.registerEntity('user2', { name: 'Bob', type: 'user' });
|
||||||
|
|
||||||
|
assert.equal(search.entities.size, 2, 'Should have 2 entities');
|
||||||
|
|
||||||
|
search.unregisterEntity('user1');
|
||||||
|
assert.equal(search.entities.size, 1, 'Should have 1 entity after unregister');
|
||||||
|
assert.ok(!search.entities.get('user1'), 'Should not have user1');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('SpatialSearch can update entity position', () => {
|
||||||
|
const search = new SpatialSearch();
|
||||||
|
|
||||||
|
search.registerEntity('user1', {
|
||||||
|
name: 'Alice',
|
||||||
|
type: 'user',
|
||||||
|
position: { x: 10, y: 0, z: 5 }
|
||||||
|
});
|
||||||
|
|
||||||
|
const newPos = { x: 15, y: 0, z: 10 };
|
||||||
|
search.updateEntityPosition('user1', newPos);
|
||||||
|
|
||||||
|
const entity = search.entities.get('user1');
|
||||||
|
assert.deepEqual(entity.position, newPos, 'Should update position');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('SpatialSearch calculates distance correctly', () => {
|
||||||
|
const search = new SpatialSearch();
|
||||||
|
|
||||||
|
// Mock getLocalPlayerPosition
|
||||||
|
search.getLocalPlayerPosition = () => ({ x: 0, y: 0, z: 0 });
|
||||||
|
|
||||||
|
const pos1 = { x: 3, y: 0, z: 4 };
|
||||||
|
const distance1 = search.calculateDistance(pos1);
|
||||||
|
assert.equal(distance1, 5, 'Should calculate 3-4-5 triangle correctly');
|
||||||
|
|
||||||
|
const pos2 = { x: 0, y: 0, z: 0 };
|
||||||
|
const distance2 = search.calculateDistance(pos2);
|
||||||
|
assert.equal(distance2, 0, 'Should be 0 at same position');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('SpatialSearch calculates direction correctly', () => {
|
||||||
|
const search = new SpatialSearch();
|
||||||
|
|
||||||
|
// Mock getLocalPlayerPosition
|
||||||
|
search.getLocalPlayerPosition = () => ({ x: 0, y: 0, z: 0 });
|
||||||
|
|
||||||
|
// Test different directions
|
||||||
|
assert.equal(search.calculateDirection({ x: 10, y: 0, z: 0 }), 'E', 'Should be East');
|
||||||
|
assert.equal(search.calculateDirection({ x: 0, y: 0, z: 10 }), 'S', 'Should be South');
|
||||||
|
assert.equal(search.calculateDirection({ x: -10, y: 0, z: 0 }), 'W', 'Should be West');
|
||||||
|
assert.equal(search.calculateDirection({ x: 0, y: 0, z: -10 }), 'N', 'Should be North');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('SpatialSearch searches entities correctly', () => {
|
||||||
|
const search = new SpatialSearch();
|
||||||
|
|
||||||
|
// Mock getLocalPlayerPosition
|
||||||
|
search.getLocalPlayerPosition = () => ({ x: 0, y: 0, z: 0 });
|
||||||
|
|
||||||
|
// Register entities
|
||||||
|
search.registerEntity('user1', {
|
||||||
|
name: 'Alice',
|
||||||
|
type: 'user',
|
||||||
|
position: { x: 10, y: 0, z: 0 }
|
||||||
|
});
|
||||||
|
|
||||||
|
search.registerEntity('user2', {
|
||||||
|
name: 'Bob',
|
||||||
|
type: 'user',
|
||||||
|
position: { x: 20, y: 0, z: 0 }
|
||||||
|
});
|
||||||
|
|
||||||
|
search.registerEntity('obj1', {
|
||||||
|
name: 'Apple',
|
||||||
|
type: 'object',
|
||||||
|
position: { x: 5, y: 0, z: 0 }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Search for 'ali'
|
||||||
|
const results1 = search.searchEntities('ali');
|
||||||
|
assert.equal(results1.length, 1, 'Should find 1 result for "ali"');
|
||||||
|
assert.equal(results1[0].name, 'Alice', 'Should find Alice');
|
||||||
|
|
||||||
|
// Search for 'bob'
|
||||||
|
const results2 = search.searchEntities('bob');
|
||||||
|
assert.equal(results2.length, 1, 'Should find 1 result for "bob"');
|
||||||
|
assert.equal(results2[0].name, 'Bob', 'Should find Bob');
|
||||||
|
|
||||||
|
// Search for 'user' (type)
|
||||||
|
const results3 = search.searchEntities('user');
|
||||||
|
assert.equal(results3.length, 2, 'Should find 2 results for "user"');
|
||||||
|
|
||||||
|
// Search for 'apple'
|
||||||
|
const results4 = search.searchEntities('apple');
|
||||||
|
assert.equal(results4.length, 1, 'Should find 1 result for "apple"');
|
||||||
|
assert.equal(results4[0].name, 'Apple', 'Should find Apple');
|
||||||
|
|
||||||
|
// Search for non-existent
|
||||||
|
const results5 = search.searchEntities('xyz');
|
||||||
|
assert.equal(results5.length, 0, 'Should find 0 results for "xyz"');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('SpatialSearch gets status', () => {
|
||||||
|
const search = new SpatialSearch();
|
||||||
|
|
||||||
|
search.registerEntity('user1', { name: 'Alice', type: 'user' });
|
||||||
|
search.registerEntity('user2', { name: 'Bob', type: 'user' });
|
||||||
|
|
||||||
|
const status = search.getStatus();
|
||||||
|
|
||||||
|
assert.ok(status, 'Should return status object');
|
||||||
|
assert.equal(status.entityCount, 2, 'Should have 2 entities');
|
||||||
|
assert.equal(status.maxDistance, 1000, 'Should have correct max distance');
|
||||||
|
assert.equal(status.searchDelay, 300, 'Should have correct search delay');
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('All SpatialSearch tests passed!');
|
||||||
Reference in New Issue
Block a user