您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Mod Menu for GeoFS flight model variables using console-modifiable input fields
当前为
// ==UserScript== // @name GeoFS Mod Menu -cool- // @namespace http://tampermonkey.net/ // @version 1.6 // @description Mod Menu for GeoFS flight model variables using console-modifiable input fields // @author Jasp // @match https://www.geo-fs.com/* // @grant none // @license MIT // ==/UserScript== (function () { 'use strict'; /* =========================== Data / Variable Definitions =========================== */ // Variables that will be editable (order & names shown in UI) const allVars = [ "maxRPM","minRPM","starterRPM","idleThrottle","fuelFlow","enginePower","brakeRPM", "wingArea","dragFactor","liftFactor","CD0","CLmax","elevatorFactor","rudderFactor","aileronFactor", "mass","emptyWeight","maxWeight","inertia","pitchMoment","yawMoment","rollMoment", "gearDrag","gearCompression","gearLength" ]; const explanations = { maxRPM: "Maximum revolutions per minute of the engine.", minRPM: "Minimum engine RPM (idle lower bound).", starterRPM: "RPM used when starting the engine.", idleThrottle: "Throttle percentage at idle.", fuelFlow: "Fuel flow rate scaling.", enginePower: "Base engine power factor.", brakeRPM: "RPM threshold for brakes.", wingArea: "Wing area used for lift calculations.", dragFactor: "General drag multiplier.", liftFactor: "Lift multiplier applied to wing calculations.", CD0: "Parasitic drag coefficient.", CLmax: "Maximum lift coefficient before stall.", elevatorFactor: "Elevator control effectiveness.", rudderFactor: "Rudder control effectiveness.", aileronFactor: "Aileron control effectiveness.", mass: "Aircraft mass.", emptyWeight: "Empty weight of aircraft.", maxWeight: "Maximum allowable weight.", inertia: "Rotational inertia parameter.", pitchMoment: "Pitch moment (stability).", yawMoment: "Yaw moment (stability).", rollMoment: "Roll moment (stability).", gearDrag: "Drag contribution from gear.", gearCompression: "Suspension stiffness / compression.", gearLength: "Landing gear strut length." }; /* =========================== Theme definitions & helpers =========================== */ const PALETTE_DARK = { bg: "#0f1720", panel: "#111827", accent: "#2b9af3", text: "#e6eef8", muted: "#9aa8b6", glass: "rgba(255,255,255,0.03)" }; const PALETTE_LIGHT = { bg: "#f6fbff", panel: "#e9f2fb", accent: "#1e6fb8", text: "#07263a", muted: "#3f6b83", glass: "rgba(4,20,31,0.03)" }; function hexToRgb(hex) { const h = hex.replace("#", ""); if (h.length === 3) return [parseInt(h[0]+h[0],16), parseInt(h[1]+h[1],16), parseInt(h[2]+h[2],16)]; const bigint = parseInt(h,16); return [(bigint>>16)&255, (bigint>>8)&255, bigint&255]; } function rgbToHex(rgb){ return "#" + rgb.map(v=>{const s=Math.round(v).toString(16); return s.length===1?"0"+s:s;}).join(""); } function lerpColor(a,b,t){ const A=hexToRgb(a), B=hexToRgb(b); return rgbToHex([A[0]+(B[0]-A[0])*t, A[1]+(B[1]-A[1])*t, A[2]+(B[2]-A[2])*t]); } function lerpGlass(gA,gB,t){ const parseAlpha = s => { const m = s.match(/rgba\([^,]+,[^,]+,[^,]+,([^)]+)\)/); return m?parseFloat(m[1]):0.03; }; const a=parseAlpha(gA), b=parseAlpha(gB), alpha=a+(b-a)*t; return `rgba(0,0,0,${alpha})`; } function applyThemeToDocument(doc, t){ const bg = lerpColor(PALETTE_DARK.bg, PALETTE_LIGHT.bg, t); const panel = lerpColor(PALETTE_DARK.panel, PALETTE_LIGHT.panel, t); const accent = lerpColor(PALETTE_DARK.accent, PALETTE_LIGHT.accent, t); const text = lerpColor(PALETTE_DARK.text, PALETTE_LIGHT.text, t); const muted = lerpColor(PALETTE_DARK.muted, PALETTE_LIGHT.muted, t); const glass = lerpGlass(PALETTE_DARK.glass, PALETTE_LIGHT.glass, t); const root = doc.documentElement; root.style.setProperty('--mod-bg', bg); root.style.setProperty('--mod-panel', panel); root.style.setProperty('--mod-accent', accent); root.style.setProperty('--mod-text', text); root.style.setProperty('--mod-muted', muted); root.style.setProperty('--mod-glass', glass); (doc.querySelectorAll?.('.theme-range')||[]).forEach(el=>{ try{ el.style.accentColor=accent; }catch(e){} }); } /* =========================== Injection: single-document UI builder =========================== */ const injectedDocs = new WeakSet(); function injectIntoWindow(targetWin) { if (!targetWin || !targetWin.document) return; const doc = targetWin.document; if (injectedDocs.has(doc)) return; injectedDocs.add(doc); // CSS (uses CSS vars) const css = ` :root{ --mod-bg:${PALETTE_DARK.bg}; --mod-panel:${PALETTE_DARK.panel}; --mod-accent:${PALETTE_DARK.accent}; --mod-text:${PALETTE_DARK.text}; --mod-muted:${PALETTE_DARK.muted}; --mod-glass:${PALETTE_DARK.glass}; } #geofsModMenu{ position:fixed; top:80px; right:18px; width:360px; max-height:86vh; overflow-y:auto; color:var(--mod-text); border-radius:14px; padding:12px; box-shadow:0 10px 40px rgba(2,6,23,0.6); backdrop-filter:blur(8px); z-index:2147483000; border:1px solid rgba(255,255,255,0.04); display:none; font-family:"Segoe UI",Roboto,Arial; font-size:13px; background:linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01)); } #geofsModMenu .panel{ background:linear-gradient(180deg,var(--mod-panel),rgba(0,0,0,0.015)); border-radius:10px; padding:10px; box-shadow:inset 0 1px 0 rgba(255,255,255,0.02); } header{ display:flex; align-items:center; gap:10px; margin-bottom:8px; } #geofsModMenu h1{ font-size:15px; margin:0; color:var(--mod-text); font-weight:600; } #themeControl{ margin-left:auto; display:flex; align-items:center; gap:8px; } .theme-label{ font-size:12px; color:var(--mod-muted); width:36px; text-align:center; } #themeSlider{ width:140px; appearance:none; height:8px; border-radius:999px; background:linear-gradient(90deg,var(--mod-accent),#00b4ff 50%,#9ad6ff); outline:none; box-shadow:inset 0 -1px 0 rgba(0,0,0,0.2); } #themeSlider::-webkit-slider-thumb{ -webkit-appearance:none; width:18px; height:18px; border-radius:50%; background:#fff; box-shadow:0 2px 6px rgba(0,0,0,0.35); border:2px solid rgba(0,0,0,0.2); cursor:pointer; } #presetButtons{ display:flex; flex-wrap:wrap; gap:8px; margin-bottom:10px; } .action-button{ background:linear-gradient(180deg,rgba(255,255,255,0.03),rgba(0,0,0,0.04)); border:1px solid rgba(255,255,255,0.03); color:var(--mod-text); padding:6px 10px; border-radius:8px; cursor:pointer; font-weight:600; font-size:13px; } .action-button.primary{ background:linear-gradient(180deg,rgba(255,255,255,0.02),rgba(0,0,0,0.06)); border:1px solid rgba(255,255,255,0.04); } h2.section-title{ margin:12px 0 6px; font-size:13px; color:var(--mod-accent); text-transform:uppercase; letter-spacing:0.6px; } .var-row{ display:flex; flex-direction:column; gap:6px; margin-bottom:8px; } .label-row{ display:flex; justify-content:space-between; align-items:center; gap:12px; } label.name{ font-weight:600; color:var(--mod-text); font-size:13px; } .explain{ font-size:12px; color:var(--mod-muted); margin-top:-4px; } input[type="number"].num-input{ width:140px; background:transparent; border:1px solid rgba(255,255,255,0.04); color:var(--mod-text); padding:6px 8px; border-radius:8px; text-align:right; } #menuToggleBtn{ position:fixed; top:18px; right:18px; width:46px; height:46px; border-radius:50%; display:flex; align-items:center; justify-content:center; background:var(--mod-panel); color:var(--mod-text); border:1px solid rgba(255,255,255,0.03); box-shadow:0 6px 20px rgba(2,6,23,0.5); z-index:2147483001; cursor:pointer; } #helpOverlay{ position:fixed; top:0; left:0; width:100%; height:100%; background: rgba(2,6,23,0.9); color:#eaf6ff; padding:28px; z-index:2147483002; display:none; overflow-y:auto; font-size:14px; } #closeHelp{ position:absolute; top:18px; right:24px; font-size:22px; cursor:pointer; } @media (max-width:520px){ #geofsModMenu{ width:92%; left:4%; right:4%; top:80px; } } `; const styleEl = doc.createElement('style'); styleEl.textContent = css; doc.head.appendChild(styleEl); // Create elements const menu = doc.createElement('div'); menu.id = 'geofsModMenu'; menu.className = 'panel'; const toggleBtn = doc.createElement('div'); toggleBtn.id = 'menuToggleBtn'; toggleBtn.title = 'Toggle GeoFS Mod Menu'; toggleBtn.textContent = '⚙️'; const helpOverlay = doc.createElement('div'); helpOverlay.id = 'helpOverlay'; helpOverlay.innerHTML = `<div id="closeHelp">✕</div><h1>GeoFS Mod Menu — Help</h1><div id="helpContent"></div>`; // Header with theme slider const header = doc.createElement('header'); header.innerHTML = `<h1>GeoFS Mod Menu</h1>`; const themeControl = doc.createElement('div'); themeControl.id = 'themeControl'; themeControl.innerHTML = `<div class="theme-label">Dark</div><input id="themeSlider" class="theme-range" type="range" min="0" max="100" value="0" /><div class="theme-label">Light</div>`; header.appendChild(themeControl); // Action buttons (no presets) const actionRow = doc.createElement('div'); actionRow.id = 'presetButtons'; // Reset button const resetBtn = doc.createElement('button'); resetBtn.className = 'action-button'; resetBtn.textContent = 'Reset'; actionRow.appendChild(resetBtn); // Help button const helpBtn = doc.createElement('button'); helpBtn.className = 'action-button primary'; helpBtn.textContent = 'Help'; actionRow.appendChild(helpBtn); const bodyWrap = doc.createElement('div'); bodyWrap.className = 'panel'; menu.appendChild(header); menu.appendChild(actionRow); menu.appendChild(bodyWrap); // Append to document try { doc.body.appendChild(menu); doc.body.appendChild(toggleBtn); doc.body.appendChild(helpOverlay); } catch (e) { console.error('[ModMenu] append failed', e); return; } // Fill help content const helpContent = helpOverlay.querySelector('#helpContent'); helpContent.innerHTML = '<p>Type any numeric value into the boxes. Each box accepts up to 9 digits (digits only; decimal point and minus sign are allowed). Use Reset to set all values to 0.</p>'; for (const [k,v] of Object.entries(explanations)) { const p = doc.createElement('p'); p.innerHTML = `<strong>${k}</strong>: ${v}`; helpContent.appendChild(p); } helpOverlay.querySelector('#closeHelp').addEventListener('click', ()=>{ helpOverlay.style.display='none'; menu.style.display='block'; }); helpBtn.addEventListener('click', ()=>{ menu.style.display='none'; helpOverlay.style.display='block'; }); // Map to hold inputs in this doc const inputsMap = {}; // Input digit cap: 9 digits (excluding '.' and '-') function enforceNineDigitCap(str) { // remove all characters except digits, '.', '-' // do not allow 'e' / exponential notation let cleaned = str.replace(/[^\d\.\-]/g, ''); // prevent multiple minus signs; minus only at start const minusMatches = cleaned.match(/-/g); if (minusMatches && minusMatches.length > 1) { cleaned = cleaned.replace(/-/g, ''); cleaned = '-' + cleaned; } if (cleaned.indexOf('-') > 0) cleaned = cleaned.replace('-', ''); // prevent multiple dots const parts = cleaned.split('.'); if (parts.length > 2) { cleaned = parts.shift() + '.' + parts.join(''); } // count digits (exclude dot and minus) const digitsOnly = cleaned.replace(/[^0-9]/g, ''); if (digitsOnly.length > 9) { // truncate to first 9 digits while preserving decimal & minus placement as best effort const allowedDigits = digitsOnly.slice(0,9); // rebuild: if there was a dot, try to keep fractional digits after truncation if (cleaned.includes('.')) { const [intPart, fracPart=''] = cleaned.split('.'); const intDigits = intPart.replace(/[^0-9\-]/g,'').replace('-',''); // take as many int digits as possible (up to allowedDigits length) const keepInt = Math.min(intDigits.length, allowedDigits.length); const newInt = intDigits.slice(0, keepInt); const newFrac = allowedDigits.slice(keepInt); cleaned = (cleaned.startsWith('-')?'-':'') + (newInt || '0') + (newFrac?'.'+newFrac:''); } else { // no dot: integer-only — keep first 9 digits (respect minus) const wasNegative = cleaned.startsWith('-'); const d = allowedDigits; cleaned = (wasNegative?'-':'') + d; } } return cleaned; } function createNumberControl(varName) { const container = doc.createElement('div'); container.className = 'var-row'; const labelRow = doc.createElement('div'); labelRow.className='label-row'; const label = doc.createElement('label'); label.className='name'; label.textContent = varName; const input = doc.createElement('input'); input.type='number'; input.className='num-input'; input.setAttribute('inputmode','decimal'); input.placeholder = "0"; // allow any numeric value but cap digits to 9 input.addEventListener('input', (e)=>{ const original = input.value; const enforced = enforceNineDigitCap(original); if (enforced !== original) { // set value preserving cursor where reasonable input.value = enforced; } // try to update model on any numeric parse const parsed = parseFloat(input.value); if (!Number.isNaN(parsed)) updateModelInDoc(doc, varName, parsed); }, {passive:true}); // also sanitize on paste input.addEventListener('paste', (ev)=>{ ev.preventDefault(); const text = (ev.clipboardData || window.clipboardData).getData('text') || ''; const enforced = enforceNineDigitCap(text); input.value = enforced; const parsed = parseFloat(enforced); if (!Number.isNaN(parsed)) updateModelInDoc(doc, varName, parsed); }); // on blur, ensure value is valid number (or set 0) input.addEventListener('blur', ()=>{ if (input.value === '' || input.value === '-' || input.value === '.' || input.value === '-.') { input.value = '0'; updateModelInDoc(doc, varName, 0); } else { const parsed = parseFloat(input.value); if (Number.isNaN(parsed)) { input.value = '0'; updateModelInDoc(doc, varName, 0); } else updateModelInDoc(doc, varName, parsed); } }); labelRow.appendChild(label); labelRow.appendChild(input); container.appendChild(labelRow); const explain = doc.createElement('div'); explain.className='explain'; explain.textContent = explanations[varName] || ''; container.appendChild(explain); inputsMap[varName] = { input, container }; return container; } // Build UI groups (grouped for readability) const groups = { Engine: ["maxRPM","minRPM","starterRPM","idleThrottle","fuelFlow","enginePower","brakeRPM"], Aerodynamics: ["wingArea","dragFactor","liftFactor","CD0","CLmax","elevatorFactor","rudderFactor","aileronFactor"], "Flight Model": ["mass","emptyWeight","maxWeight","inertia","pitchMoment","yawMoment","rollMoment"], "Landing Gear": ["gearDrag","gearCompression","gearLength"] }; for (const [groupName, vars] of Object.entries(groups)) { const h = doc.createElement('h2'); h.className='section-title'; h.textContent = groupName; bodyWrap.appendChild(h); for (const v of vars) { if (!allVars.includes(v)) continue; const ctrl = createNumberControl(v); bodyWrap.appendChild(ctrl); } } // Reset button behavior: set all to 0 and update model resetBtn.addEventListener('click', ()=>{ for (const [k,entry] of Object.entries(inputsMap)) { entry.input.value = '0'; updateModelInDoc(doc, k, 0); } }); // Model update helper (works in this doc's window) function updateModelInDoc(docRef, variable, value) { const win = docRef.defaultView || window; try { const def = win.geofs?.aircraft?.instance?.definition || win.geofs?.aircraft?.definition; if (def && variable in def) { def[variable] = value; } else { // fallback attempt (safe) if (win.geofs && win.geofs.aircraft && win.geofs.aircraft.instance && win.geofs.aircraft.instance.definition) { win.geofs.aircraft.instance.definition[variable] = value; } } } catch (err) { // ignore: update may fail if geofs internal structure differs } } // Theme slider logic (smooth interpolation) const themeSlider = doc.getElementById('themeSlider'); let themeT = parseFloat(themeSlider.value)/100; applyThemeToDocument(doc, themeT); let themeAnim = null; function animateThemeToDoc(docRef, targetT, duration=320) { const startT = themeT, delta = targetT - startT, startTime = performance.now(); if (themeAnim) cancelAnimationFrame(themeAnim); function step(now) { const elapsed = now - startTime; const p = Math.min(1, elapsed/duration); const eased = p<0.5?2*p*p:-1+(4-2*p)*p; themeT = startT + delta*eased; applyThemeToDocument(docRef, themeT); if (p < 1) themeAnim = requestAnimationFrame(step); else themeAnim = null; } themeAnim = requestAnimationFrame(step); } themeSlider.addEventListener('input', (e)=> { const v = parseInt(e.target.value,10)/100; animateThemeToDoc(doc, v, 320); }); // Toggle button behavior toggleBtn.addEventListener('click', ()=> { const open = menu.style.display === 'block'; menu.style.display = open? 'none':'block'; if (!open) toggleBtn.style.boxShadow = '0 10px 26px rgba(2,6,23,0.6)'; else toggleBtn.style.boxShadow = '0 6px 20px rgba(2,6,23,0.5)'; }); // Keybinding inside this doc doc.addEventListener('keydown', (e)=> { if (e.key === '#') toggleBtn.click(); }); // initialize all inputs to 0 (so entries exist) for (const [k,entry] of Object.entries(inputsMap)) { entry.input.value = '0'; updateModelInDoc(doc,k,0); } console.log('[ModMenu] injected numeric-only overlay into', doc.location && doc.location.href ? doc.location.href : 'unknown'); } // end injectIntoWindow /* =========================== Inject into main document and any iframes (observe changes) =========================== */ function attemptInjectionAll() { try { injectIntoWindow(window); } catch(e){/*ignore*/} const iframes = Array.from(document.getElementsByTagName('iframe')); for (const iframe of iframes) { try { const src = iframe.getAttribute('src') || ''; // heuristics: likely sim iframe if (src.includes('geofs.php') || src.includes('geo-fs') || (iframe.id && /geofs|map|game/i.test(iframe.id))) { try { if (iframe.contentWindow && iframe.contentWindow.document) injectIntoWindow(iframe.contentWindow); } catch(err){} } } catch(err){} } } attemptInjectionAll(); // Watch for new iframes / attribute changes (when you click Fly) const observer = new MutationObserver(muts => { for (const m of muts) { if (m.type === 'childList' && m.addedNodes && m.addedNodes.length) { for (const n of m.addedNodes) { if (n.tagName && n.tagName.toLowerCase() === 'iframe') { const iframe = n; setTimeout(()=> { try { if (iframe.contentWindow && iframe.contentWindow.document) injectIntoWindow(iframe.contentWindow); } catch(e){} }, 300); } } } if (m.type === 'attributes' && m.target && m.target.tagName && m.target.tagName.toLowerCase()==='iframe') { const iframe = m.target; setTimeout(()=> { try { if (iframe.contentWindow && iframe.contentWindow.document) injectIntoWindow(iframe.contentWindow); } catch(e){} }, 300); } } }); observer.observe(document.documentElement || document.body, { childList:true, subtree:true, attributes:true, attributeFilter:['src'] }); // Poll occasionally as a fallback const poll = setInterval(()=> { attemptInjectionAll(); }, 1200); setTimeout(()=> clearInterval(poll), 90000); // stop after 90s console.log('[ModMenu] numeric-only overlay injector running. Click the cog in the top-right of the flight view to open the panel.'); })();