/* inserted by pull.js */ import _twAsset0 from "./active.png"; import _twAsset1 from "./close.svg"; import _twAsset2 from "./cursor.png"; import _twAsset3 from "./dot.svg"; const _twGetAsset = (path) => { if (path === "/active.png") return _twAsset0; if (path === "/close.svg") return _twAsset1; if (path === "/cursor.png") return _twAsset2; if (path === "/dot.svg") return _twAsset3; throw new Error(`Unknown asset: ${path}`); }; import GamepadLib from "./gamepadlib.js"; export default async function (scaffolding, pointerlock) { const vm = scaffolding.vm; // Wait for the project to finish loading. Renderer and scripts will not be fully available until this happens. await new Promise((resolve) => { if (vm.editingTarget) return resolve(); vm.runtime.once("PROJECT_LOADED", resolve); }); const vmStarted = () => vm.runtime._steppingInterval !== null; const scratchKeyToKey = (key) => { switch (key) { case "right arrow": return "ArrowRight"; case "up arrow": return "ArrowUp"; case "left arrow": return "ArrowLeft"; case "down arrow": return "ArrowDown"; case "enter": return "Enter"; case "space": return " "; } return key.toLowerCase().charAt(0); }; const getKeysUsedByProject = () => { const allBlocks = [vm.runtime.getTargetForStage(), ...vm.runtime.targets] .filter((i) => i.isOriginal) .map((i) => i.blocks); const result = new Set(); for (const blocks of allBlocks) { for (const block of Object.values(blocks._blocks)) { if (block.opcode === "event_whenkeypressed" || block.opcode === "event_whenkeyhit" || block.opcode === "sensing_keyoptions") { // For blocks like "key (my variable) pressed?", the sensing_keyoptions still exists but has a null parent. if (block.opcode === "sensing_keyoptions" && !block.parent) { continue; } const key = block.fields.KEY_OPTION.value; result.add(scratchKeyToKey(key)); } } } return result; }; const GAMEPAD_CONFIG_MAGIC = " // _gamepad_"; const findOptionsComment = () => { const target = vm.runtime.getTargetForStage(); const comments = target.comments; for (const comment of Object.values(comments)) { if (comment.text.includes(GAMEPAD_CONFIG_MAGIC)) { return comment; } } return null; }; const parseOptionsComment = () => { const comment = findOptionsComment(); if (!comment) { return null; } const lineWithMagic = comment.text.split("\n").find((i) => i.endsWith(GAMEPAD_CONFIG_MAGIC)); if (!lineWithMagic) { console.warn("Gamepad comment does not contain valid line"); return null; } const jsonText = lineWithMagic.substr(0, lineWithMagic.length - GAMEPAD_CONFIG_MAGIC.length); let parsed; try { parsed = JSON.parse(jsonText); if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.buttons) || !Array.isArray(parsed.axes)) { throw new Error("Invalid data"); } } catch (e) { console.warn("Gamepad comment has invalid JSON", e); return null; } return parsed; }; GamepadLib.setConsole(console); const gamepad = new GamepadLib(); const parsedOptions = parseOptionsComment(); gamepad.getUserHints = () => { if (parsedOptions) { return { importedSettings: parsedOptions, }; } return { usedKeys: getKeysUsedByProject(), }; }; const renderer = vm.runtime.renderer; const width = renderer._xRight - renderer._xLeft; const height = renderer._yTop - renderer._yBottom; const canvas = renderer.canvas; const virtualCursorElement = document.createElement("img"); virtualCursorElement.hidden = true; virtualCursorElement.className = "sa-gamepad-cursor"; virtualCursorElement.src = _twGetAsset("/cursor.png"); let hideCursorTimeout; const hideRealCursor = () => { document.body.classList.add("sa-gamepad-hide-cursor"); }; const showRealCursor = () => { document.body.classList.remove("sa-gamepad-hide-cursor"); }; const virtualCursorSetVisible = (visible) => { virtualCursorElement.hidden = !visible; clearTimeout(hideCursorTimeout); if (visible) { hideRealCursor(); hideCursorTimeout = setTimeout(virtualCursorHide, 8000); } }; const virtualCursorHide = () => { virtualCursorSetVisible(false); }; const virtualCursorSetDown = (down) => { virtualCursorSetVisible(true); virtualCursorElement.classList.toggle("sa-gamepad-cursor-down", down); }; const virtualCursorSetPosition = (x, y) => { virtualCursorSetVisible(true); const CURSOR_SIZE = 6; const stageX = width / 2 + x - CURSOR_SIZE / 2; const stageY = height / 2 - y - CURSOR_SIZE / 2; virtualCursorElement.style.transform = `translate(${stageX}px, ${stageY}px)`; }; document.addEventListener("mousemove", () => { virtualCursorSetVisible(false); showRealCursor(); }); let getCanvasSize; // Support modern ResizeObserver and slow getBoundingClientRect version for improved browser support (matters for TurboWarp) if (window.ResizeObserver) { let canvasWidth = width; let canvasHeight = height; const resizeObserver = new ResizeObserver((entries) => { for (const entry of entries) { canvasWidth = entry.contentRect.width; canvasHeight = entry.contentRect.height; } }); resizeObserver.observe(canvas); getCanvasSize = () => [canvasWidth, canvasHeight]; } else { getCanvasSize = () => { const rect = canvas.getBoundingClientRect(); return [rect.width, rect.height]; }; } // Both in Scratch space let virtualX = 0; let virtualY = 0; const postMouseData = (data) => { if (!vmStarted()) return; const [rectWidth, rectHeight] = getCanvasSize(); vm.postIOData("mouse", { ...data, canvasWidth: rectWidth, canvasHeight: rectHeight, x: (virtualX + width / 2) * (rectWidth / width), y: (height / 2 - virtualY) * (rectHeight / height), }); }; const postKeyboardData = (key, isDown) => { if (!vmStarted()) return; vm.postIOData("keyboard", { key, isDown, }); }; const handleGamepadButtonDown = (e) => postKeyboardData(e.detail, true); const handleGamepadButtonUp = (e) => postKeyboardData(e.detail, false); const handleGamepadMouseDown = (e) => { virtualCursorSetDown(true); postMouseData({ isDown: true, button: e.detail, }); }; const handleGamepadMouseUp = (e) => { virtualCursorSetDown(false); postMouseData({ isDown: false, button: e.detail, }); }; const handleGamepadMouseMove = (e) => { const {x, y} = e.detail; if (pointerlock) { const deltaX = x - virtualX; const deltaY = -(y - virtualY); virtualX = x; virtualY = y; // Coordinates that pointerlock accepts are in "screen space" but virtual cursor is in "stage space" const SPEED_MULTIPLIER = 4.0; const zoomMultiplierX = scaffolding.layersRect.width / vm.runtime.stageWidth; const zoomMultiplierY = scaffolding.layersRect.height / vm.runtime.stageHeight; // This is defined in pointerlock addon vm.pointerLockMove( deltaX * SPEED_MULTIPLIER * zoomMultiplierX, deltaY * SPEED_MULTIPLIER * zoomMultiplierY ); } else { virtualX = x; virtualY = y; virtualCursorSetPosition(virtualX, virtualY); postMouseData({}); } }; if (!pointerlock) { gamepad.virtualCursor.maxX = renderer._xRight; gamepad.virtualCursor.minX = renderer._xLeft; gamepad.virtualCursor.maxY = renderer._yTop; gamepad.virtualCursor.minY = renderer._yBottom; } gamepad.addEventListener("keydown", handleGamepadButtonDown); gamepad.addEventListener("keyup", handleGamepadButtonUp); gamepad.addEventListener("mousedown", handleGamepadMouseDown); gamepad.addEventListener("mouseup", handleGamepadMouseUp); gamepad.addEventListener("mousemove", handleGamepadMouseMove); if (!pointerlock) { scaffolding._overlays.appendChild(virtualCursorElement); } }