PageSourceSearch

https://wwmmap.pages.dev/js/map_2026/tracking.js

js wwmmap.pages.dev collected 2026-10-03 06:06:08 UTC 6,674 bytes, 199 lines download raw bytes

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.