UNPKG

pxt-arcade

Version:
1,212 lines 243 kB
var pxsim; (function (pxsim) { // Keep in sync with pxt-common-packages/libs/game/keymap.ts let Key; (function (Key) { Key[Key["None"] = 0] = "None"; // Player 1 Key[Key["Left"] = 1] = "Left"; Key[Key["Up"] = 2] = "Up"; Key[Key["Right"] = 3] = "Right"; Key[Key["Down"] = 4] = "Down"; Key[Key["A"] = 5] = "A"; Key[Key["B"] = 6] = "B"; Key[Key["Menu"] = 7] = "Menu"; // Player 2 = Player 1 + 7 // Player 3 = Player 2 + 7 // Player 4 = Player 3 + 7 // system keys Key[Key["Screenshot"] = -1] = "Screenshot"; Key[Key["Gif"] = -2] = "Gif"; Key[Key["Reset"] = -3] = "Reset"; Key[Key["TogglePause"] = -4] = "TogglePause"; })(Key = pxsim.Key || (pxsim.Key = {})); // Map of MouseEvent.button to keymap.KeyCode // Values are from the `KeyCode` enum in pxt-common-packages/libs/game/keymap.ts pxsim.MouseButtonToKeyCode = { 0: -1, 1: -3, 2: -2, // Right button }; function pauseAsync(ms) { return pxsim.U.delay(ms); } pxsim.pauseAsync = pauseAsync; })(pxsim || (pxsim = {})); (function (pxsim) { var game; (function (game) { function takeScreenshot() { const b = pxsim.board(); b.tryScreenshot(); } game.takeScreenshot = takeScreenshot; })(game = pxsim.game || (pxsim.game = {})); })(pxsim || (pxsim = {})); (function (pxsim) { var control; (function (control) { function programList() { let m = pxsim.Array_.mk(); m.push("flappy_duck"); m.push("chase_the_pizza"); m.push("happy_flower"); for (let i = 0; i < 10; ++i) { m.push(`game_${i}`); } return m; } control.programList = programList; function runProgram(name) { // TODO console.log(`run ${name}`); } control.runProgram = runProgram; })(control = pxsim.control || (pxsim.control = {})); })(pxsim || (pxsim = {})); var DAL; (function (DAL) { DAL.DEVICE_PIN_EVENT_NONE = 0; DAL.DEVICE_PIN_EVENT_ON_EDGE = 1; DAL.DEVICE_PIN_EVENT_ON_PULSE = 2; DAL.DEVICE_PIN_EVENT_ON_TOUCH = 3; DAL.DEVICE_PIN_EVT_RISE = 2; DAL.DEVICE_PIN_EVT_FALL = 3; DAL.DEVICE_PIN_EVT_PULSE_HI = 4; DAL.DEVICE_PIN_EVT_PULSE_LO = 5; DAL.DEVICE_ID_IO_P0 = 100; DAL.ACCELEROMETER_IMU_DATA_VALID = 2, DAL.ACCELEROMETER_EVT_DATA_UPDATE = 1, DAL.ACCELEROMETER_EVT_NONE = 0, DAL.ACCELEROMETER_EVT_TILT_UP = 1, DAL.ACCELEROMETER_EVT_TILT_DOWN = 2, DAL.ACCELEROMETER_EVT_TILT_LEFT = 3, DAL.ACCELEROMETER_EVT_TILT_RIGHT = 4, DAL.ACCELEROMETER_EVT_FACE_UP = 5, DAL.ACCELEROMETER_EVT_FACE_DOWN = 6, DAL.ACCELEROMETER_EVT_FREEFALL = 7, DAL.ACCELEROMETER_EVT_3G = 8, DAL.ACCELEROMETER_EVT_6G = 9, DAL.ACCELEROMETER_EVT_8G = 10, DAL.ACCELEROMETER_EVT_SHAKE = 11, DAL.ACCELEROMETER_REST_TOLERANCE = 200, DAL.ACCELEROMETER_TILT_TOLERANCE = 200, DAL.ACCELEROMETER_FREEFALL_TOLERANCE = 400, DAL.ACCELEROMETER_SHAKE_TOLERANCE = 400, DAL.ACCELEROMETER_3G_TOLERANCE = 3072, DAL.ACCELEROMETER_6G_TOLERANCE = 6144, DAL.ACCELEROMETER_8G_TOLERANCE = 8192, DAL.ACCELEROMETER_GESTURE_DAMPING = 5, DAL.ACCELEROMETER_SHAKE_DAMPING = 10, DAL.ACCELEROMETER_SHAKE_RTX = 30, DAL.ACCELEROMETER_SHAKE_COUNT_THRESHOLD = 4, DAL.DEVICE_ID_GESTURE = 13, DAL.DEVICE_ID_ACCELEROMETER = 5, DAL.SENSOR_THRESHOLD_LOW = 1, DAL.SENSOR_THRESHOLD_HIGH = 2, DAL.LEVEL_THRESHOLD_LOW = 1, DAL.LEVEL_THRESHOLD_HIGH = 2, DAL.DEVICE_ID_THERMOMETER = 8, DAL.DEVICE_ID_LIGHT_SENSOR = 17, DAL.DEVICE_ID_RADIO = 9, DAL.DEVICE_RADIO_EVT_DATAGRAM = 1, DAL.DEVICE_ID_MICROPHONE = 3001; })(DAL || (DAL = {})); var pxsim; (function (pxsim) { let PlayerNumber; (function (PlayerNumber) { PlayerNumber[PlayerNumber["One"] = 1] = "One"; PlayerNumber[PlayerNumber["Two"] = 2] = "Two"; PlayerNumber[PlayerNumber["Three"] = 3] = "Three"; PlayerNumber[PlayerNumber["Four"] = 4] = "Four"; })(PlayerNumber || (PlayerNumber = {})); let init = false; let connected = false; let all = {}; let player = PlayerNumber.One; function initGamepad() { if (init) return; init = true; window.addEventListener("gamepadconnected", (e) => { if (connected) return; connected = true; setInterval(() => { onUpdate(); }, 20); }); } pxsim.initGamepad = initGamepad; function onUpdate() { const g = navigator.getGamepads(); if (g) { for (let i = 0; i < g.length; i++) { const gamepad = g[i]; if (gamepad && gamepad.buttons && gamepad.buttons.length) { const ctrl = getState(gamepad); updateState(ctrl, pxsim.Key.A, 0, gamepad); updateState(ctrl, pxsim.Key.B, 1, gamepad); updateState(ctrl, pxsim.Key.Menu, 9, gamepad); updateState(ctrl, pxsim.Key.Up, 12, gamepad, 1, false); updateState(ctrl, pxsim.Key.Down, 13, gamepad, 1, true); updateState(ctrl, pxsim.Key.Left, 14, gamepad, 0, false); updateState(ctrl, pxsim.Key.Right, 15, gamepad, 0, true); if (ctrl.player == 1) { //Support reset for first player only updateState(ctrl, pxsim.Key.Reset, 10, gamepad); } } } } } pxsim.onUpdate = onUpdate; function getState(gamepad) { if (all[gamepad.index]) return all[gamepad.index]; const newState = { state: {}, player }; all[gamepad.index] = newState; player++; return newState; } function updateState(ctrl, key, buttonIndex, gamepad, axis, axisPositive) { let btn = gamepad.buttons[buttonIndex]; let pressed = btn && btn.pressed; if (axis != undefined && gamepad.axes && gamepad.axes[axis]) { const value = gamepad.axes[axis]; if (Math.abs(value) > 0.5) { pressed = pressed || (axisPositive === value > 0); } } const old = ctrl.state[key] || false; if (old != pressed) { pxsim.Keyboard.cancelSystemKeyboard(); ctrl.state[key] = pressed; pxsim.board().setButton(key + (7 * (ctrl.player - 1)), pressed); } } })(pxsim || (pxsim = {})); var pxsim; (function (pxsim) { const icon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Pro 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) --><path d="M215.03 71.05L126.06 160H24c-13.26 0-24 10.74-24 24v144c0 13.25 10.74 24 24 24h102.06l88.97 88.95c15.03 15.03 40.97 4.47 40.97-16.97V88.02c0-21.46-25.96-31.98-40.97-16.97zM461.64 256l45.64-45.64c6.3-6.3 6.3-16.52 0-22.82l-22.82-22.82c-6.3-6.3-16.52-6.3-22.82 0L416 210.36l-45.64-45.64c-6.3-6.3-16.52-6.3-22.82 0l-22.82 22.82c-6.3 6.3-6.3 16.52 0 22.82L370.36 256l-45.63 45.63c-6.3 6.3-6.3 16.52 0 22.82l22.82 22.82c6.3 6.3 16.52 6.3 22.82 0L416 301.64l45.64 45.64c6.3 6.3 16.52 6.3 22.82 0l22.82-22.82c6.3-6.3 6.3-16.52 0-22.82L461.64 256z"/></svg>`; // We only need to unmute from within the iframe once let hasUnmuted = false; function createMuteButton() { const el = document.createElement("div"); el.setAttribute("id", "safari-mute-button-outer"); el.innerHTML = ` <button class="safari-mute-button"> ${icon} </button> `; const button = el.firstElementChild; button.setAttribute("title", pxsim.localization.lf("Unmute simulator")); button.addEventListener("click", () => { pxsim.AudioContextManager.mute(false); pxsim.setParentMuteState("unmuted"); button.remove(); hasUnmuted = true; }); return el; } pxsim.createMuteButton = createMuteButton; function shouldShowMute() { return isSafari() && !hasUnmuted; } pxsim.shouldShowMute = shouldShowMute; })(pxsim || (pxsim = {})); var pxsim; (function (pxsim) { let forcedUpdateLoop; let isFirstRunSafari = true; let themeFromQueryParameter = false; pxsim.simButtonsHidden = false; window.addEventListener("DOMContentLoaded", () => { const searchParams = new URL(window.location.toString()).searchParams; const setThemeIfDefined = (themeType) => { const paramVal = searchParams.get(themeType); if (paramVal) { themeFromQueryParameter = true; pxsim.theme.setSimThemeColor(themeType, paramVal); } }; setThemeIfDefined("background-color"); setThemeIfDefined("button-stroke"); setThemeIfDefined("text-color"); setThemeIfDefined("button-fill"); setThemeIfDefined("dpad-fill"); const skin = searchParams.get("skin"); if (skin) { themeFromQueryParameter = true; pxsim.theme.applySkin(skin); } registerPointerEvents(!!searchParams.get("pointer-events")); pxsim.simButtonsHidden = !!searchParams.get("hideSimButtons"); if (pxsim.simButtonsHidden) hideSimButtons(); if (!!searchParams.get("noExtraPadding")) noExtraPadding(); }); if (hasNavigator()) { // only XBOX webview looks at this, to get rid of cursor navigator.gamepadInputEmulation = "gamepad"; } function hideSimButtons() { const gamePlayer = document.getElementsByClassName("game-player"); if (gamePlayer && gamePlayer.length) { gamePlayer[0].classList.add("just-screen"); } } function noExtraPadding() { const gamePlayer = document.getElementsByClassName("game-player"); if (gamePlayer && gamePlayer.length) { gamePlayer[0].classList.add("no-padding"); } } function registerPointerEvents(sendMessages) { const canvas = document.getElementById("game-screen"); const encoder = new TextEncoder(); const events = [ "pointerdown", "pointerup", "pointermove", "pointerleave", "pointerenter", "pointercancel", "pointerover", "pointerout", ]; const sendMsg = (msg) => { const data = encoder.encode(JSON.stringify(msg)); const board = pxsim.board(); const state = board && board.controlMessageState; // queue in control sims state && state.enqueue({ type: "messagepacket", channel: "pointer-events", broadcast: false, data, }); }; const reporter = (event) => { // don't do this or it prevents the user to select // the canvas using mouse/touch //event.preventDefault(); // map canvas coordinates to arcade screen coordinates const rect = canvas.getBoundingClientRect(); const aspectRatio = canvas.height / canvas.width; const actualWidth = Math.min(rect.height / aspectRatio, rect.width); const actualHeight = Math.min(rect.width * aspectRatio, rect.height); let xOffset = (rect.width - actualWidth) / 2; let yOffset = (rect.height - actualHeight) / 2; const x = ((event.clientX - rect.left - xOffset) / actualWidth) * canvas.width; const y = ((event.clientY - rect.top - yOffset) / actualHeight) * canvas.height; if (!isMultiplayerSession()) { board().mouseState.onEvent(event, x, y); } if (sendMessages) { sendMsg({ type: event.type, pointerId: event.pointerId, pointerType: event.pointerType, x, y, buttons: event.buttons, pressure: event.pressure, }); } }; const wheelReporter = (event) => { if (!isMultiplayerSession()) { board().mouseState.onWheelEvent(event.deltaX, event.deltaY, event.deltaZ); } if (sendMessages) { sendMsg({ type: event.type, dx: event.deltaX, dy: event.deltaY, dz: event.deltaZ, }); } }; for (const event of events) { canvas.addEventListener(event, reporter); } canvas.addEventListener("wheel", wheelReporter); } /** * This function gets called each time the program restarts */ pxsim.initCurrentRuntime = (msg) => { pxsim.runtime.board = new Board(); pxsim.Keyboard.cancelTextPrompt(); pxsim.initGamepad(); const theme = pxsim.theme.parseTheme(msg.theme); if (theme && !themeFromQueryParameter) { pxsim.theme.applyTheme(theme); } board().setActivePlayer(msg.activePlayer, theme); if (!forcedUpdateLoop) { forcedUpdateLoop = true; // this is used to force screen update if game loop is stuck or not set up properly //forcedUpdateLoop = setInterval(() => { //board().screenState.maybeForceUpdate() //}, 100) window.onfocus = function (e) { indicateFocus(true); return false; }; window.onblur = function (e) { indicateFocus(false); return false; }; window.onkeydown = function (e) { if (e.key === "Unidentified") { // This is a synthetic keyboard event derived from another input source (e.g. gamepad), ignore. return; } const b = board(); if (b) { const key = typeof e.which == "number" ? e.which : e.keyCode; b.setKey(key, true, e); } pxsim.Runtime.postMessage({ type: "messagepacket", broadcast: false, channel: "keydown-" + e.key, data: new Uint8Array(), }); }; window.onkeyup = function (e) { if (e.key === "Unidentified") { // This is a synthetic keyboard event derived from another input source (e.g. gamepad), ignore. return; } const b = board(); if (b) { const key = typeof e.which == "number" ? e.which : e.keyCode; b.setKey(key, false, e); } pxsim.Runtime.postMessage({ type: "messagepacket", broadcast: false, channel: "keyup-" + e.key, data: new Uint8Array(), }); }; window.oncontextmenu = function (e) { e.preventDefault(); e.stopPropagation(); return false; }; window.onmouseover = function () { if (!document.hasFocus()) window.focus(); }; window.onmousedown = function (e) { const b = board(); if (b) { const keyCode = pxsim.MouseButtonToKeyCode[e.button]; if (keyCode) { b.setKey(keyCode, true, e); } } }; window.onmouseup = function (e) { const b = board(); if (b) { const keyCode = pxsim.MouseButtonToKeyCode[e.button]; if (keyCode) { b.setKey(keyCode, false, e); } } }; window.addEventListener("message", (ev) => { var _a; if (ev.data.button !== undefined && ev.data.type !== "multiplayer") { let key; switch (ev.data.button) { case 0: key = pxsim.Key.A; break; case 1: key = pxsim.Key.B; break; case 2: key = pxsim.Key.Up; break; case 3: key = pxsim.Key.Down; break; case 4: key = pxsim.Key.Left; break; case 5: key = pxsim.Key.Right; break; case 6: key = pxsim.Key.Menu; break; case 7: key = pxsim.Key.Reset; break; } const b = board(); if (b) b.setButton(key, ev.data.pressed); } if (ev.data.context !== undefined) { if (ev.data.context == "client" || ev.data.context == "server") { const b = board(); b.multiplayerState.origin = ev.data.context; } } if (ev.data.type == "setactiveplayer") { const b = board(); if (!(b.multiplayerState && b.multiplayerState.origin)) { b.setActivePlayer(ev.data.playerNumber, theme); } } else if (ev.data.type == "setsimthemecolor") { pxsim.theme.setSimThemeColor(ev.data.part, (_a = ev.data.color) === null || _a === void 0 ? void 0 : _a.replace("#", "")); } }); } }; /** * Gets the current 'board', eg. program state. */ function board() { return pxsim.runtime && pxsim.runtime.board; } pxsim.board = board; /** * Represents the entire state of the executing program. * Do not store state anywhere else! */ class Board extends pxsim.BaseBoard { constructor() { super(); this.startTime = Date.now(); this.isPaused = false; this.disableKeyEvents = false; this.lightState = {}; this.screenState = new pxsim.ScreenState(null); this.audioState = new pxsim.AudioState(); this.accelerometerState = new pxsim.AccelerometerState(pxsim.runtime); this.microphoneState = new pxsim.MicrophoneState(DAL.DEVICE_ID_MICROPHONE, 0, 255, 50, 120); this.controlMessageState = new pxsim.ControlMessageState(this); // set all pin ids [ { prefix: "PIN_A", id: 100, count: 32, }, { prefix: "PIN_B", id: 300, count: 32, }, { prefix: "PIN_C", id: 350, count: 32, }, { prefix: "PIN_D", id: 150, count: 32, }, { prefix: "PIN_P", id: 400, count: 20, }, ].forEach((pinp) => { for (let i = 0; i < pinp.count; ++i) { const id = pinp.id + i; pxsim.setConfigKey(pinp.prefix + i, id); if (pxsim.getConfig(id) == null) pxsim.setConfig(id, id); } }); // add pins from config const pins = pxsim .getAllConfigKeys() .filter((k) => /^PIN_/.test(k)) .map((k) => pxsim.getConfig(pxsim.getConfigKey(k))) .filter((id) => !!id); this.edgeConnectorState = new pxsim.EdgeConnectorState({ pins, }); this.lightSensorState = new pxsim.AnalogSensorState(DAL.DEVICE_ID_LIGHT_SENSOR); this.thermometerState = new pxsim.AnalogSensorState(DAL.DEVICE_ID_THERMOMETER); this.thermometerUnitState = pxsim.TemperatureUnit.Celsius; this.radioState = new pxsim.RadioState(pxsim.runtime, this, { ID_RADIO: DAL.DEVICE_ID_RADIO, RADIO_EVT_DATAGRAM: DAL.DEVICE_RADIO_EVT_DATAGRAM, }); this.multiplayerState = new pxsim.MultiplayerState(); this.keymapState = new pxsim.KeymapState(); this.mouseState = new pxsim.browserEvents.MouseState(); const scale = isEdge() || isIE() ? 10 : 1; this.gameplayer = new pxsim.visuals.GamePlayer(scale); throttleAnimation((cb) => (this.screenState.onChange = cb), () => this.gameplayer.draw(this.screenState)); this.activePlayer = undefined; } setActivePlayer(playerNumber, theme) { if (this.multiplayerState && this.multiplayerState.origin) return; const playerThemes = [ undefined, "p1", "p2", "p3", "p4", ]; const newPlayerTheme = playerThemes[playerNumber || 0]; if (!newPlayerTheme) { // invalid playerNumber return; } if ((!theme || !Object.keys(theme).length) && !themeFromQueryParameter) { pxsim.theme.applySkin(newPlayerTheme); } this.activePlayer = playerNumber || undefined; } getDefaultPitchPin() { return undefined; } setKey(which, isPressed, e) { if (this.disableKeyEvents) { return; } if (!isMultiplayerSession() && e instanceof KeyboardEvent) { pxsim.browserEvents.onKeyboardEvent(e, isPressed); } let k = this.keymapState.getKey(which); if (k) { this.setButton(k, isPressed); e.preventDefault(); e.stopPropagation(); } } setButton(which, isPressed) { const inMultiplayerSession = !!this.multiplayerState.origin; // Disallow local input for player 2+ in multiplayer mode. if (inMultiplayerSession && which > pxsim.Key.Menu) return; if (!which) return; let playerOffset = 0; if (!inMultiplayerSession && this.activePlayer && this.activePlayer > 1 && which > 0 && which < 7) { playerOffset = this.activePlayer - 1; } this.handleKeyEvent(which, isPressed, playerOffset); } handleKeyEvent(key, isPressed, playerOffset = 0) { const gameKey = key + 7 * (playerOffset | 0); // handle system keys switch (key) { case pxsim.Key.Reset: if (!isPressed) { this.gameplayer.dispose(); pxsim.Runtime.postMessage({ type: "simulator", command: "restart", }); } return; case pxsim.Key.Screenshot: if (!isPressed) { pxsim.Runtime.postScreenshotAsync(); } return; case pxsim.Key.Gif: if (!isPressed) pxsim.Runtime.requestToggleRecording(); break; case pxsim.Key.TogglePause: if (!isPressed) { // TODO: https://github.com/microsoft/pxt-arcade/issues/1580 // Add pause/resume screen to simulator when clicking (YouTube style) this.isPaused = !this.isPaused; } break; } //this.lastKey = Date.now() this.bus.queue(isPressed ? INTERNAL_KEY_DOWN : INTERNAL_KEY_UP, gameKey); // no 'any' for p2-4 if (!playerOffset) { this.bus.queue(isPressed ? INTERNAL_KEY_DOWN : INTERNAL_KEY_UP, 0); // "any" key } if (this.gameplayer) { this.gameplayer.buttonChanged(key, isPressed); } if (this.multiplayerState && key >= pxsim.Key.Left && key <= pxsim.Key.B) { this.multiplayerState.setButton(key, isPressed); } } screenshotAsync() { const cvs = this.gameplayer.screen; const ctx = cvs.getContext("2d"); const id = ctx.getImageData(0, 0, cvs.width, cvs.height); return Promise.resolve(id); } tryScreenshot() { // ignore } resize() { } async initAsync(msg) { var _a; this.runOptions = msg; this.stats = document.getElementById("debug-stats"); this.stats.className = "stats no-select"; this.id = msg.id; indicateFocus(document.hasFocus()); this.accelerometerState.attachEvents(document.body); const mpRole = this.runOptions && this.runOptions.options && this.runOptions.options.mpRole; this.multiplayerState.init(mpRole); if (mpRole === "client") { const wrapper = document.getElementById("wrap"); wrapper && wrapper.classList.add("mp-client"); } const theme = pxsim.theme.parseTheme(msg.theme); if (theme && !themeFromQueryParameter) { pxsim.theme.applyTheme(theme); } this.setActivePlayer(msg.activePlayer, theme); this.updateStats(); if (((_a = msg.options) === null || _a === void 0 ? void 0 : _a.mpRole) !== "server") { if (pxsim.shouldShowMute()) { document.body.appendChild(pxsim.createMuteButton()); pxsim.AudioContextManager.mute(true); pxsim.setParentMuteState("disabled"); } } let safariEnablePromise; if (isFirstRunSafari && !safariEnablePromise) { const safariWarning = document.getElementById("safari-enable-game"); if (isSafari() && msg.options && msg.options.mpRole === "server") { safariEnablePromise = new Promise(resolve => { safariWarning.style.display = "flex"; safariWarning.addEventListener("click", () => { safariWarning.remove(); isFirstRunSafari = false; resolve(); }); }); } else { safariWarning.remove(); isFirstRunSafari = false; } } if (isFirstRunSafari) { await safariEnablePromise; } } updateStats() { this.stats.textContent = this.screenState.stats || ""; // screenshots are handled in the share dialog } tryGetNeopixelState(pinId) { return this.lightState[pinId]; } neopixelState(pinId) { if (pinId === undefined) pinId = this.edgeConnectorState.pins[0].id; let state = this.lightState[pinId]; if (!state) state = this.lightState[pinId] = new pxsim.CommonNeoPixelState(); return state; } kill() { super.kill(); if (this.gameplayer) this.gameplayer.dispose(); pxsim.Keyboard.cancelTextPrompt(); } } pxsim.Board = Board; function indicateFocus(hasFocus) { document .getElementById("root") .setAttribute("class", hasFocus ? "" : "blur"); const b = board(); if (b) { b.gameplayer.indicateFocus(hasFocus); if (hasFocus && b.disableKeyEvents) { pxsim.Keyboard.focusTextInput(); } } } pxsim.indicateFocus = indicateFocus; function throttleAnimation(event, handler) { let requested = false; event(() => { if (!requested) { window.requestAnimationFrame(() => { handler(); requested = false; }); } }); } function isMultiplayerSession() { return !!board().multiplayerState.origin; } })(pxsim || (pxsim = {})); // Copied verbatim from pxt-core function hasNavigator() { return typeof navigator !== "undefined"; } function isEdge() { return hasNavigator() && /Edge/i.test(navigator.userAgent); } function isIE() { return hasNavigator() && /Trident/i.test(navigator.userAgent); } //MacIntel on modern Macs function isMac() { return hasNavigator() && /Mac/i.test(navigator.platform); } //Microsoft Edge and IE11 lie about being Chrome function isChrome() { return (!isEdge() && !isIE() && !!navigator && (/Chrome/i.test(navigator.userAgent) || /Chromium/i.test(navigator.userAgent))); } //Chrome and Microsoft Edge lie about being Safari function isSafari() { //Could also check isMac but I don't want to risk excluding iOS //Checking for iPhone, iPod or iPad as well as Safari in order to detect home screen browsers on iOS return !isChrome() && !isEdge() && !!navigator && /(Macintosh|Safari|iPod|iPhone|iPad)/i.test(navigator.userAgent); } (function (pxsim) { var pxtcore; (function (pxtcore) { function getButtonByPinCfg(key) { return { id: key }; } pxtcore.getButtonByPinCfg = getButtonByPinCfg; })(pxtcore = pxsim.pxtcore || (pxsim.pxtcore = {})); })(pxsim || (pxsim = {})); (function (pxsim) { var ButtonMethods; (function (ButtonMethods) { function id(button) { return button.id; } ButtonMethods.id = id; })(ButtonMethods = pxsim.ButtonMethods || (pxsim.ButtonMethods = {})); })(pxsim || (pxsim = {})); var pxsim; (function (pxsim) { var Keyboard; (function (Keyboard) { let KeyboardEvent; (function (KeyboardEvent) { KeyboardEvent[KeyboardEvent["Change"] = 7339] = "Change"; KeyboardEvent[KeyboardEvent["Enter"] = 7340] = "Enter"; KeyboardEvent[KeyboardEvent["Cancel"] = 7341] = "Cancel"; })(KeyboardEvent = Keyboard.KeyboardEvent || (Keyboard.KeyboardEvent = {})); let input; let onClick = (e) => { if (input) input.focus(); e.stopPropagation(); e.preventDefault(); }; function validateNumberInput() { if (!input) return; let cleaned = ""; for (let i = 0; i < input.value.length; i++) { const current = input.value.charAt(i); if (current === "-") { if (cleaned.length !== 0) { continue; } } else if (current === ".") { if (cleaned.indexOf(".") !== -1) { continue; } } else if (!/[0-9]/.test(current)) { continue; } cleaned += current; } input.value = cleaned; } function promptForText(maxLength, numberOnly) { if (input) input.remove(); pxsim.board().disableKeyEvents = true; // clear all active button presses pxsim.board().setButton(pxsim.Key.A, false); pxsim.board().setButton(pxsim.Key.B, false); pxsim.board().setButton(pxsim.Key.Up, false); pxsim.board().setButton(pxsim.Key.Right, false); pxsim.board().setButton(pxsim.Key.Down, false); pxsim.board().setButton(pxsim.Key.Left, false); pxsim.board().setButton(pxsim.Key.Menu, false); input = document.createElement("input"); input.type = "text"; input.maxLength = maxLength; document.body.appendChild(input); input.addEventListener("change", e => { if (numberOnly) { validateNumberInput(); } pxsim.board().bus.queue(KeyboardEvent.Change, 0); }); input.addEventListener("keyup", e => { if (e.key === "Enter" || e.key === "Escape") { cancelTextPrompt(); pxsim.board().bus.queue(KeyboardEvent.Enter, 0); } else if (numberOnly) { validateNumberInput(); } pxsim.board().bus.queue(KeyboardEvent.Change, 0); }); input.addEventListener("keydown", e => { pxsim.board().bus.queue(KeyboardEvent.Change, 0); if (numberOnly) { validateNumberInput(); } }); document.addEventListener("pointerdown", onClick); document.addEventListener("click", onClick); input.style.opacity = "0"; input.focus(); } Keyboard.promptForText = promptForText; function cancelTextPrompt() { if (input) input.remove(); pxsim.board().disableKeyEvents = false; document.removeEventListener("pointerdown", onClick); document.removeEventListener("click", onClick); } Keyboard.cancelTextPrompt = cancelTextPrompt; function cancelSystemKeyboard() { if (input) { cancelTextPrompt(); pxsim.board().bus.queue(KeyboardEvent.Cancel, 0); } } Keyboard.cancelSystemKeyboard = cancelSystemKeyboard; function getTextPromptString() { if (input) return input.value; return ""; } Keyboard.getTextPromptString = getTextPromptString; function getLocalizedInstructions() { return pxsim.localization.lf("Type your response using your keyboard and hit ENTER"); } Keyboard.getLocalizedInstructions = getLocalizedInstructions; function focusTextInput() { if (input) input.focus(); } Keyboard.focusTextInput = focusTextInput; function getTextPromptSelectionStart() { if (input) return input.selectionStart; return 0; } Keyboard.getTextPromptSelectionStart = getTextPromptSelectionStart; function getTextPromptSelectionEnd() { if (input) return input.selectionEnd; return 0; } Keyboard.getTextPromptSelectionEnd = getTextPromptSelectionEnd; function isSystemKeyboardSupported() { return !pxsim.board().multiplayerState.origin && !pxsim.simButtonsHidden; } Keyboard.isSystemKeyboardSupported = isSystemKeyboardSupported; })(Keyboard = pxsim.Keyboard || (pxsim.Keyboard = {})); })(pxsim || (pxsim = {})); var pxsim; (function (pxsim) { var theme; (function (theme_1) { function parseTheme(theme) { if (!theme) return undefined; return (typeof theme == "string") ? { skin: theme } : theme; } theme_1.parseTheme = parseTheme; function applyTheme(theme) { const parsedTheme = parseTheme(theme); if (parsedTheme.skin) { applySkin(parsedTheme.skin.toLowerCase()); } const setThemeIfDefined = (themeType) => { const paramVal = parsedTheme[themeType]; if (paramVal) { setSimThemeColor(themeType, paramVal); } }; setThemeIfDefined("background-color"); setThemeIfDefined("button-stroke"); setThemeIfDefined("text-color"); setThemeIfDefined("button-fill"); setThemeIfDefined("dpad-fill"); } theme_1.applyTheme = applyTheme; function setSimThemeColor(part, color) { if (!part || (!(color == undefined || /^(#|0x)?[0-9A-F]{6}$/i.test(color)))) return; if (part != "background-color" && part != "button-stroke" && part != "text-color" && part != "button-fill" && part != "dpad-fill") { return; } const propName = `--sim-${part}`; const propColor = color ? `#${color.replace(/^(#|0x)/i, "")}` : undefined; const wrapper = document.getElementById("wrap"); if (propColor) { wrapper.style.setProperty(propName, propColor); } else { wrapper.style.removeProperty(propName); } } theme_1.setSimThemeColor = setSimThemeColor; function applySkin(skin) { switch (skin) { case "zune": { zuneSkin(); break; } case "p1": case "red": { redSkin(); break; } case "p2": case "blue": { blueSkin(); break; } case "p3": case "orange": { orangeSkin(); break; } case "p4": case "green": { greenSkin(); break; } case "brown": { brownSkin(); break; } case "bubblegum": { bubblegumSkin(); break; } case "purple": { purpleSkin(); break; } case "microcode": { microcodeSkin(); break; } case "junior": { juniorSkin(); break; } default: break; } } theme_1.applySkin = applySkin; function zuneSkin() { setSimThemeColor("background-color", "#564131"); setSimThemeColor("button-stroke", "#524F4E"); setSimThemeColor("text-color", "#E7E7E7"); const wrapper = document.getElementById("wrap"); if (wrapper) { wrapper.classList.add("zune", "portrait-only"); /** SVG overriding a, b button positions: b on left, a on right <svg class="game-button-svg" viewBox="0 0 40 40"=>"0 0 100 28"> <circle class="button-b" cx="13"=>"18" cy="28"=>"12" /> <text class="label-b no-select" x="13"=>"18" y="28"=>"12">B</text> <circle class="button-a" cx="28"=>"82" cy="12.5"=>"12"/> <text class="label-a no-select" x="28"=>"82" y="12.5"=>"12">A</text> </svg> // simplify viewBox <svg class="game-joystick-svg" viewBox="1 0 40 40"=>"0 0 40 40"/> **/ const gameButtonSvg = document.querySelector(".game-button-svg"); gameButtonSvg.removeAttribute("width"); gameButtonSvg.removeAttribute("height"); gameButtonSvg.setAttribute("viewBox", "0 0 100 28"); const joystickSvg = document.querySelector(".game-joystick-svg"); joystickSvg.setAttribute("viewBox", "0 0 40 40"); const bButton = document.querySelector(".button-b"); const bLabel = document.querySelector(".label-b"); bButton.setAttribute("cx", "18"); bButton.setAttribute("cy", "12"); bLabel.setAttribute("x", "18"); bLabel.setAttribute("y", "12"); const aButton = document.querySelector(".button-a"); const aLabel = document.querySelector(".label-a"); aButton.setAttribute("cx", "82"); aButton.setAttribute("cy", "12"); aLabel.setAttribute("x", "82"); aLabel.setAttribute("y", "12"); } } function juniorSkin() { setSimThemeColor("background-color", "#EB4444"); setSimThemeColor("button-fill", "#D54322"); setSimThemeColor("button-stroke", "#670C0C"); setSimThemeColor("text-color", "#FFFFFF"); const wrapper = document.getElementById("wrap"); if (wrapper) { wrapper.classList.add("junior", "portrait-only"); const gameButtonSvg = document.querySelector(".game-button-svg"); gameButtonSvg.removeAttribute("width"); gameButtonSvg.removeAttribute("height"); gameButtonSvg.setAttribute("viewBox", "0 0 100 28"); const aButton = document.querySelector(".button-a"); const aLabel = document.querySelector(".label-a"); aButton.setAttribute("cx", "50"); aButton.setAttribute("cy", "13"); aButton.setAttribute("r", "11.5"); aLabel.setAttribute("x", "50"); aLabel.setAttribute("y", "13"); } } function brownSkin() { setSimThemeColor("background-color", "#8B4513"); setSimThemeColor("button-stroke", "#68320C"); } function bubblegumSkin() { setSimThemeColor("background-color", "#F7ABB9"); setSimThemeColor("button-stroke", "#71C1C9"); setSimThemeColor("button-fill", "#92F5FF"); setSimThemeColor("text-color", "#4E4E4E"); setSimThemeColor("dpad-fill", "#F7ABB9"); const msftLogo = document.querySelector(".game-player-msft"); if (msftLogo) { msftLogo.classList.add("gray"); } } function redSkin() { setSimThemeColor("background-color", "#ED3636"); setSimThemeColor("button-stroke", "#8D2525"); } function blueSkin() { setSimThemeColor("background-color", "#4E4EE9"); setSimThemeColor("button-stroke", "#3333A1"); } function orangeSkin() { setSimThemeColor("background-color", "#FF9A14"); setSimThemeColor("button-stroke", "#B0701A"); } function greenSkin() { setSimThemeColor("background-color", "#4EB94E"); setSimThemeColor("button-stroke", "#245D24"); } function purpleSkin() { setSimThemeColor("background-color", "#660fC7"); setSimThemeColor("button-stroke", "#4C0B95"); } function microcodeSkin() { setSimThemeColor("background-color", "#3F3F3F"); setSimThemeColor("button-stroke", "#212121"); setSimThemeColor("button-fill", "#2D2D2D"); setSimThemeColor("text-color", "#D9D9D9"); } })(theme = pxsim.theme || (pxsim.theme = {})); })(pxsim || (pxsim = {})); var pxsim; (function (pxsim) { var visuals; (function (visuals) { class GameButtons { // <div id="game-buttons-container" class="game-buttons"> // <div class="spacer" /> // <div class="action-button"> // <button class="share-mod-button">Share</button> // </div> // <svg xmlns="http://www.w3.org/2000/svg" class="game-button-svg" viewBox="0 0 40 40" width="200px" height="200px"> // <circle class="button-b" cx="13" cy="28" r="9" fill="var(--sim-button-fill)" stroke="var(--sim-button-stroke)" strokeWidth="2.5" /> // <text class="label-b" x="13" y="28" textAnchor="middle" dy="2.5" fontSize="8">B</text> // <circle class="button-a" cx="28" cy="11" r="9" fill="var(--sim-button-fill)" stroke="var(--sim-button-stroke)" strokeWidth="2.5" /> // <text class="label-a" x="28" y="11" textAnchor="middle" dy="2.5" fontSize="8">A</text> // </svg> // </div> constructor(container) { this.buttonPressCount = {}; this.bindings = []; this.logEvents = () => { if (Object.keys(this.buttonPressCount).some(x => !!this.buttonPressCount[x])) { // tickEvent("shareExperiment.play.buttonPress", this.buttonPressCount); Object.keys(this.buttonPressCount).forEach(k => this.buttonPressCount[k] = 0); } }; this.cleanupInterval = () => { clearInterval(this.buttonPressInterval); this.buttonPressCount = {}; }; this.parent = container || document.getElementById("game-buttons-container"); this.aButton = this.parent.getElementsByClassName("button-a").item(0); this.aLabel = this.parent.getElementsByClassName("label-a").item(0); this.bButton = this.parent.getElementsByClassName("button-b").item(0); ; this.bLabel = this.parent.getElementsByClassName("label-b").item(0); this.dragSurface = this.parent.getElementsByClassName("game-button-svg").item(0); this.bindEvents(this.dragSurface); } buttonChanged(button, isPressed) { switch (button) { case pxsim.Key.A: this.setButtonState(button, isPressed, true); break; case pxsim.Key.B: this.setButtonState(button, isPressed, true); break; default: break; } } dispose() { this.aButton = undefined; this.aLabel = undefined; this.bButton = undefined; this.bLabel = undefined; this.parent = undefined; this.dragSurface = undefined; this.cleanupInterval(); this.unBindEvents(); } pointIsWithinCircle(x, y, circle) { const bounds = circle.getBoundingClientRect(); const radius = bounds.width / 2; const distance = Math.sqrt(Math.pow(x - (bounds.left + radius), 2) + Math.pow(y - (bounds.top + radius), 2)); return distance < radius; } updateButtonGesture(x, y) { this.setButtonState(pxsim.Key.A, this.pointIsWithinCircle(x, y, this.aButton)); this.setButtonState(pxsim.Key.B, this.pointIsWithinCircle(x, y, this.bButton)); pxsim.indicateFocus(true); } clearButtonPresses() { this.setButtonState(pxsim.Key.A, false); this.setButtonState(pxsim.Key.B, false); } setButtonState(button, pressed, quiet = false) { const isAButton = button === pxsim.Key.A; const circle = isAButton ? this.aButton : this.bButton; const label = isAButton ? this.aLabel : this.bLabel; if (circle && label) { circle.setAttribute("fill", pressed ? "var(--sim-background-color)" : "var(--sim-button-fill)"); label.setAttribute("fill", pressed ? "var(--sim-button-fill)" : ""); } if (!quiet) { if (pressed) { if (!this.buttonPressCount[pxsim.Key[button]]) this.buttonPressCount[pxsim.Key[button]] = 0; this.buttonPressCount[pxsim.Key[button]] += 1; visuals.pressButton(button); } else visuals.releaseButton(button); } } bindEvents(surface) { if (!surface) return; this.unBindEvents(); if (visuals.hasPointerEvents()) {