pxt-arcade
Version:
Small arcade editor for MakeCode
1,212 lines • 243 kB
JavaScript
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()) {