Skip to main content
Glama
spatial.repo.ts12.3 kB
import Database from 'better-sqlite3'; import { RoomNode, RoomNodeSchema, Exit, NodeNetwork, NodeNetworkSchema } from '../../schema/spatial.js'; export class SpatialRepository { constructor(private db: Database.Database) { } create(room: RoomNode): void { const validRoom = RoomNodeSchema.parse(room); const stmt = this.db.prepare(` INSERT INTO room_nodes ( id, name, base_description, biome_context, atmospherics, exits, entity_ids, created_at, updated_at, visited_count, last_visited_at, network_id, local_x, local_y ) VALUES ( @id, @name, @baseDescription, @biomeContext, @atmospherics, @exits, @entityIds, @createdAt, @updatedAt, @visitedCount, @lastVisitedAt, @networkId, @localX, @localY ) `); stmt.run({ id: validRoom.id, name: validRoom.name, baseDescription: validRoom.baseDescription, biomeContext: validRoom.biomeContext, atmospherics: JSON.stringify(validRoom.atmospherics), exits: JSON.stringify(validRoom.exits), entityIds: JSON.stringify(validRoom.entityIds), createdAt: validRoom.createdAt, updatedAt: validRoom.updatedAt, visitedCount: validRoom.visitedCount, lastVisitedAt: validRoom.lastVisitedAt || null, networkId: validRoom.networkId || null, localX: validRoom.localX !== undefined ? validRoom.localX : null, localY: validRoom.localY !== undefined ? validRoom.localY : null, }); } findById(id: string): RoomNode | null { const stmt = this.db.prepare('SELECT * FROM room_nodes WHERE id = ?'); const row = stmt.get(id) as RoomNodeRow | undefined; if (!row) return null; return this.rowToRoomNode(row); } findAll(): RoomNode[] { const stmt = this.db.prepare('SELECT * FROM room_nodes ORDER BY name'); const rows = stmt.all() as RoomNodeRow[]; return rows.map(row => this.rowToRoomNode(row)); } findByBiome(biome: string): RoomNode[] { const stmt = this.db.prepare('SELECT * FROM room_nodes WHERE biome_context = ? ORDER BY name'); const rows = stmt.all(biome) as RoomNodeRow[]; return rows.map(row => this.rowToRoomNode(row)); } update(id: string, updates: Partial<RoomNode>): RoomNode | null { const existing = this.findById(id); if (!existing) return null; const updated = { ...existing, ...updates, updatedAt: new Date().toISOString() }; // Validate full object const validRoom = RoomNodeSchema.parse(updated); const stmt = this.db.prepare(` UPDATE room_nodes SET name = ?, base_description = ?, biome_context = ?, atmospherics = ?, exits = ?, entity_ids = ?, visited_count = ?, last_visited_at = ?, updated_at = ?, network_id = ?, local_x = ?, local_y = ? WHERE id = ? `); stmt.run( validRoom.name, validRoom.baseDescription, validRoom.biomeContext, JSON.stringify(validRoom.atmospherics), JSON.stringify(validRoom.exits), JSON.stringify(validRoom.entityIds), validRoom.visitedCount, validRoom.lastVisitedAt || null, validRoom.updatedAt, validRoom.networkId || null, validRoom.localX !== undefined ? validRoom.localX : null, validRoom.localY !== undefined ? validRoom.localY : null, id ); return validRoom; } delete(id: string): boolean { const stmt = this.db.prepare('DELETE FROM room_nodes WHERE id = ?'); const result = stmt.run(id); return result.changes > 0; } // Helper methods for entity management addEntityToRoom(roomId: string, entityId: string): void { const room = this.findById(roomId); if (!room) throw new Error(`Room ${roomId} not found`); if (!room.entityIds.includes(entityId)) { room.entityIds.push(entityId); this.update(roomId, { entityIds: room.entityIds }); } } removeEntityFromRoom(roomId: string, entityId: string): void { const room = this.findById(roomId); if (!room) throw new Error(`Room ${roomId} not found`); room.entityIds = room.entityIds.filter(id => id !== entityId); this.update(roomId, { entityIds: room.entityIds }); } getEntitiesInRoom(roomId: string): string[] { const room = this.findById(roomId); if (!room) return []; return room.entityIds; } // Helper methods for exit management addExit(roomId: string, exit: Exit): void { const room = this.findById(roomId); if (!room) throw new Error(`Room ${roomId} not found`); room.exits.push(exit); this.update(roomId, { exits: room.exits }); } findConnectedRooms(roomId: string): RoomNode[] { const room = this.findById(roomId); if (!room) return []; const connectedRooms: RoomNode[] = []; for (const exit of room.exits) { const targetRoom = this.findById(exit.targetNodeId); if (targetRoom) { connectedRooms.push(targetRoom); } } return connectedRooms; } incrementVisitCount(roomId: string): void { const room = this.findById(roomId); if (!room) throw new Error(`Room ${roomId} not found`); this.update(roomId, { visitedCount: room.visitedCount + 1, lastVisitedAt: new Date().toISOString() }); } // ============================================================ // LOCAL COORDINATE QUERIES (within networks) // ============================================================ findRoomsByLocalCoordinates(networkId: string, x: number, y: number): RoomNode[] { const stmt = this.db.prepare( 'SELECT * FROM room_nodes WHERE network_id = ? AND local_x = ? AND local_y = ? ORDER BY name' ); const rows = stmt.all(networkId, x, y) as RoomNodeRow[]; return rows.map(row => this.rowToRoomNode(row)); } // ============================================================ // NETWORK COORDINATE QUERIES (world map coordinates) // ============================================================ findNetworksInBoundingBox(minX: number, maxX: number, minY: number, maxY: number): NodeNetwork[] { const stmt = this.db.prepare(` SELECT * FROM node_networks WHERE center_x >= ? AND center_x <= ? AND center_y >= ? AND center_y <= ? ORDER BY center_x, center_y, name `); const rows = stmt.all(minX, maxX, minY, maxY) as NodeNetworkRow[]; return rows.map(row => this.rowToNodeNetwork(row)); } findNearestNetwork(x: number, y: number): NodeNetwork | null { // Find network with minimum Euclidean distance const stmt = this.db.prepare(` SELECT *, ((center_x - ?) * (center_x - ?) + (center_y - ?) * (center_y - ?)) as distance_squared FROM node_networks ORDER BY distance_squared LIMIT 1 `); const row = stmt.get(x, x, y, y) as NodeNetworkRow | undefined; if (!row) return null; return this.rowToNodeNetwork(row); } // ============================================================ // NODE NETWORK METHODS // ============================================================ createNetwork(network: NodeNetwork): void { const validNetwork = NodeNetworkSchema.parse(network); const stmt = this.db.prepare(` INSERT INTO node_networks ( id, name, type, world_id, center_x, center_y, bounding_box, created_at, updated_at ) VALUES ( @id, @name, @type, @worldId, @centerX, @centerY, @boundingBox, @createdAt, @updatedAt ) `); stmt.run({ id: validNetwork.id, name: validNetwork.name, type: validNetwork.type, worldId: validNetwork.worldId, centerX: validNetwork.centerX, centerY: validNetwork.centerY, boundingBox: validNetwork.boundingBox ? JSON.stringify(validNetwork.boundingBox) : null, createdAt: validNetwork.createdAt, updatedAt: validNetwork.updatedAt, }); } findNetworkById(id: string): NodeNetwork | null { const stmt = this.db.prepare('SELECT * FROM node_networks WHERE id = ?'); const row = stmt.get(id) as NodeNetworkRow | undefined; if (!row) return null; return this.rowToNodeNetwork(row); } findRoomsByNetwork(networkId: string): RoomNode[] { const stmt = this.db.prepare( 'SELECT * FROM room_nodes WHERE network_id = ? ORDER BY name' ); const rows = stmt.all(networkId) as RoomNodeRow[]; return rows.map(row => this.rowToRoomNode(row)); } findNetworksAtCoordinates(x: number, y: number): NodeNetwork[] { // Find networks where (x,y) is within their bounds or at their center const stmt = this.db.prepare(` SELECT * FROM node_networks WHERE center_x = ? AND center_y = ? `); const rows = stmt.all(x, y) as NodeNetworkRow[]; // Also check bounding boxes const boundedNetworks = this.db.prepare(` SELECT * FROM node_networks WHERE bounding_box IS NOT NULL `).all() as NodeNetworkRow[]; const allNetworks = [...rows]; for (const row of boundedNetworks) { if (row.bounding_box) { const bbox = JSON.parse(row.bounding_box); if (x >= bbox.minX && x <= bbox.maxX && y >= bbox.minY && y <= bbox.maxY) { if (!allNetworks.find(n => n.id === row.id)) { allNetworks.push(row); } } } } return allNetworks.map(row => this.rowToNodeNetwork(row)); } private rowToRoomNode(row: RoomNodeRow): RoomNode { return RoomNodeSchema.parse({ id: row.id, name: row.name, baseDescription: row.base_description, biomeContext: row.biome_context, atmospherics: JSON.parse(row.atmospherics), exits: JSON.parse(row.exits), entityIds: JSON.parse(row.entity_ids), createdAt: row.created_at, updatedAt: row.updated_at, visitedCount: row.visited_count, lastVisitedAt: row.last_visited_at || undefined, networkId: row.network_id || undefined, localX: row.local_x !== null ? row.local_x : undefined, localY: row.local_y !== null ? row.local_y : undefined, }); } private rowToNodeNetwork(row: NodeNetworkRow): NodeNetwork { return NodeNetworkSchema.parse({ id: row.id, name: row.name, type: row.type, worldId: row.world_id, centerX: row.center_x, centerY: row.center_y, boundingBox: row.bounding_box ? JSON.parse(row.bounding_box) : undefined, createdAt: row.created_at, updatedAt: row.updated_at, }); } } interface RoomNodeRow { id: string; name: string; base_description: string; biome_context: string; atmospherics: string; exits: string; entity_ids: string; created_at: string; updated_at: string; visited_count: number; last_visited_at: string | null; network_id: string | null; local_x: number | null; local_y: number | null; } interface NodeNetworkRow { id: string; name: string; type: 'cluster' | 'linear'; world_id: string; center_x: number; center_y: number; bounding_box: string | null; created_at: string; updated_at: string; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Mnehmos/rpg-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server