1import { i18n } from './i18n.js'; 2 3const TRACKING_SOURCE_ID = 'tracking-line-source'; 4const TRACKING_LAYER_ID = 'tracking-line-layer'; 5 6/** 7 * Reusable tracking module. 8 * Draws a line from the player marker to a target position on the map, 9 * displays the distance (including the in-game Y axis difference). 10 * 11 * Usage: 12 * const tracker = new Tracker(map, gameSyncer); 13 * tracker.track(targetLng, targetLat, targetGameY); 14 * tracker.stop(); 15 */ 16export class Tracker { 17 /** 18 * @param {import('./gflatmap.js').Map} map - GFlatMap instance 19 * @param {import('./gamesync.js').GameSyncer2} gameSyncer 20 */ 21 constructor(map, gameSyncer) { 22 this.map = map; 23 this.gameSyncer = gameSyncer; 24 this._active = false; 25 this._targetLngLat = null; 26 this._targetGameY = null; 27 this._updateBound = this._update.bind(this); 28 this._labelEl = null; 29 this._labelMarker = null; 30 } 31 32 get active() { return this._active; } 33 34 /** 35 * Start tracking from the player to the given map coordinates. 36 * @param {number} lng - Target longitude (map coord) 37 * @param {number} lat - Target latitude (map coord) 38 * @param {number|null} gameY - Target in-game Y (height), null if unknown 39 */ 40 track(lng, lat, gameY) { 41 this.stop(); // clean up any previous tracking 42 43 this._targetLngLat = [lng, lat]; 44 this._targetGameY = gameY; 45 this._active = true; 46 47 // Add GeoJSON source for the line 48 this.map.addSource(TRACKING_SOURCE_ID, { 49 type: 'geojson', 50 data: this._makeLineGeoJSON([0, 0], [lng, lat]) 51 }); 52 53 // Add line layer 54 this.map.addLayer({ 55 id: TRACKING_LAYER_ID, 56 type: 'line', 57 source: TRACKING_SOURCE_ID, 58 paint: { 59 'line-color': '#00c8ff', 60 'line-width': 2.5, 61 'line-opacity': 0.85, 62 } 63 }); 64 65 // Create distance label 66 this._createLabel(); 67 68 // Start the update loop 69 this._raf = requestAnimationFrame(this._updateBound); 70 } 71 72 stop() { 73 this._active = false; 74 75 if (this._raf) { 76 cancelAnimationFrame(this._raf); 77 this._raf = null; 78 } 79 80 // Remove label 81 if (this._labelMarker) { 82 this._labelMarker.remove(); 83 this._labelMarker = null; 84 this._labelEl = null; 85 } 86 87 // Remove layer and source 88 try { this.map.removeLayer(TRACKING_LAYER_ID); } catch (_) {} 89 try { this.map.removeSource(TRACKING_SOURCE_ID); } catch (_) {} 90 } 91 92 _createLabel() { 93 const el = document.createElement('div'); 94 el.className = 'tracking-distance-label'; 95 el.textContent = ''; 96 this._labelEl = el; 97 98 this._labelMarker = new GFlatMap.Marker({ element: el, interactive: false }) 99 .setLngLat([0, 0]) 100 .addTo(this.map); 101 } 102 103 _update() { 104 if (!this._active) return; 105 106 const playerLngLat = this._getPlayerLngLat(); 107 if (playerLngLat) { 108 // Update line source 109 const src = this.map.getSource(TRACKING_SOURCE_ID); 110 if (src) { 111 src.setData(this._makeLineGeoJSON( 112 [playerLngLat.lng, playerLngLat.lat], 113 this._targetLngLat 114 )); 115 } 116 117 // Compute distance 118 const dist = this._computeDistance(playerLngLat); 119 120 // Update label at midpoint 121 if (this._labelEl) { 122 this._labelEl.textContent = dist.text; 123 } 124 if (this._labelMarker) { 125 const midLng = (playerLngLat.lng + this._targetLngLat[0]) / 2; 126 const midLat = (playerLngLat.lat + this._targetLngLat[1]) / 2; 127 this._labelMarker.setLngLat([midLng, midLat]); 128 } 129 } 130 131 this._raf = requestAnimationFrame(this._updateBound); 132 } 133 134 _getPlayerLngLat() { 135 // GameSyncer2 stores playerMarker with current lngLat 136 const syncer = this.gameSyncer; 137 if (!syncer || !syncer.isSyncing) return null; 138 return syncer.playerMarker?.getLngLat() || null; 139 } 140 141 _computeDistance(playerLngLat) { 142 const markerManager = this.gameSyncer.markerManager; 143 const playerGameY = markerManager?.currentPlayerGameY; 144 const targetGameY = this._targetGameY; 145 146 // Horizontal distance in map coords â game meters 147 // map coords are in pixels, PIXEL_PER_METER = worldRange / gameRange 148 // We use the corellator to convert both points to game coords for accurate distance 149 let horizontalDist = null; 150 let verticalDiff = null; 151 152 try { 153 const corellator = this.gameSyncer.corellator; 154 if (corellator) { 155 const playerGame = corellator.mapToGame(playerLngLat.lng, playerLngLat.lat); 156 const targetGame = corellator.mapToGame(this._targetLngLat[0], this._targetLngLat[1]); 157 const dx = targetGame.game_x - playerGame.game_x; 158 const dz = targetGame.game_z - playerGame.game_z; 159 horizontalDist = Math.sqrt(dx * dx + dz * dz); 160 } 161 } catch (_) {} 162 163 // Vertical difference (Y axis) 164 if (playerGameY != null && targetGameY != null) { 165 verticalDiff = targetGameY - playerGameY; 166 } 167 168 // Compute 3D distance if we have vertical info 169 let totalDist = horizontalDist; 170 if (horizontalDist != null && verticalDiff != null) { 171 totalDist = Math.sqrt(horizontalDist * horizontalDist + verticalDiff * verticalDiff); 172 } 173 174 // Format text 175 let text = ''; 176 if (totalDist != null) { 177 text = `${Math.round(totalDist)}m`; 178 if (verticalDiff != null && Math.abs(verticalDiff) > 2) { 179 const arrow = verticalDiff > 0 ? 'â' : 'â'; 180 text += ` (${arrow}${Math.round(Math.abs(verticalDiff))}m)`; 181 } 182 } 183 184 return { totalDist, horizontalDist, verticalDiff, text }; 185 } 186 187 _makeLineGeoJSON(from, to) { 188 return { 189 type: 'FeatureCollection', 190 features: [{ 191 type: 'Feature', 192 geometry: { 193 type: 'LineString', 194 coordinates: [from, to] 195 } 196 }] 197 }; 198 } 199}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.