|
243 | 243 |
|
244 | 244 | // ========== BLOCKS ========== |
245 | 245 | let blocks = []; |
| 246 | + const STORAGE_KEY = 'robotics_playground_state'; |
| 247 | + let lastSaveTime = 0; |
246 | 248 |
|
247 | 249 | function generateBlocks() { |
248 | 250 | blocks = []; |
|
301 | 303 | } |
302 | 304 | robot.holdingBlock = null; |
303 | 305 | showMsg('Dropped!'); |
| 306 | + saveState(); |
304 | 307 | } else { |
305 | 308 | // PICK |
306 | 309 | for (let b of blocks) { |
|
312 | 315 | b.held = true; |
313 | 316 | robot.holdingBlock = b.id; |
314 | 317 | showMsg('Grabbed! SPACE to drop'); |
| 318 | + saveState(); |
315 | 319 | break; |
316 | 320 | } |
317 | 321 | } |
|
355 | 359 | } |
356 | 360 | updatePartsList(); |
357 | 361 | showMsg(`${type.charAt(0).toUpperCase() + type.slice(1)} added!`); |
| 362 | + saveState(); |
358 | 363 | } |
359 | 364 |
|
360 | 365 | function removePart(type) { |
|
396 | 401 |
|
397 | 402 | updatePartsList(); |
398 | 403 | showMsg(`${type.charAt(0).toUpperCase() + type.slice(1)} removed!`); |
| 404 | + saveState(); |
399 | 405 | } |
400 | 406 |
|
401 | 407 | function resetAll() { |
|
422 | 428 | document.getElementById('detection-info').innerHTML = '<div class="text-gray-500">LiDAR not installed</div>'; |
423 | 429 | document.getElementById('detection-indicator').classList.add('hidden'); |
424 | 430 | updatePartsList(); |
| 431 | + localStorage.removeItem(STORAGE_KEY); |
425 | 432 | } |
426 | 433 |
|
427 | 434 | function updatePartsList() { |
|
432 | 439 | function updatePower() { |
433 | 440 | motorPower = +document.getElementById('power-slider').value; |
434 | 441 | document.getElementById('power-val').textContent = motorPower + '%'; |
| 442 | + saveState(); |
435 | 443 | } |
436 | 444 |
|
437 | 445 | function updateTurn() { |
438 | 446 | turnSpeed = +document.getElementById('turn-slider').value; |
439 | 447 | document.getElementById('turn-val').textContent = turnSpeed + '°'; |
| 448 | + saveState(); |
440 | 449 | } |
441 | 450 |
|
442 | 451 | function updateColor() { |
443 | 452 | robot.color = document.getElementById('color-picker').value; |
| 453 | + saveState(); |
444 | 454 | } |
445 | 455 |
|
446 | 456 | // ========== GAME LOOP ========== |
|
731 | 741 | function loop() { |
732 | 742 | update(); |
733 | 743 | render(); |
| 744 | + const now = Date.now(); |
| 745 | + if (robot.parts.chassis && now - lastSaveTime > 1000) { |
| 746 | + saveState(); |
| 747 | + lastSaveTime = now; |
| 748 | + } |
734 | 749 | requestAnimationFrame(loop); |
735 | 750 | } |
736 | 751 |
|
737 | 752 | // ========== LIDAR DETECTION ========== |
738 | 753 | let lastDetectedId = null; |
739 | 754 |
|
| 755 | + loadState(); |
740 | 756 | loop(); |
741 | 757 |
|
742 | 758 | function segmentsIntersect(x1, y1, x2, y2, x3, y3, x4, y4) { |
|
832 | 848 | indicator.classList.add('hidden'); |
833 | 849 | } |
834 | 850 | } |
| 851 | + |
| 852 | + // ========== STATE PERSISTENCE ========== |
| 853 | + function saveState() { |
| 854 | + try { |
| 855 | + const state = { |
| 856 | + robot: { |
| 857 | + x: robot.x, |
| 858 | + y: robot.y, |
| 859 | + angle: robot.angle, |
| 860 | + color: robot.color, |
| 861 | + holdingBlock: robot.holdingBlock, |
| 862 | + parts: { ...robot.parts }, |
| 863 | + }, |
| 864 | + motorPower, |
| 865 | + turnSpeed, |
| 866 | + blocks: blocks.map(b => ({ ...b })), |
| 867 | + }; |
| 868 | + localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); |
| 869 | + } catch (e) { |
| 870 | + // localStorage may be unavailable |
| 871 | + } |
| 872 | + } |
| 873 | + |
| 874 | + function loadState() { |
| 875 | + try { |
| 876 | + const saved = localStorage.getItem(STORAGE_KEY); |
| 877 | + if (!saved) return; |
| 878 | + const state = JSON.parse(saved); |
| 879 | + if (!state) return; |
| 880 | + |
| 881 | + if (state.robot) { |
| 882 | + robot.x = typeof state.robot.x === 'number' ? Math.max(35, Math.min(canvas.width - 35, state.robot.x)) : canvas.width / 2; |
| 883 | + robot.y = typeof state.robot.y === 'number' ? Math.max(35, Math.min(canvas.height - 35, state.robot.y)) : canvas.height / 2; |
| 884 | + robot.angle = typeof state.robot.angle === 'number' ? state.robot.angle : 0; |
| 885 | + robot.color = state.robot.color || '#0d9488'; |
| 886 | + robot.holdingBlock = state.robot.holdingBlock !== undefined ? state.robot.holdingBlock : null; |
| 887 | + if (state.robot.parts) { |
| 888 | + robot.parts = { ...robot.parts, ...state.robot.parts }; |
| 889 | + } |
| 890 | + } |
| 891 | + |
| 892 | + if (typeof state.motorPower === 'number') { |
| 893 | + motorPower = state.motorPower; |
| 894 | + document.getElementById('power-slider').value = motorPower; |
| 895 | + document.getElementById('power-val').textContent = motorPower + '%'; |
| 896 | + } |
| 897 | + if (typeof state.turnSpeed === 'number') { |
| 898 | + turnSpeed = state.turnSpeed; |
| 899 | + document.getElementById('turn-slider').value = turnSpeed; |
| 900 | + document.getElementById('turn-val').textContent = turnSpeed + '°'; |
| 901 | + } |
| 902 | + |
| 903 | + if (Array.isArray(state.blocks) && state.blocks.length > 0) { |
| 904 | + blocks = state.blocks; |
| 905 | + document.getElementById('block-count').textContent = blocks.length; |
| 906 | + } |
| 907 | + |
| 908 | + document.getElementById('color-picker').value = robot.color; |
| 909 | + |
| 910 | + if (robot.parts.chassis) { |
| 911 | + document.getElementById('start-prompt').classList.add('hidden'); |
| 912 | + } |
| 913 | + if (robot.parts.camera) { |
| 914 | + document.getElementById('camera-feed').classList.remove('hidden'); |
| 915 | + } |
| 916 | + if (robot.parts.lidar) { |
| 917 | + updateDetectionUI(null); |
| 918 | + } |
| 919 | + updatePartsList(); |
| 920 | + } catch (e) { |
| 921 | + // Corrupted state, ignore |
| 922 | + } |
| 923 | + } |
835 | 924 | </script> |
836 | 925 | </body> |
837 | 926 | </html> |
0 commit comments