您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
移动端RPG虚拟键。
当前为
// ==UserScript== // @name RPG 虚拟按键 // @namespace https://douglas.example // @version 1.1.0 // @description 移动端RPG虚拟键。 // @author you // @match *://*.oraclecloud.com/* // @run-at document-end // @grant none // ==/UserScript== (function () { 'use strict'; /********************** 键位映射与事件派发 *************************/ function mapCharToKey(char) { return { key: char, code: '', keyCode: char.charCodeAt(0) }; } const SPECIAL = { 'ArrowUp': { key: 'ArrowUp', code: 'ArrowUp', keyCode: 38 }, 'ArrowDown': { key: 'ArrowDown', code: 'ArrowDown', keyCode: 40 }, 'ArrowLeft': { key: 'ArrowLeft', code: 'ArrowLeft', keyCode: 37 }, 'ArrowRight': { key: 'ArrowRight', code: 'ArrowRight', keyCode: 39 }, 'Enter': { key: 'Enter', code: 'Enter', keyCode: 13 }, 'Escape': { key: 'Escape', code: 'Escape', keyCode: 27 }, 'Space': { key: ' ', code: 'Space', keyCode: 32 }, 'Shift': { key: 'Shift', code: 'ShiftLeft', keyCode: 16 }, 'Control': { key: 'Control', code: 'ControlLeft',keyCode: 17 }, 'Z': mapCharToKey('z'), 'X': mapCharToKey('x'), 'C': mapCharToKey('c') }; function makeKeyEvent(type, def) { const init = { key: def.key, code: def.code, bubbles: true, cancelable: true, keyCode: def.keyCode, which: def.keyCode, charCode: 0, repeat: type === 'keydown' ? false : undefined }; const ev = new KeyboardEvent(type, init); try { Object.defineProperty(ev, 'keyCode', { get: () => def.keyCode }); Object.defineProperty(ev, 'which', { get: () => def.keyCode }); } catch (_) {} return ev; } function dispatchKey(type, def) { const canvas = document.querySelector('canvas'); const targets = canvas ? [canvas, document, window] : [document, window]; const ev = makeKeyEvent(type, def); for (const t of targets) { try { t.dispatchEvent(ev); } catch (_) {} } } /********************** 样式 *************************/ const style = document.createElement('style'); style.textContent = ` .vg-toggle { position: fixed; left: 10px; top: 10px; z-index: 2147483646; padding: 6px 10px; background: rgba(0,0,0,.6); color: #fff; border-radius: 8px; font-size: 12px; user-select: none; -webkit-user-select: none; backdrop-filter: saturate(150%) blur(4px); } .vg-toggle:active { transform: scale(0.98); } .vg-panel { position: fixed; left: 0; right: 0; bottom: 0; z-index: 2147483645; width: 100vw; background: rgba(20,20,24,.55); color: #fff; border-radius: 16px 16px 0 0; box-shadow: 0 -8px 24px rgba(0,0,0,.25); font-family: system-ui, -apple-system, "Segoe UI", Roboto, Arial, "PingFang SC","Microsoft Yahei", sans-serif; -webkit-user-select: none; user-select: none; touch-action: none; backdrop-filter: blur(6px); } .vg-header { display: flex; align-items: center; justify-content: space-between; padding: 6px 10px; border-bottom: 1px solid rgba(255,255,255,.08); } .vg-handle { width: 28px; height: 28px; display: inline-flex; align-items: center; justify-content: center; border-radius: 50%; background: rgba(255,255,255,.12); cursor: grab; font-size: 16px; } .vg-locked .vg-handle { display: none; } .vg-actions { display: flex; gap: 8px; } .vg-lock { width: 28px; height: 28px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; background: rgba(255,255,255,.12); font-size: 14px; } .vg-body { display: flex; align-items: stretch; justify-content: space-between; gap: 8px; padding: 8px 10px 10px; } .vg-left, .vg-right { flex: 1 1 0; display: flex; align-items: center; justify-content: center; } .vg-left { justify-content: flex-start; } .vg-right{ justify-content: flex-end; } /* 方向键(左侧) */ .vg-arrows { display: grid; grid-template-columns: repeat(3, 1fr); grid-template-rows: repeat(2, 48px); gap: 6px; } .vg-arrows .spacer { visibility: hidden; } .vg-btn { display: inline-flex; align-items: center; justify-content: center; min-width: 60px; height: 48px; padding: 0 12px; border-radius: 12px; background: rgba(255,255,255,.12); color: #fff; font-weight: 600; font-size: 14px; box-shadow: inset 0 0 0 1px rgba(255,255,255,.08); -webkit-tap-highlight-color: transparent; border: 0; outline: none; pointer-events: auto; } .vg-btn:active { transform: scale(.98); } .vg-pill { border-radius: 999px; } .vg-wide { min-width: 96px; } /* 功能键(右侧) */ .vg-grid { display: grid; gap: 6px; grid-template-columns: repeat(3, minmax(60px, 1fr)); grid-auto-rows: 48px; } /* 小屏优化:纵向堆叠仍保持左右优先 */ @media (max-width: 520px) { .vg-btn { min-width: 48px; height: 44px; font-size: 13px; } .vg-arrows { grid-template-rows: repeat(2, 44px); } .vg-grid { grid-template-columns: repeat(3, minmax(48px, 1fr)); grid-auto-rows: 44px; } } .vg-hidden { display: none !important; } `; document.documentElement.appendChild(style); /********************** 顶部“隐藏/显示”按钮 *************************/ const toggle = document.createElement('button'); toggle.className = 'vg-toggle'; toggle.type = 'button'; toggle.textContent = '隐藏'; toggle.setAttribute('tabindex', '-1'); toggle.addEventListener('mousedown', e => e.preventDefault(), { passive: false }); toggle.addEventListener('touchstart', e => e.preventDefault(), { passive: false }); document.body.appendChild(toggle); /********************** 面板结构 *************************/ const panel = document.createElement('div'); panel.className = 'vg-panel'; // 头部:拖动手柄 + 无文字锁定按钮 const header = document.createElement('div'); header.className = 'vg-header'; const handle = document.createElement('div'); handle.className = 'vg-handle'; handle.setAttribute('title', '拖动面板'); handle.setAttribute('aria-label', '拖动面板'); handle.textContent = '⤒⤓'; // 暗示可上下拖动 const actions = document.createElement('div'); actions.className = 'vg-actions'; const lockBtn = document.createElement('button'); lockBtn.className = 'vg-lock'; lockBtn.type = 'button'; lockBtn.setAttribute('tabindex', '-1'); lockBtn.setAttribute('title', '锁定/解锁'); lockBtn.textContent = '🔓'; actions.appendChild(lockBtn); header.appendChild(handle); header.appendChild(actions); panel.appendChild(header); // 内容:左右分区 const body = document.createElement('div'); body.className = 'vg-body'; // 左:方向键 const left = document.createElement('div'); left.className = 'vg-left'; const arrows = document.createElement('div'); arrows.className = 'vg-arrows'; arrows.innerHTML = ` <div class="spacer"></div> <button class="vg-btn vg-pill" data-key="ArrowUp" tabindex="-1">↑</button> <div class="spacer"></div> <button class="vg-btn vg-pill" data-key="ArrowLeft" tabindex="-1">←</button> <button class="vg-btn vg-pill" data-key="ArrowDown" tabindex="-1">↓</button> <button class="vg-btn vg-pill" data-key="ArrowRight" tabindex="-1">→</button> `; left.appendChild(arrows); // 右:功能键 const right = document.createElement('div'); right.className = 'vg-right'; const grid = document.createElement('div'); grid.className = 'vg-grid'; const keys = [ { label: 'Z', key: 'Z' }, { label: 'X', key: 'X' }, { label: 'C', key: 'C' }, { label: 'Space', key: 'Space', wide: true }, { label: 'Enter', key: 'Enter', wide: true }, { label: 'Esc', key: 'Escape' }, { label: 'Ctrl', key: 'Control' }, { label: 'Shift', key: 'Shift' } ]; for (const k of keys) { const btn = document.createElement('button'); btn.className = 'vg-btn' + (k.wide ? ' vg-wide' : ''); btn.textContent = k.label; btn.dataset.key = k.key; btn.setAttribute('tabindex', '-1'); btn.addEventListener('mousedown', e => e.preventDefault(), { passive: false }); btn.addEventListener('touchstart', e => e.preventDefault(), { passive: false }); grid.appendChild(btn); } right.appendChild(grid); body.appendChild(left); body.appendChild(right); panel.appendChild(body); document.body.appendChild(panel); /********************** 状态保存(Y轴位置/锁定/显示) *************************/ const LS_KEY = `vg-fullwidth-pos-lock-${location.host}`; function saveState() { const rect = panel.getBoundingClientRect(); const locked = panel.classList.contains('vg-locked'); const hidden = panel.classList.contains('vg-hidden'); localStorage.setItem(LS_KEY, JSON.stringify({ top: rect.top, locked, hidden })); } function loadState() { try { const s = JSON.parse(localStorage.getItem(LS_KEY) || '{}'); if (typeof s.top === 'number') { panel.style.top = `${Math.max(0, Math.min(window.innerHeight - 120, s.top))}px`; panel.style.bottom = 'auto'; } if (s.locked) setLocked(true); if (s.hidden) setHidden(true); } catch (_) {} } function setHidden(v) { if (v) { panel.classList.add('vg-hidden'); toggle.textContent = '显示'; } else { panel.classList.remove('vg-hidden'); toggle.textContent = '隐藏'; } } toggle.addEventListener('click', () => { setHidden(!panel.classList.contains('vg-hidden')); saveState(); }); function setLocked(locked) { if (locked) { panel.classList.add('vg-locked'); lockBtn.textContent = '🔒'; // 锁定时隐藏标题(拖动手柄) handle.style.display = 'none'; } else { panel.classList.remove('vg-locked'); lockBtn.textContent = '🔓'; handle.style.display = ''; } } lockBtn.addEventListener('click', () => { setLocked(!panel.classList.contains('vg-locked')); saveState(); }); loadState(); /********************** 拖动(上下拖动) *************************/ (function dragVerticalOnly() { let dragging = false; let startY = 0; let originTop = 0; function onDown(e) { if (panel.classList.contains('vg-locked')) return; dragging = true; const pt = getPoint(e); startY = pt.clientY; originTop = panel.getBoundingClientRect().top; handle.style.cursor = 'grabbing'; e.preventDefault(); } function onMove(e) { if (!dragging) return; const pt = getPoint(e); const dy = pt.clientY - startY; const top = Math.max(0, Math.min(window.innerHeight - 120, originTop + dy)); panel.style.top = `${top}px`; panel.style.bottom = 'auto'; } function onUp() { if (!dragging) return; dragging = false; handle.style.cursor = 'grab'; saveState(); } function getPoint(e) { return (e.touches && e.touches[0]) || e; } handle.addEventListener('mousedown', onDown, { passive: false }); window.addEventListener('mousemove', onMove, { passive: false }); window.addEventListener('mouseup', onUp, { passive: true }); handle.addEventListener('touchstart', onDown, { passive: false }); window.addEventListener('touchmove', onMove, { passive: false }); window.addEventListener('touchend', onUp, { passive: true }); window.addEventListener('touchcancel',onUp, { passive: true }); })(); /********************** 按键绑定 *************************/ const activePresses = new Map(); // id -> {def, timer} function startPress(def, id) { if (activePresses.has(id)) return; dispatchKey('keydown', def); const timer = setInterval(() => dispatchKey('keydown', def), 65); activePresses.set(id, { def, timer }); } function endPress(id) { const rec = activePresses.get(id); if (!rec) return; clearInterval(rec.timer); activePresses.delete(id); dispatchKey('keyup', rec.def); } function defFromDatasetKey(dsKey) { if (SPECIAL[dsKey]) return SPECIAL[dsKey]; if (dsKey === 'Space') return SPECIAL['Space']; return mapCharToKey(dsKey); } function bindButton(el) { const dsKey = el.dataset.key; const def = defFromDatasetKey(dsKey); const id = Symbol(dsKey); const prevent = (e) => { e.preventDefault(); e.stopPropagation(); }; el.addEventListener('mousedown', (e) => { prevent(e); startPress(def, id); }, { passive: false }); el.addEventListener('mouseup', (e) => { prevent(e); endPress(id); }, { passive: false }); el.addEventListener('mouseleave',(e) => { prevent(e); endPress(id); }, { passive: false }); el.addEventListener('touchstart', (e) => { prevent(e); startPress(def, id); }, { passive: false }); el.addEventListener('touchend', (e) => { prevent(e); endPress(id); }, { passive: false }); el.addEventListener('touchcancel',(e) => { prevent(e); endPress(id); }, { passive: false }); el.setAttribute('tabindex', '-1'); } panel.querySelectorAll('.vg-btn').forEach(bindButton); // 页面不可见时防止“卡键” document.addEventListener('visibilitychange', () => { if (document.visibilityState !== 'visible') { for (const [id, rec] of activePresses.entries()) { clearInterval(rec.timer); dispatchKey('keyup', rec.def); } activePresses.clear(); } }); })();