您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
AI ATC for GeoFS using free PuterJS GPT API
当前为
// ==UserScript== // @name GeoFS AI (GPT) ATC // @namespace https://avramovic.info/ // @version 2025-01-13 // @description AI ATC for GeoFS using free PuterJS GPT API // @author Nemanja Avramovic // @license MIT // @match https://www.geo-fs.com/geofs.php* // @icon https://www.google.com/s2/favicons?sz=64&domain=geo-fs.com // @grant GM.getResourceText // @grant GM.getResourceUrl // @resource airports https://github.com/avramovic/geofs-ai-atc/raw/master/airports.json // @resource radiostatic https://github.com/avramovic/geofs-ai-atc/raw/master/radio-static.mp3 // ==/UserScript== (function() { 'use strict'; const head = document.querySelector('head'); if (head) { const puterJS = document.createElement('script'); puterJS.src = 'https://js.puter.com/v2/'; head.appendChild(puterJS); const growlJS = document.createElement('script'); growlJS.src = 'https://cdn.jsdelivr.net/gh/avramovic/geofs-ai-atc@master/vanilla-notify.min.js'; head.appendChild(growlJS); const growlCSS = document.createElement('link'); growlCSS.href = 'https://cdn.jsdelivr.net/gh/avramovic/geofs-ai-atc@master/vanilla-notify.css'; growlCSS.rel = 'stylesheet'; head.appendChild(growlCSS); } let airports; GM.getResourceText("airports").then((data) => { airports = JSON.parse(data); }); let radiostatic; GM.getResourceText("radiostatic").then((data) => { radiostatic = new Audio('data:audio/mp3;'+data); radiostatic.loop = false; }); let tunedInAtc; let controllers = {}; let context = {}; const observer = new MutationObserver(() => { const menuList = document.querySelector('div.geofs-ui-bottom'); if (menuList && !menuList.querySelector('.geofs-atc-icon')) { const micIcon = document.createElement('i'); micIcon.className = 'material-icons'; micIcon.innerText = 'headset_mic'; const knobIcon = document.createElement('i'); knobIcon.className = 'material-icons'; knobIcon.innerText = 'radio'; const tuneInButton = document.createElement('button'); tuneInButton.className = 'mdl-button mdl-js-button mdl-button--icon geofs-f-standard-ui geofs-tunein-icon'; tuneInButton.title = "Click to set ATC frequency."; tuneInButton.addEventListener('click', (e) => { let nearestAp = findNearestAirport(); let apCode = prompt('Enter airport ICAO code', nearestAp.code); if (apCode == null || apCode === '') { error('You cancelled the dialog.') } else { apCode = apCode.toUpperCase(); if (typeof unsafeWindow.geofs.mainAirportList[apCode] === 'undefined') { error('Airport with code '+ apCode + ' can not be found!'); } else { tunedInAtc = apCode; initController(apCode); info('Your radio is now tuned to '+apCode+' frequency. You will now talk to them.'); } } }); const atcButton = document.createElement('button'); atcButton.className = 'mdl-button mdl-js-button mdl-button--icon geofs-f-standard-ui geofs-atc-icon'; atcButton.title = "Click to talk to the ATC. Ctrl+click (Cmd+click on Mac) to input text instead of talking."; atcButton.addEventListener('click', (e) => { if (typeof tunedInAtc === 'undefined') { error("No frequency set. Click the radio icon to set the frequency!"); } else if (e.ctrlKey || e.metaKey) { let pilotMsg = prompt("Please enter your message to the ATC:"); if (pilotMsg != null && pilotMsg != "") { callAtc(pilotMsg); } else { error("You cancelled the dialog"); } } else { navigator.mediaDevices.getUserMedia({ audio: true }); let SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; let recognition = new SpeechRecognition(); recognition.continuous = false; recognition.lang = 'en-US'; recognition.interimResults = false; recognition.maxAlternatives = 1; recognition.start(); recognition.onresult = (event) => { let pilotMsg = event.results[event.results.length - 1][0].transcript; if (pilotMsg != null && pilotMsg != "") { callAtc(pilotMsg); } else { error("No speech recognized. Speak up?"); } recognition.stop(); }; recognition.onerror = (event) => { error('Speech recognition error: ' + event.error); }; } }); atcButton.appendChild(micIcon); tuneInButton.appendChild(knobIcon); menuList.appendChild(tuneInButton); menuList.appendChild(atcButton); } }); observer.observe(document.body, {childList: true, subtree: true}); function haversine(lat1, lon1, lat2, lon2) { const R = 6371; // Radius of the Earth in kilometers const toRad = (deg) => deg * (Math.PI / 180); const dLat = toRad(lat2 - lat1); const dLon = toRad(lon2 - lon1); const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) * Math.sin(dLon / 2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return R * c; // Distance in kilometers } function findNearestAirport() { let nearestAirport = null; let minDistance = Infinity; for (let apCode in unsafeWindow.geofs.mainAirportList) { let distance = findAirportDistance(apCode); if (distance < minDistance) { minDistance = distance; nearestAirport = { code: apCode, distanceInKm: distance }; } } return nearestAirport; } function findAirportDistance(code) { let aircraftPosition = { lat: unsafeWindow.geofs.aircraft.instance.lastLlaLocation[0], lon: unsafeWindow.geofs.aircraft.instance.lastLlaLocation[1], }; let ap = unsafeWindow.geofs.mainAirportList[code]; let airportPosition = { lat: ap[0], lon: ap[1] }; return haversine( aircraftPosition.lat, aircraftPosition.lon, airportPosition.lat, airportPosition.lon ); } function initController(apCode) { controllers[apCode] = controllers[apCode] || null; if (controllers[apCode] == null) { let date = new Date().toISOString().split('T')[0]; fetch('https://randomuser.me/api/?gender=male&nat=au,br,ca,ch,de,us,dk,fr,gb,in,mx,nl,no,nz,rs,tr,ua,us&seed='+apCode+'-'+date) .then(response => { if (!response.ok) { throw new Error('HTTP error! status: '+response.status); } return response.text(); }).then(resourceText => { let json = JSON.parse(resourceText) controllers[apCode] = json.results[0]; }); } } function error(msg) { vNotify.error({text:msg, title:'Error', visibleDuration: 10000}); } function info(msg, title) { title = title || 'Information'; vNotify.info({text:msg, title:title, visibleDuration: 10000}); } function atcSpeak(text) { let synth = window.speechSynthesis; let voices = synth.getVoices(); let toSpeak = new SpeechSynthesisUtterance(text); toSpeak.voice = voices[0]; synth.speak(toSpeak); } function atcGrowl(text, airport_code) { vNotify.warning({text: text, title: airport_code+' ATC', visibleDuration: 15000}); } function atcMessage(text, airport_code) { atcGrowl(text, airport_code); atcSpeak(text); } function pilotMessage(text) { let user = unsafeWindow.geofs.userRecord; let airplane = unsafeWindow.geofs.aircraft.instance.aircraftRecord; let callsign = "Foo"; if (user.id != 0) { callsign = user.callsign; } vNotify.success({text: text, title: airplane.name+': '+callsign, visibleDuration: 10000}); } function isOnGround() { return unsafeWindow.geofs.animation.values.groundContact === 1; } function seaAltitude() { return unsafeWindow.geofs.animation.values.altitude; } function groundAltitude() { return Math.max(seaAltitude() - unsafeWindow.geofs.animation.values.groundElevationFeet - 50, 0); } let oldNearest = null; // generate controller for the nearest airport for today setInterval(function() { let airport = findNearestAirport(); let airportMeta = airports[airport.code]; if (oldNearest !== airport.code) { let apName = airportMeta ? airportMeta.name+' ('+airport.code+')' : airport.code; info('You are now in range of '+apName+'. Set your radio frequency to <b>'+airport.code+'</b> to tune in with them'); oldNearest = airport.code; } initController(airport.code); }, 500); function callAtc(pilotMsg) { let airport = { distanceInKm: findAirportDistance(tunedInAtc), code: tunedInAtc, }; let airportMeta = airports[airport.code]; let controller = controllers[airport.code]; if (typeof controller === 'undefined') { let apName = airportMeta ? airportMeta.name + ' (' + airport.code + ')' : airport.code; radiostatic.play(); info('Airport '+apName+' seems to be closed right now. Try again later...'); return; } if (airport.distanceInKm > 100) { radiostatic.play(); error('Frequency '+airport.code+' is out of range. You need to be at least 54 nautical miles (100 km) away from the airport to contact it.'); return; } let airportPosition = { lat: unsafeWindow.geofs.mainAirportList[airport.code][0], lon: unsafeWindow.geofs.mainAirportList[airport.code][1], }; let today = new Date().toISOString().split('T')[0]; if (typeof context[airport.code] === "undefined") { let user = unsafeWindow.geofs.userRecord; let pilot = { callsign: 'Foo', name: 'not known', licensed_at: today }; if (user.id != 0) { pilot = { callsign: user.callsign, name: user.firstname + ' ' + user.lastname, licensed_at: user.created }; } let apName = airportMeta ? airportMeta.name + ' (' + airport.code + ')' : airport.code; let season = unsafeWindow.geofs.animation.values.season; let daynight = unsafeWindow.geofs.animation.values.night ? 'night' : 'day'; if (unsafeWindow.geofs.isSnow || unsafeWindow.geofs.isSnowy) { daynight = 'snowy '+daynight; } let time = unsafeWindow.geofs.animation.values.hours + ':' + unsafeWindow.geofs.animation.values.minutes; let intro = 'You are '+controller.name.first+' '+controller.name.last+', a '+controller.dob.age+' years old '+controller.gender+' ATC controller on the '+apName+' for today. ' + 'Your airport location is (lat: '+airportPosition.lat+', lon: '+airportPosition.lon+'). You are talking to pilot whose name is '+pilot.name+' callsign ('+pilot.callsign+') and they\'ve been piloting since '+pilot.licensed_at+'. ' + 'You will be acting as ground, tower, approach or departure, depending on whether the plane is on the ground, their distance from the airport and previous context. ' + 'If the aircraft is in the air, keep your communication short and concise, as real ATC. If they\'re on the ground, your replies should still be short (1-2 sentence per reply), but you can ' + 'use a more relaxed communication like making jokes, discussing weather, other traffic etc. If asked why so slow on replies, say you\'re busy, like the real ATC. '+ 'You should address them by their aircraft type/model and callsign, rarely by their name. ' + 'Today is '+today+', time is '+time+', a beautiful '+season+' '+daynight; context[airport.code] = []; context[airport.code].push({content: intro, role: 'system'}); } // provide current update let airplane = unsafeWindow.geofs.aircraft.instance.aircraftRecord; let aircraftPosition = { lat: unsafeWindow.geofs.aircraft.instance.lastLlaLocation[0], lon: unsafeWindow.geofs.aircraft.instance.lastLlaLocation[1], }; let onGround = isOnGround() ? 'on the ground' : 'in the air'; let distance; if (airport.distanceInKm > 1) { distance = airport.distanceInKm+' km away from the airport'; } else if (isOnGround()) { distance = 'at the airport'; } else { distance = 'above the airport'; } let movingSpeed; if (isOnGround()) { if (unsafeWindow.geofs.animation.values.kias > 1) { movingSpeed = 'moving at '+unsafeWindow.geofs.animation.values.kias+' kts' } else { movingSpeed = 'stationary'; } } else { movingSpeed = 'flying at '+unsafeWindow.geofs.animation.values.kias+' kts, heading '+unsafeWindow.geofs.animation.values.heading360; } let relativeWindDirection = unsafeWindow.geofs.animation.values.relativeWind; let windDirection = unsafeWindow.geofs.animation.values.heading360 + relativeWindDirection; let wind = unsafeWindow.geofs.animation.values.windSpeedLabel + ', direction '+ windDirection + ' degrees (or '+relativeWindDirection+' degrees relative to the heading of the aircraft).'; let currentUpdate = 'The pilot is flying '+airplane.name+' and their position is (lat: '+aircraftPosition.lat+', lon: '+aircraftPosition.lon+'), '+onGround+' '+distance+'. Based on the airport and ' + 'the aircraft coordinates you can figure out their relative position to the airport. The altitude of the aircraft is '+seaAltitude()+' feet above the sea level ('+groundAltitude()+' feet above ground). ' + 'The plane is '+movingSpeed+'. Wind speed is '+wind+'. Air temperature is '+unsafeWindow.geofs.animation.values.airTemp+' degrees celsius.'; context[airport.code].push({content: currentUpdate, role: 'system'}); context[airport.code].push({content: pilotMsg, role: 'user'}); pilotMessage(pilotMsg); puter.ai.chat(context[airport.code]).then(function(resp) { context[airport.code].push(resp.message); atcMessage(resp.message.content, airport.code); }); } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址