diff --git a/extensions/src/common/extension/mixins/optional/addCostumes/MockBitmapAdapter.ts b/extensions/src/common/extension/mixins/optional/addCostumes/MockBitmapAdapter.ts new file mode 100644 index 000000000..8d4201b55 --- /dev/null +++ b/extensions/src/common/extension/mixins/optional/addCostumes/MockBitmapAdapter.ts @@ -0,0 +1,136 @@ +/** + * Class adapted from: https://github.com/LLK/scratch-svg-renderer/blob/develop/src/bitmap-adapter.js + */ +export default class { + private makeImage() { return new Image() } + private makeCanvas() { return document.createElement('canvas') } + + /** + * Return a canvas with the resized version of the given image, done using nearest-neighbor interpolation + * @param {CanvasImageSource} image The image to resize + * @param {int} newWidth The desired post-resize width of the image + * @param {int} newHeight The desired post-resize height of the image + * @returns {HTMLCanvasElement} A canvas with the resized image drawn on it. + */ + resize(image, newWidth, newHeight) { + // We want to always resize using nearest-neighbor interpolation. However, canvas implementations are free to + // use linear interpolation (or other "smooth" interpolation methods) when downscaling: + // https://bugzilla.mozilla.org/show_bug.cgi?id=1360415 + // It seems we can get around this by resizing in two steps: first width, then height. This will always result + // in nearest-neighbor interpolation, even when downscaling. + const stretchWidthCanvas = this.makeCanvas(); + stretchWidthCanvas.width = newWidth; + stretchWidthCanvas.height = image.height; + let context = stretchWidthCanvas.getContext('2d'); + context.imageSmoothingEnabled = false; + context.drawImage(image, 0, 0, stretchWidthCanvas.width, stretchWidthCanvas.height); + const stretchHeightCanvas = this.makeCanvas(); + stretchHeightCanvas.width = newWidth; + stretchHeightCanvas.height = newHeight; + context = stretchHeightCanvas.getContext('2d'); + context.imageSmoothingEnabled = false; + context.drawImage(stretchWidthCanvas, 0, 0, stretchHeightCanvas.width, stretchHeightCanvas.height); + return stretchHeightCanvas; + } + + /** + * Scratch 2.0 had resolution 1 and 2 bitmaps. All bitmaps in Scratch 3.0 are equivalent + * to resolution 2 bitmaps. Therefore, converting a resolution 1 bitmap means doubling + * it in width and height. + * @param {!string} dataURI Base 64 encoded image data of the bitmap + * @param {!function} callback Node-style callback that returns updated dataURI if conversion succeeded + */ + convertResolution1Bitmap(dataURI, callback) { + const image = new Image(); + image.src = dataURI; + image.onload = () => { + callback(null, this.resize(image, image.width * 2, image.height * 2).toDataURL()); + }; + image.onerror = () => { + callback('Image load failed'); + }; + } + + /** + * Given width/height of an uploaded item, return width/height the image will be resized + * to in Scratch 3.0 + * @param {!number} oldWidth original width + * @param {!number} oldHeight original height + * @return {object} Array of new width, new height + */ + getResizedWidthHeight(oldWidth, oldHeight) { + const STAGE_WIDTH = 480; + const STAGE_HEIGHT = 360; + const STAGE_RATIO = STAGE_WIDTH / STAGE_HEIGHT; + + // If both dimensions are smaller than or equal to corresponding stage dimension, + // double both dimensions + if ((oldWidth <= STAGE_WIDTH) && (oldHeight <= STAGE_HEIGHT)) { + return { width: oldWidth * 2, height: oldHeight * 2 }; + } + + // If neither dimension is larger than 2x corresponding stage dimension, + // this is an in-between image, return it as is + if ((oldWidth <= STAGE_WIDTH * 2) && (oldHeight <= STAGE_HEIGHT * 2)) { + return { width: oldWidth, height: oldHeight }; + } + + const imageRatio = oldWidth / oldHeight; + // Otherwise, figure out how to resize + if (imageRatio >= STAGE_RATIO) { + // Wide Image + return { width: STAGE_WIDTH * 2, height: STAGE_WIDTH * 2 / imageRatio }; + } + // In this case we have either: + // - A wide image, but not with as big a ratio between width and height, + // making it so that fitting the width to double stage size would leave + // the height too big to fit in double the stage height + // - A square image that's still larger than the double at least + // one of the stage dimensions, so pick the smaller of the two dimensions (to fit) + // - A tall image + // In any of these cases, resize the image to fit the height to double the stage height + return { width: STAGE_HEIGHT * 2 * imageRatio, height: STAGE_HEIGHT * 2 }; + } + + /** + * Given bitmap data, resize as necessary. + * @param {string} fileData Base 64 encoded image data of the bitmap + * @param {string} fileType The MIME type of this file + * @returns {Promise} Resolves to resized image data Uint8Array + */ + importBitmap(dataURI: string): Promise { + return new Promise((resolve, reject) => { + const image = this.makeImage(); + image.src = dataURI; + image.onload = () => { + const newSize = this.getResizedWidthHeight(image.width, image.height); + if (newSize.width === image.width && newSize.height === image.height) { + // No change + resolve(this.convertDataURIToBinary(dataURI)); + } else { + const resizedDataURI = this.resize(image, newSize.width, newSize.height).toDataURL(); + resolve(this.convertDataURIToBinary(resizedDataURI)); + } + }; + image.onerror = () => { + reject('Image load failed'); + }; + }); + } + + // TODO consolidate with scratch-vm/src/util/base64-util.js + // From https://gist.github.com/borismus/1032746 + convertDataURIToBinary(dataURI) { + const BASE64_MARKER = ';base64,'; + const base64Index = dataURI.indexOf(BASE64_MARKER) + BASE64_MARKER.length; + const base64 = dataURI.substring(base64Index); + const raw = window.atob(base64); + const rawLength = raw.length; + const array = new Uint8Array(new ArrayBuffer(rawLength)); + + for (let i = 0; i < rawLength; i++) { + array[i] = raw.charCodeAt(i); + } + return array; + } +} \ No newline at end of file diff --git a/extensions/src/common/extension/mixins/optional/addCostumes/index.ts b/extensions/src/common/extension/mixins/optional/addCostumes/index.ts new file mode 100644 index 000000000..5f40ccdd3 --- /dev/null +++ b/extensions/src/common/extension/mixins/optional/addCostumes/index.ts @@ -0,0 +1,112 @@ +import type RenderedTarget from "$scratch-vm/sprites/rendered-target"; +import Target from "$scratch-vm/engine/target"; +import { MinimalExtensionConstructor } from "../../required"; +import MockBitmapAdapter from "./MockBitmapAdapter"; +import { getUrlHelper } from "./utils"; + +let bitmapAdapter: MockBitmapAdapter; +let urlHelper: ReturnType; + +const rendererKey: keyof RenderedTarget = "renderer"; +const isRenderedTarget = (target: Target | RenderedTarget): target is RenderedTarget => rendererKey in target; + +/** + * Mixin the ability for extensions to add costumes to sprites + * @param Ctor + * @returns + * @see https://www.typescriptlang.org/docs/handbook/mixins.html + */ +export default function (Ctor: T) { + abstract class ExtensionWithCustomSupport extends Ctor { + + /** + * Add a costume to the current sprite based on some image data + * @param {RenderedTarget} target (e.g. `util.target`) + * @param {ImageData} image What image to use to create the costume + * @param {"add only" | "add and set"} action What action should be applied + * - **_add only_**: generates the costume and append it it to the sprite's costume library + * - **_add and set_**: Both generate the costume (adding it to the sprite's costume library) and set it as the sprite's current costume + * @param {string?} name optional name to attach to the costume + */ + async addCostume(target: Target, image: ImageData, action: "add only" | "add and set", name?: string) { + if (!isRenderedTarget(target)) return console.warn("Costume could not be added as the supplied target wasn't a rendered target"); + + name ??= `${this.id}_generated_${Date.now()}`; + bitmapAdapter ??= new MockBitmapAdapter(); + urlHelper ??= getUrlHelper(image); + + // storage is of type: https://github.com/LLK/scratch-storage/blob/develop/src/ScratchStorage.js + const { storage } = this.runtime; + const dataFormat = storage.DataFormat.PNG; + const assetType = storage.AssetType.ImageBitmap; + const dataBuffer = await bitmapAdapter.importBitmap(urlHelper.getDataURL(image)); + + const asset = storage.createAsset(assetType, dataFormat, dataBuffer, null, true); + const { assetId } = asset; + const costume = { name, dataFormat, asset, md5: `${assetId}.${dataFormat}`, assetId }; + + await this.runtime.addCostume(costume); + + const { length } = target.getCostumes(); + + target.addCostume(costume, length); + if (action === "add and set") target.setCostume(length); + } + + /** + * Add a costume to the current sprite based on a bitmpa input + * @param {RenderedTarget} target (e.g. `util.target`) + * @param {string} bitmapImage What image to use to create the costume + * @param {"add only" | "add and set"} action What action should be applied + * - **_add only_**: generates the costume and append it it to the sprite's costume library + * - **_add and set_**: Both generate the costume (adding it to the sprite's costume library) and set it as the sprite's current costume + * @param {string?} name optional name to attach to the costume + */ + async addCostumeBitmap(target: Target, bitmapImage: string, action: "add only" | "add and set", name?: string) { + if (!isRenderedTarget(target)) return console.warn("Costume could not be added as the supplied target wasn't a rendered target"); + + name ??= `${this.id}_generated_${Date.now()}`; + bitmapAdapter ??= new MockBitmapAdapter(); + //urlHelper ??= getUrlHelper(image); + + // storage is of type: https://github.com/LLK/scratch-storage/blob/develop/src/ScratchStorage.js + const { storage } = this.runtime; + const dataFormat = storage.DataFormat.PNG; + const assetType = storage.AssetType.ImageBitmap; + const dataBuffer = await bitmapAdapter.importBitmap(bitmapImage); + + const asset = storage.createAsset(assetType, dataFormat, dataBuffer, null, true); + const { assetId } = asset; + const costume = { name, dataFormat, asset, md5: `${assetId}.${dataFormat}`, assetId }; + + await this.runtime.addCostume(costume); + + const { length } = target.getCostumes(); + + target.addCostume(costume, length); + if (action === "add and set") target.setCostume(length); + } + + /** + * Add a costume to the current sprite based on same image data + * @param {RenderedTarget} target (e.g. `util.target`) + * @param {string?} name costume name to look for + */ + setCostumeByName(target: Target, name: string): boolean { + if (!isRenderedTarget(target)) { + console.warn("Costume could not be set as the supplied target wasn't a rendered target"); + return false; + } + + let costumeIdx = target.getCostumeIndexByName(name); + if (costumeIdx >= 0) { + target.setCostume(costumeIdx); + return true; + } + return false; + } + + } + + return ExtensionWithCustomSupport; +} \ No newline at end of file diff --git a/extensions/src/common/extension/mixins/optional/addCostumes/utils.ts b/extensions/src/common/extension/mixins/optional/addCostumes/utils.ts new file mode 100644 index 000000000..d7b1b8260 --- /dev/null +++ b/extensions/src/common/extension/mixins/optional/addCostumes/utils.ts @@ -0,0 +1,31 @@ +export const getUrlHelper = (dimensions: { width: number, height: number }) => { + const canvas = document.body.appendChild(document.createElement("canvas")); + + const setDimensions = ({ width, height }: Parameters[0]) => { + if (canvas.width !== width) canvas.width = width; + if (canvas.height !== height) canvas.height = height; + }; + + setDimensions(dimensions); + + canvas.hidden = true; + const context = canvas.getContext("2d"); + + return { + /** + * + * @param image + * @returns + */ + getDataURL(image: ImageData) { + const { width, height } = image; + setDimensions(image); + context.save(); + context.clearRect(0, 0, width, height); + context.putImageData(image, 0, 0); + const url = canvas.toDataURL('image/png'); + context.restore(); + return url; + } + } +} \ No newline at end of file diff --git a/extensions/src/common/extension/mixins/optional/blocks/setVideoTransparency.ts b/extensions/src/common/extension/mixins/optional/blocks/setVideoTransparency.ts new file mode 100644 index 000000000..a125f824e --- /dev/null +++ b/extensions/src/common/extension/mixins/optional/blocks/setVideoTransparency.ts @@ -0,0 +1,29 @@ +import { block } from "$common/extension/decorators/blocks"; +import { withDependencies } from "../../dependencies"; +import { MinimalExtensionConstructor } from "../../required"; +import video from "../video"; + +/** + * Mixin a 'setVideoTransparency' Block to control the transparency of the videofeed + * @param Ctor + * @returns + * @see https://www.typescriptlang.org/docs/handbook/mixins.html + */ +export default function (Ctor: T) { + abstract class ExtensionWithSetVideoTransparencyBlock extends withDependencies(Ctor, video) { + /** + * A `command` block that takes a single number argument and uses it to set the transparency of the video feed. + * @param transparency What transparency to set -- a higher number is more transparent (thus '100' is fully invisible) + */ + @block({ + type: "command", + text: (transparency) => `Set video to ${transparency}% transparent`, + arg: "number" + }) + setVideoTransparencyBlock(transparency: number) { + this.setVideoTransparency(transparency); + } + } + + return ExtensionWithSetVideoTransparencyBlock; +} diff --git a/extensions/src/common/extension/mixins/optional/blocks/toggleVideoState.ts b/extensions/src/common/extension/mixins/optional/blocks/toggleVideoState.ts new file mode 100644 index 000000000..989581fa2 --- /dev/null +++ b/extensions/src/common/extension/mixins/optional/blocks/toggleVideoState.ts @@ -0,0 +1,31 @@ +import { block } from "$common/extension/decorators/blocks"; +import { withDependencies } from "../../dependencies"; +import { MinimalExtensionConstructor } from "../../required"; +import video from "../video"; + +/** + * Mixin a 'toggleVideo' Block to control whether the video feed is on, off, or flipped + * @param Ctor + * @returns + * @see https://www.typescriptlang.org/docs/handbook/mixins.html + */ +export default function (Ctor: T) { + abstract class ExtensionWithToggleVideoBlock extends withDependencies(Ctor, video) { + /** + * A `command` block that sets the current video state + * @param state What state to set ("on", "off", or "on (flipped)") + * @returns + */ + @block({ + type: "command", + text: (state) => `Set video feed to ${state}`, + arg: { type: "string", options: ["on", "off", "on (flipped)"] } + }) + toggleVideoBlock(state: "off" | "on" | "on (flipped)") { + if (state === "off") return this.disableVideo(); + this.enableVideo(state === "on"); + } + } + + return ExtensionWithToggleVideoBlock; +} diff --git a/extensions/src/common/extension/mixins/optional/customArguments/index.ts b/extensions/src/common/extension/mixins/optional/customArguments/index.ts new file mode 100644 index 000000000..a86646382 --- /dev/null +++ b/extensions/src/common/extension/mixins/optional/customArguments/index.ts @@ -0,0 +1,104 @@ +import type Runtime from "$scratch-vm/engine/runtime"; +import CustomArgumentManager, { ArgumentEntry } from "$common/extension/mixins/optional/customArguments/CustomArgumentManager"; +import { CustomArgumentUIConstructor, renderToDropdown } from "$common/extension/mixins/optional/customArguments/dropdownOverride"; +import { ArgumentType } from "$common/types/enums"; +import { openDropdownState, closeDropdownState, initDropdownState, customArgumentFlag, customArgumentCheck, dropdownStateFlag, dropdownEntryFlag } from "$common/globals"; +import { Argument, BaseGenericExtension } from "$common/types"; +import { MinimalExtensionConstructor } from "../../required"; +import { withDependencies } from "../../dependencies"; +import customSaveData from "../customSaveData"; + +type ComponentGetter = (id: string, componentName: string) => CustomArgumentUIConstructor; + +const callingContext = { + DrowpdownOpen: openDropdownState, + DropdownClose: closeDropdownState, + Init: initDropdownState, +} as const; + +/** + * Mixin the ability for extensions to create custom argument types with their own specific UIs + * @param Ctor + * @returns + * @see https://www.typescriptlang.org/docs/handbook/mixins.html + */ +export default function mixin(Ctor: T) { + abstract class ExtensionWithCustomArgumentSupport extends withDependencies(Ctor, customSaveData) { + /** + * Create a custom argument for one of this block's arguments + * @param param0 + * - component: The svelte component to render the custom argument UI + * - initial: The starting value of the the custom argument (including both its value and text representation) + * - acceptReportersHandler: A function that must be defined if you'd like for your custom argument to accept reporters + * @returns + */ + protected makeCustomArgument = ({ component, initial, acceptReportersHandler: handler }: { component: string, initial: ArgumentEntry, acceptReportersHandler?: (x: any) => ArgumentEntry }): Argument => { + this.argumentManager ??= new CustomArgumentManager(); + const id = this.argumentManager.add(initial); + const getItems = () => [{ text: customArgumentFlag, value: JSON.stringify({ component, id }) }]; + return { + type: ArgumentType.Custom, + defaultValue: id, + options: handler === undefined ? getItems : { acceptsReports: true, getItems, handler }, + } as Argument + } + + protected argumentManager: CustomArgumentManager = null; + + public get customArgumentManager(): CustomArgumentManager { + return this.argumentManager + } + + public getOrCreateCustomArgumentManager(): CustomArgumentManager { + this.argumentManager ??= new CustomArgumentManager(); + return this.argumentManager; + } + + /** + * Utilized externally by scratch-vm to check if a given argument should be treated as a 'custom argument'. + * Checks if the value returned by a dyanmic menu indicates that it should be treated as a 'custom argument' + */ + private [customArgumentCheck](arr: Array) { + if (arr.length !== 1) return false; + const item = arr[0]; + if (typeof item !== "object") return false; + const { text } = item; + return text === customArgumentFlag; + }; + + /** + * Utilized externally by scratch-vm to process custom arguments + * @param runtime NOTE: once we switch to V2, we can remove this and instead use the extension's runtime + * @param param1 + * @param getComponent + * @returns + */ + private processCustomArgumentHack(runtime: Runtime, [{ value }]: { value: string }[], getComponent: ComponentGetter): (readonly [string, string])[] { + + const { id: extensionID, customArgumentManager: argumentManager } = this; + const { component, id: initialID } = JSON.parse(value) as { component: string, id: string }; + const context = runtime[dropdownStateFlag]; + + switch (context) { + case callingContext.Init: + return argumentManager.getCurrentEntries(); + case callingContext.DropdownClose: { + const result = argumentManager.tryResolve(); + return result ? [[result.entry.text, result.id]] : argumentManager.getCurrentEntries(); + } + case callingContext.DrowpdownOpen: { + const currentEntry = runtime[dropdownEntryFlag] as ArgumentEntry; + const prevID = currentEntry?.value ?? initialID; + const current = argumentManager.getEntry(prevID); + const [id, setEntry] = argumentManager.request(); + renderToDropdown(getComponent(extensionID, component), { setter: setEntry, current, extension: this as any as BaseGenericExtension }); + return [["Apply", id]]; + } + } + + throw new Error("Error during processing -- Context:" + callingContext); + }; + + } + return ExtensionWithCustomArgumentSupport; +} \ No newline at end of file diff --git a/extensions/src/common/extension/mixins/optional/customSaveData.ts b/extensions/src/common/extension/mixins/optional/customSaveData.ts new file mode 100644 index 000000000..66ba000d4 --- /dev/null +++ b/extensions/src/common/extension/mixins/optional/customSaveData.ts @@ -0,0 +1,97 @@ +import { BaseGenericExtension, NonAbstractConstructor } from "$common/types"; +import { MinimalExtensionConstructor } from "../required"; +import { ExtensionIntanceWithFunctionality } from ".."; + +/** + * WARNING! If you change this key, it will affect already saved projects. + * Do not rename this without first developing a mechanism for searching for previously used keys. + */ +export const saveDataKey = "customSaveDataPerExtension" as const; + +/** + * @summary Utility class to assist in creating a (typesafe) object that, for a given Extension type, handles both: + * - writing out data on save + * - doing something with save data on load + * + * @description This class's constructor takes an object with both an `onSave` and an `onLoad` method + * (and the `onSave`'s return type must match `onLoad`'s argument type) + * @example + * new SaveDataHandler({ + * Extension: MyExtension, + * onSave: () => ({x: 0, y: 3}), + * onLoad: (data) => { + * const sum = data.x + data.y; // do something with saved data + * } + * }) + * @todo Remove the `BaseGenericExtension` Generic Type restraint once Generic Extensions are no longer supported + */ +export class SaveDataHandler, TData> { + constructor(public hooks: { + // @ts-ignore + Extension: NonAbstractConstructor, + onSave: (self: T) => TData, + onLoad: (self: T, data: TData) => void, + }) { } +} + +/** + * Mixin the ability for extensions to save and reload custom data (including any data related to custom arguments) + * @param Ctor + * @returns + * @see https://www.typescriptlang.org/docs/handbook/mixins.html + */ +export default function mixin(Ctor: T) { + abstract class ExtensionWithCustomSaveDataSupport extends Ctor { + /** + * Optional field that can be defined if you need to save custom data for an extension + * (like some extension specific variable, or an API endpoint). + * @example + * class Example extends Extension<..., ...> { + * someValue = 5; + * ... + * saveDataHandler = new SaveDataHandler({ + * Extension: Example, + * // NOTE: The type info for 'instance' could be left off in the line below + * onSave: (instance: Example) => ({ valueToSave: instance.someValue }), + * onLoad: (instance, data) => instance.someValue = data.valueToSave + * }) + * } + * @see Extension.MakeSaveDataHandler + */ + protected saveDataHandler: SaveDataHandler = undefined; + + /** + * Save function called 'internally' by the VM when serializing a project. + * @param toSave + * @param extensionIDs + * @returns + */ + private save(toSave: { [saveDataKey]: Record }, extensionIDs: Set) { + const { saveDataHandler, id } = this; + const argumentManager = this.supports("customArguments") ? this.customArgumentManager : null; + const saveData = saveDataHandler?.hooks.onSave(this) ?? {}; + argumentManager?.saveTo(saveData); + if (Object.keys(saveData).length === 0) return; + const container = toSave[saveDataKey]; + container ? (container[id] = saveData) : (toSave[saveDataKey] = { [id]: saveData }); + extensionIDs.add(id); + } + + /** + * Load function called 'internally' by the VM when loading a project. + * Will be invoked on an extension immediately after it is constructed. + * @param saved + * @returns + */ + private load(saved: { [saveDataKey]: Record }) { + if (!saved) return; + const { saveDataHandler, id } = this; + const saveData = saveDataKey in saved ? saved[saveDataKey][id] : null; + if (!saveData) return; + saveDataHandler?.hooks.onLoad(this, saveData); + + if (this.supports("customArguments")) this.getOrCreateCustomArgumentManager().loadFrom(saveData); + } + } + return ExtensionWithCustomSaveDataSupport; +} \ No newline at end of file diff --git a/extensions/src/common/extension/mixins/optional/drawable.ts b/extensions/src/common/extension/mixins/optional/drawable.ts new file mode 100644 index 000000000..9133cc48b --- /dev/null +++ b/extensions/src/common/extension/mixins/optional/drawable.ts @@ -0,0 +1,112 @@ +import { StageLayering, ValueOf } from "$common/types"; +import { MinimalExtensionConstructor } from "../required"; + +type Handle = number; + +type Renderer = { + /** + * Create a new bitmap skin from a snapshot of the provided bitmap data. + * @param {ImageData|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} bitmapData - new contents for this skin. + * @param {!int} [costumeResolution=1] - The resolution to use for this bitmap. + * @param {?Array} [rotationCenter] Optional: rotation center of the skin. If not supplied, the center of + * the skin will be used. + * @returns {!int} the ID for the new skin. + */ + createBitmapSkin(bitmapData: ImageData | HTMLImageElement | HTMLCanvasElement | HTMLVideoElement, costumeResolution?: number, rotationCenter?: Array | null): Handle; + /** + * Create a new Drawable and add it to the scene. + * @param {string} group Layer group to add the drawable to + * @returns {int} The ID of the new Drawable. + */ + createDrawable(group: string): Handle; + /** + * Update a drawable's visibility. + * @param {number} drawableID The drawable's id. + * @param {boolean} visible Will the drawable be visible? + */ + updateDrawableVisible(drawableID: number, visible: boolean): void; + /** + * Update a drawable's visual effect. + * @param {number} drawableID The drawable's id. + * @param {string} effectName The effect to change. + * @param {number} value A new effect value. + */ + updateDrawableEffect(drawableID: number, effectName: string, value: number): void; + /** + * Update a drawable's skin. + * @param {number} drawableID The drawable's id. + * @param {number} skinId The skin to update to. + */ + updateDrawableSkinId(drawableID: Handle, skinId: Handle): void; + /** + * Update an existing bitmap skin, or create a bitmap skin if the previous skin was not bitmap. + * @param {!int} skinId the ID for the skin to change. + * @param {!ImageData|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} imgData - new contents for this skin. + * @param {!number} bitmapResolution - the resolution scale for a bitmap costume. + * @param {?Array} rotationCenter Optional: rotation center of the skin. If not supplied, the center of the + * skin will be used + */ + updateBitmapSkin(skinId: Handle, imgData: ImageData | HTMLImageElement | HTMLCanvasElement | HTMLVideoElement, bitmapResolution: number, rotationCenter?: Array | null): void; + /** + * Destroy an existing skin. Do not use the skin or its ID after calling this. + * @param {!int} skinId - The ID of the skin to destroy. + */ + destroySkin(skinId: number): void; + /** + * Destroy a Drawable, removing it from the scene. + * @param {int} drawableID The ID of the Drawable to remove. + * @param {string} group Group name that the drawable belongs to + */ + destroyDrawable(drawableID: Handle, group: string): void; +} + +/** + * Mixin the ability for extensions to draw images into the canvas + * @param Ctor + * @returns + * @see https://www.typescriptlang.org/docs/handbook/mixins.html + */ +export default function (Ctor: T) { + abstract class ExtensionWithDrawingSupport extends Ctor { + private renderer: Renderer; + + /** + * Draw an item on screen using image data + * @param {ImageData | ImageBitmap} image + * @returns + */ + protected createDrawable(image: T) { + this.renderer ??= this.runtime.renderer; + const { renderer } = this; + + if (!renderer) return null; + + const skin = renderer.createBitmapSkin(image as ImageData, 1); + const drawable = renderer.createDrawable(StageLayering.VideoLayer); + + renderer.updateDrawableSkinId(drawable, skin); + + const setTransparency = (transparency: number) => + renderer.updateDrawableEffect(drawable, 'ghost', transparency); + + const setVisible = (visible: boolean = true) => + renderer.updateDrawableVisible(drawable, visible); + + const update = (image: ImageData | ImageBitmap) => + renderer.updateBitmapSkin(skin, image as ImageData, 1); + + const destroy = () => { + setVisible(false); + renderer.destroyDrawable(drawable, StageLayering.VideoLayer); + renderer.destroySkin(skin); + } + + setTransparency(0); + setVisible(true); + + return { setTransparency, setVisible, update, destroy } + } + } + + return ExtensionWithDrawingSupport; +} diff --git a/extensions/src/common/extension/mixins/optional/legacySupport.ts b/extensions/src/common/extension/mixins/optional/legacySupport.ts new file mode 100644 index 000000000..a5d2192b9 --- /dev/null +++ b/extensions/src/common/extension/mixins/optional/legacySupport.ts @@ -0,0 +1,161 @@ +import { ExtensionInstance } from "$common/extension"; +import { AbstractConstructor, ExtensionArgumentMetadata, ExtensionBlockMetadata, ExtensionMenuMetadata, ExtensionMetadata } from "$common/types"; +import { isString, set } from "$common/utils"; +import { isDynamicMenu, parseText } from "../../decorators/legacySupport/index"; +import { MinimalExtensionConstructor } from "../required"; +import { getImplementationName, wrapOperation } from "../required/scratchInfo/index"; + +type WrappedOperation = ReturnType; +type WrappedOperationParams = Parameters; +type WithLegacySupport = InstanceType>; +type BlockMap = Map & { index: number }>; + +export const isLegacy = (extension: ExtensionInstance | WithLegacySupport): extension is WithLegacySupport => { + const key: keyof WithLegacySupport = "__isLegacy"; + return key in extension; +} + +const validBlock = (legacyBlock: string | ExtensionBlockMetadata, blockMap: BlockMap): legacyBlock is ExtensionBlockMetadata => { + if (isString(legacyBlock)) throw new Error("Block was unexpectedly a string: " + legacyBlock); + if (!blockMap.has(legacyBlock.opcode)) { + console.error(`Could not find legacy opcode ${legacyBlock.opcode} within currently defined blocks`); + return false; + }; + return true; +} + +const validArg = (pair: { legacy: ExtensionArgumentMetadata, modern: ExtensionArgumentMetadata }): typeof pair => { + if (typeof pair.legacy.menu !== typeof pair.modern.menu) throw new Error("Menus don't match") + return pair; +} + +const getDynamicMenuName = (menu: ExtensionMenuMetadata): string => { + if (isDynamicMenu(menu)) return menu; + if (isDynamicMenu(menu.items)) return menu.items; + throw new Error("Menu is not dynamic: " + menu); +} + +/** + * Mixin the ability for extensions to make use of 'legacy' `getInfo` json, + * so that extensions ported to the framework can support old, serialized projects + * @param Ctor + * @param legacyInfo + * @returns + */ +export default function legacySupportMixin(Ctor: T) { + abstract class ExtensionWithLegacySupport extends Ctor { + private validatedInfo: ExtensionMetadata; + + protected abstract getLegacyInfo(): ExtensionMetadata; + + public __isLegacy = true; + public orderArgumentNamesByBlock: Map = new Map(); + + protected override getInfo(): ExtensionMetadata { + if (!this.validatedInfo) { + const info = super.getInfo(); + this.validatedInfo = this.validateAndAttach(info); + } + + return this.validatedInfo; + } + + private getArgNames = (legacyBlock: ExtensionBlockMetadata) => { + const { opcode } = legacyBlock; + + if (!this.orderArgumentNamesByBlock.has(opcode)) { + const { orderedNames } = parseText(legacyBlock); + this.orderArgumentNamesByBlock.set(opcode, orderedNames); + } + + return this.orderArgumentNamesByBlock.get(opcode); + } + + private validateAndAttach({ id, blocks, menus, ...metaData }: ExtensionMetadata): ExtensionMetadata { + const { id: legacyID, blocks: legacyBlocks, menus: legacyMenus } = this.getLegacyInfo(); + const mutableBlocks = [...blocks as ExtensionBlockMetadata[]]; + + if (id !== legacyID) throw new Error(`ID mismatch! Legacy id: ${legacyID} vs. current id: ${id}`); + + const blockMap = mutableBlocks.reduce( + (map, { opcode, ...block }, index) => map.set(opcode, { ...block, index }), + new Map() as BlockMap + ); + + const self = this; + + const updates = legacyBlocks + .map(legacyBlock => validBlock(legacyBlock, blockMap) ? legacyBlock : undefined) + .filter(Boolean) + .map(legacyBlock => { + const { opcode, arguments: legacyArgs } = legacyBlock; + const { index, arguments: modernArgs } = blockMap.get(opcode); + const argNames = this.getArgNames(legacyBlock); + + if (!argNames) return { replaceAt: { index, block: legacyBlock } }; + + const remapper = (args: Record) => argNames.reduce( + (remap, current, index) => set(remap, index, args[current]), + {} as Record); + + const implementation: WrappedOperation = this[getImplementationName(opcode)]; + + this[opcode] = ( + (...[args, util]: WrappedOperationParams) => implementation.call(self, remapper(args), util) + ).bind(self); + + const menuUpdates = argNames + .map((legacyName, index) => ({ legacy: legacyArgs[legacyName], modern: modernArgs[index] })) + .map(validArg) + .map(({ legacy: { menu: legacyName }, modern: { menu: modernName } }) => ({ legacyName, modernName })) + .filter(menus => menus.legacyName && menus.modernName) + .map(({ legacyName, modernName }) => + ({ legacyName, modernName, legacy: legacyMenus[legacyName], modern: menus[modernName] })) + .map(({ legacy, modern, legacyName, modernName }) => !isDynamicMenu(legacy) && !isDynamicMenu(legacy.items) + ? { type: "static" as const, legacy: legacyName, modern: modernName } + : { type: "dynamic" as const, legacy: legacyName, modern: modernName, methods: { legacy: getDynamicMenuName(legacy), modern: getDynamicMenuName(modern) } } + ); + + return { menuUpdates, replaceAt: { index, block: legacyBlock } }; + }); + + updates.forEach(({ replaceAt: { index, block } }) => mutableBlocks[index] = block); + + updates + .map(({ menuUpdates }) => menuUpdates) + .flat() + .filter(Boolean) + .map(menu => { + const { legacy } = menu; + if (legacy in menus) throw new Error(`Somehow, there was already a menu called ${legacy}, which will cause issues in the next step.`); + return menu; + }) + .forEach(({ type, legacy, methods }) => { + menus[legacy] = legacyMenus[legacy]; + if (type === "dynamic") self[methods.legacy] = () => self[methods.modern](); + }); + + return { + id, blocks: mutableBlocks, menus, ...metaData + }; + } + } + return ExtensionWithLegacySupport +} + +/** + * Mixin the ability for extensions to make use of 'legacy' `getInfo` json, + * so that extensions ported to the framework can support old, serialized projects + * @param Ctor + * @param legacyInfo + * @returns + */ +export function legacySupportWithInfoArgument>(Ctor: T, legacyInfo: ExtensionMetadata) { + abstract class ExtensionWithLegacySupport extends legacySupportMixin(Ctor) { + protected getLegacyInfo() { + return legacyInfo; + } + } + + return ExtensionWithLegacySupport +} \ No newline at end of file diff --git a/extensions/src/common/extension/mixins/optional/ui.ts b/extensions/src/common/extension/mixins/optional/ui.ts new file mode 100644 index 000000000..c83bf54be --- /dev/null +++ b/extensions/src/common/extension/mixins/optional/ui.ts @@ -0,0 +1,27 @@ +import { openUI } from "$common/ui"; +import { MinimalExtensionConstructor } from "../required"; + +/** + * Mixin the ability for extensions to open up UI at-will + * @param Ctor + * @returns + * @see https://www.typescriptlang.org/docs/handbook/mixins.html + */ +export default function (Ctor: T) { + abstract class ExtensionWithUISupport extends Ctor { + + /** + * Open a UI in a modal window + * @param component The name of the svelte component / file to open (which should be stored within the same folder as your extension's `index.ts` file). + * You can optionally leave off the `.svelte` extension. + * @param label What to title the modal window that pops up (defaults to your extension's name if left blank) + */ + openUI(component: string, label?: string) { + const { id, name, runtime } = this; + openUI(runtime, { id, name, component: component.replace(".svelte", ""), label }); + } + + } + + return ExtensionWithUISupport; +} diff --git a/extensions/src/common/extension/mixins/optional/video.ts b/extensions/src/common/extension/mixins/optional/video.ts new file mode 100644 index 000000000..e5ba679b6 --- /dev/null +++ b/extensions/src/common/extension/mixins/optional/video.ts @@ -0,0 +1,73 @@ +import type Video from "$scratch-vm/io/video"; +import { MinimalExtensionConstructor } from "../required"; + +const Format = { + image: "image-data", + canvas: "canvas" +} as const satisfies { + image: (typeof Video)["FORMAT_IMAGE_DATA"], + canvas: (typeof Video)["FORMAT_CANVAS"]; +} + +type VideoFrameTypeByFormat = { + "image-data": ImageData, + "canvas": HTMLCanvasElement +} + +/** + * Mixin the ability for extensions to interact with the user's web cam video feed + * @param Ctor + * @returns + * @see https://www.typescriptlang.org/docs/handbook/mixins.html + */ +export default function (Ctor: T) { + abstract class ExtensionWithVideoSupport extends Ctor { + private videoDevice: Video | null; + + private get video(): Video | null { + this.videoDevice ??= this.runtime.ioDevices?.video; + return this.videoDevice; + }; + + /** + * Dimensions of the video frame + */ + videoDimensions = { width: 480, height: 360 } as const; + + /** + * Access the most recent frame captured by the web cam + * @param {"image" | "canvas"} format + * @returns + */ + getVideoFrame(format: TFormat) { + return this.video?.getFrame({ + format: Format[format] + }) as VideoFrameTypeByFormat[typeof Format[typeof format]] + } + + setVideoTransparency(transparency: number) { + this.video?.setPreviewGhost(transparency); + } + + /** + * Turn the video feed on so that it's frames can be accessed and the feed + * diplays within the game window. + * @param {boolean} mirror defaults to `true` + * @returns + */ + enableVideo(mirror: boolean = true) { + if (!this.video) return; + this.video.enableVideo(); + this.video.provider.mirror = mirror; + } + + /** + * Disable the video feed + */ + disableVideo() { + this.video?.disableVideo(); + } + } + + return ExtensionWithVideoSupport; +} diff --git a/extensions/src/scratch3_jibo/ColorArgument.svelte b/extensions/src/scratch3_jibo/ColorArgument.svelte new file mode 100644 index 000000000..a75f99dd2 --- /dev/null +++ b/extensions/src/scratch3_jibo/ColorArgument.svelte @@ -0,0 +1,93 @@ + + +
+ {#each Object.keys(Color) as color} + + {/each} +
+ + diff --git a/extensions/src/scratch3_jibo/EmojiArgument.svelte b/extensions/src/scratch3_jibo/EmojiArgument.svelte new file mode 100644 index 000000000..e1c2c2003 --- /dev/null +++ b/extensions/src/scratch3_jibo/EmojiArgument.svelte @@ -0,0 +1,108 @@ + + +
+ {#each Object.keys(Emotion) as emotion} + + {/each} +
+ + diff --git a/extensions/src/scratch3_jibo/IconArgument.svelte b/extensions/src/scratch3_jibo/IconArgument.svelte new file mode 100644 index 000000000..eb0e11a97 --- /dev/null +++ b/extensions/src/scratch3_jibo/IconArgument.svelte @@ -0,0 +1,108 @@ + + +
+ {#each Object.keys(Icon) as icon} + + {/each} +
+ + diff --git a/extensions/src/scratch3_jibo/firebase.ts b/extensions/src/scratch3_jibo/firebase.ts new file mode 100644 index 000000000..42d30a277 --- /dev/null +++ b/extensions/src/scratch3_jibo/firebase.ts @@ -0,0 +1,32 @@ +// import firebase from 'firebase/app'; +import 'firebase/compat/database'; +import firebase from 'firebase/compat/app'; + +const config = { + apiKey: "AIzaSyBRRWCIBplurimT9S2h0ikia3zJtH8GGz4", + authDomain: "jibobecoding.firebaseapp.com", + databaseURL: "https://jibobecoding-default-rtdb.firebaseio.com", + projectId: "jibobecoding", + storageBucket: "jibobecoding.appspot.com", + messagingSenderId: "190480712101", + appId: "1:190480712101:web:d2177edff3db7b63f5284c", + measurementId: "G-251EZ4YTJF", +}; + +firebase.initializeApp(config); + +// reference to firebase db +var database = firebase.database(); + +export default database; + +// export function writeData(path: string, jibo_msg: any): Promise { +// return database.ref(path).push({ ...jibo_msg }) +// .catch((error) => { +// console.error('Error:', error); +// throw error; +// }); +// } + + + diff --git a/extensions/src/scratch3_jibo/index.test.ts b/extensions/src/scratch3_jibo/index.test.ts new file mode 100644 index 000000000..e468de757 --- /dev/null +++ b/extensions/src/scratch3_jibo/index.test.ts @@ -0,0 +1,9 @@ +import { createTestSuite } from "$testing"; +import Extension from '.'; + +createTestSuite({ Extension, __dirname }, + { + unitTests: undefined, + integrationTests: undefined + } +); \ No newline at end of file diff --git a/extensions/src/scratch3_jibo/index.ts b/extensions/src/scratch3_jibo/index.ts new file mode 100644 index 000000000..3169e5672 --- /dev/null +++ b/extensions/src/scratch3_jibo/index.ts @@ -0,0 +1,685 @@ +// firebase +import database from './firebase'; + +import { ArgumentType, BlockType } from "$common"; +import { BlockDefinitions, MenuItem } from "$common"; +import { Extension } from "$common"; +import { RuntimeEvent } from "$common"; + +import VirtualJibo from "./virtualJibo/virtualJibo"; +import { Color, ColorType, colorDef } from "./jiboUtils/ColorDef"; +import { Direction, DirType, directionDef } from "./jiboUtils/LookAtDef"; +import { + Dance, DanceType, danceFiles, + Emotion, EmotionType, emotionFiles, + Icon, IconType, iconFiles, + Audio, AudioType, audioFiles +} from "./jiboUtils/AnimDef"; + +/** Import our svelte components */ +import ColorArgUI from "./ColorArgument.svelte"; +import EmojiArgUI from "./EmojiArgument.svelte"; +import IconArgUI from "./IconArgument.svelte"; + +import ROSLIB from "roslib"; +import BlockUtility from '$root/packages/scratch-vm/src/engine/block-utility'; + +const EXTENSION_ID = "jibo"; + +// jibo's name +var jiboName: string = ""; +// var databaseRef = database.ref("Jibo-Name/" + jiboName); + +type Details = { + name: "Jibo", + description: "Program your favorite social robot, Jibo. This extension works with a physical or virtual Jibo.", + iconURL: "jibo_icon.png", + insetIconURL: "jibo_inset_icon.png", + tags: ["Made by PRG"], +}; + +type Blocks = { + JiboButton: () => void; + JiboTTS: (text: string) => void; + JiboAsk: (text: string) => void; + JiboListen: () => any; + JiboEmote: (emotion: string) => void; + JiboIcon: (icon: string) => void; + JiboDance: (dance: string) => void; + JiboAudio: (audio: string) => void; // new audio block + //JiboVolume: (text: string) => void; // new volume block + JiboLED: (color: string) => void; + JiboLook: (dir: string) => void; // (x_angle: string, y_angle: string, z_angle: string) => void; +}; + +var jibo_event = { + // readyForNext: true, + msg_type: "", + // anim_transition: 0, + // attention_mode: 1, + // audio_filename: "", + // do_anim_transition: false, + // do_attention_mode: false, + // do_led: false, + // do_lookat: false, + // do_motion: false, + // do_sound_playback: false, + // do_tts: false, + // do_volume: false, + // led_color: [0, 100, 0], //red, green, blue + // lookat: [0, 0, 0], //x, y, z + // motion: "", + // tts_duration_stretch: 0, + // tts_pitch: 0, + // tts_text: "", + // volume: 0, +}; + +class FirebaseQueue { + async timedFinish(timeoutFn: () => Promise): Promise { + const requests = [ + timeoutFn(), + this.animFinished(), + ]; + return Promise.race(requests); + } + + async ASR_received(): Promise { + return new Promise((resolve, reject) => { + console.log("Waiting to hear from JiboAsrEvent"); + const pathRef = database.ref("Jibo-Name/" + jiboName); + var eventKey: any; + var eventData: any; + pathRef.on("value", (snapshot) => { + // Loop through the child snapshots of JiboAsrResult + snapshot.forEach((childSnapshot) => { + eventKey = childSnapshot.key; + eventData = childSnapshot.val(); + }); + if (eventData.msg_type === "JiboAsrResult") { + pathRef.off(); + // console.log("eventData is: " + JSON.stringify(eventData)); + var transcription = eventData.transcription; + console.log("Jibo heard: " + transcription); + resolve(transcription); + } + }); + }); + } + async animFinished(): Promise { + return new Promise((resolve, reject) => { + console.log("Waiting for default message from database"); + const pathRef = database.ref("Jibo-Name/" + jiboName); + var eventKey: any; + var eventData: any; + pathRef.on("value", (snapshot) => { + // Loop through the child snapshots of JiboAsrResult + snapshot.forEach((childSnapshot) => { + eventKey = childSnapshot.key; + eventData = childSnapshot.val(); + }); + console.log("last event is"); + console.log(eventData); + if (eventData.msg_type === "default") { + pathRef.off(); + resolve(); + } + }); + }); + } + + async pushToFirebase(data: any, awaitFn: () => Promise) { + if (jiboName != "") { + database.ref("Jibo-Name/" + jiboName).push({ ...data }); + await new Promise(r => setTimeout(r, 2000)); // wait a bit before proceeding + await awaitFn(); + } + else { + console.log("No Jibo Name added."); + } + } +} +const queue = new FirebaseQueue(); + +export async function setJiboName(name: string): Promise { + var jiboNameRef = database.ref("Jibo-Name"); + return new Promise((resolve) => { + jiboNameRef + .once("value", (snapshot) => { + localStorage.setItem("prevJiboName", name); + if (snapshot.hasChild(name)) { + console.log("'" + name + "' exists."); + jiboName = name; + resolve(); + } else { + database.ref("Jibo-Name/" + name).push(jibo_event); + jiboName = name; + console.log( + "'" + name + "' did not exist, and has now been created." + ); + resolve(); + } + }); + }); +} + +export default class Scratch3Jibo extends Extension { + ros: any; // TODO + connected: boolean; + rosbridgeIP: string; + jbVolume: string; + asr_out: any; + dances: MenuItem[]; + dirs: MenuItem[]; + audios: MenuItem[]; // new + virtualJibo: VirtualJibo; + + init() { + this.dances = Object.entries(Dance).map(([dance, def]) => ({ + text: Dance[dance], + value: Dance[dance], + })); + this.dirs = Object.entries(Direction).map(([direction]) => ({ + text: Direction[direction], + value: Direction[direction], + })); + this.audios = Object.entries(Audio).map(([audio, def]) => ({ // new + value: Audio[audio], + text: Audio[audio], + })); + this.runtime.registerPeripheralExtension(EXTENSION_ID, this); + this.runtime.connectPeripheral(EXTENSION_ID, 0); + this.runtime.on(RuntimeEvent.PeripheralConnected, this.connect.bind(this)); + + this.ros = null; + this.connected = false; + this.rosbridgeIP = "ws://localhost:9090"; // rosbridgeIP option includes port + this.jbVolume = "60"; + this.asr_out = ""; + + this.RosConnect({ rosIP: "localhost" }); + + this.virtualJibo = new VirtualJibo(); + this.virtualJibo.init(this.runtime); + } + + checkBusy(self: Scratch3Jibo) { + // checking state + var state_listener = new ROSLIB.Topic({ + ros: this.ros, + name: "/jibo_state", + messageType: "jibo_msgs/JiboState", + }); + + state_listener.subscribe(function (message: any) { + state_listener.unsubscribe(); + }); + } + + defineTranslations() { + return undefined; + } + + + + defineBlocks(): BlockDefinitions { + return { + JiboButton: (self: Scratch3Jibo) => ({ + type: BlockType.Button, + arg: { + type: ArgumentType.String, + defaultValue: "Jibo's name here", + }, + text: () => `Connect/Disconnect Jibo`, + operation: async () => { + if (jiboName === "") + this.openUI("jiboNameModal", "Connect Jibo"); + else + jiboName = ""; + }, + }), + JiboTTS: () => ({ + type: BlockType.Command, + arg: { + type: ArgumentType.String, + defaultValue: "Hello, I am Jibo", + }, + text: (text: string) => `say ${text}`, + operation: async (text: string, { target }: BlockUtility) => { + let virtualJ = this.virtualJibo.say(text, target); + let physicalJ = this.jiboTTSFn(text); + await Promise.all([virtualJ, physicalJ]); + } + }), + JiboAsk: () => ({ + type: BlockType.Command, + arg: { + type: ArgumentType.String, + defaultValue: "How are you?", + }, + text: (text: string) => `ask ${text} and wait`, + operation: async (text: string, { target }: BlockUtility) => { + let virtualJ = this.virtualJibo.say(text, target);; + let awaitResponse; + // TODO test + if (jiboName === "") awaitResponse = this.virtualJibo.ask(text); + else awaitResponse = this.jiboAskFn(text); + + await Promise.all([virtualJ, awaitResponse]); + } + }), + JiboListen: () => ({ + type: BlockType.Reporter, + text: `answer`, + operation: () => + this.jiboListenFn(), + }), + // JiboState: () => ({ // helpful for debugging + // type:BlockType.Command, + // text: `read state`, + // operation: () => self.JiboState() + // }), + JiboDance: () => ({ + type: BlockType.Command, + arg: { + type: ArgumentType.String, + options: this.dances, + }, + text: (dname) => `play ${dname} dance`, + operation: async (dance: DanceType) => { + const akey = danceFiles[dance].file; + await this.jiboDanceFn(akey, 5000); + }, + }), + JiboAudio: () => ({ + type: BlockType.Command, + arg: { + type: ArgumentType.String, + options: this.audios, + }, + text: (audioname) => `play ${audioname} audio`, + operation: async (audio: AudioType) => { + const audiokey = audioFiles[audio].file; + await this.jiboAudioFn(audiokey); + }, + }), + /* Jibo block still does not work + // new volume block start + JiboVolume: () => ({ + type: BlockType.Command, + arg: { + type: ArgumentType.String, + defaultValue: "60", + }, + text: (volume: string) => `set volume to ${volume}`, + operation: (volume: string) => + this.jiboVolumeFn(volume), + }), + // new volume block end + */ + JiboEmote: () => ({ + type: BlockType.Command, + arg: this.makeCustomArgument({ + component: EmojiArgUI, + initial: { + value: Emotion.Happy, + text: "Happy", + }, + }), + text: (aname) => `play ${aname} emotion`, + operation: async (anim: EmotionType, { target }: BlockUtility) => { + let virtualJ = this.virtualJibo.anim(anim, "emotion", target); + const akey = emotionFiles[anim].file; + let physicalJ = this.jiboAnimFn(akey, 1000); + await Promise.all([virtualJ, physicalJ]); + }, + }), + JiboIcon: () => ({ + type: BlockType.Command, + arg: this.makeCustomArgument({ + component: IconArgUI, + initial: { + value: Icon.Taco, + text: "taco", + }, + }), + text: (aname) => `show ${aname} icon`, + operation: async (icon: IconType, { target }: BlockUtility) => { + let virtualJ = this.virtualJibo.anim(icon, "icon", target); + const akey = iconFiles[icon].file; + let physicalJ = this.jiboAnimFn(akey, 1000); + await Promise.all([virtualJ, physicalJ]); + } + }), + JiboLED: () => ({ + type: BlockType.Command, + arg: this.makeCustomArgument({ + component: ColorArgUI, + initial: { + value: Color.Blue, + text: "blue", + }, + }), + text: (cname) => `set LED ring to ${cname}`, + operation: async (color: ColorType, { target }: BlockUtility) => { + let virtualJ = this.virtualJibo.setLED(color, target); + let physicalJ = this.jiboLEDFn(color); + await Promise.all([virtualJ, physicalJ]); + } + }), + JiboLook: () => ({ + type: BlockType.Command, + arg: { + type: ArgumentType.String, + options: this.dirs, + }, + text: (dname) => `look ${dname}`, + operation: async (dir: DirType, { target }: BlockUtility) => { + let virtualJ = this.virtualJibo.lookAt(dir, target); + let physicalJ = this.jiboLookFn(dir); + await Promise.all([virtualJ, physicalJ]); + }, + }), + }; + } + + /* The following 4 functions have to exist for the peripherial indicator */ + connect() { + console.log(`Jibo this.connect ${jiboName}`); + this.jiboTTSFn("Hey there. I am ready to program now"); + } + disconnect() { + } + scan() { } + isConnected() { + console.log("isConnected status: " + jiboName); + return !(jiboName === ""); + } + + RosConnect(args: { rosIP: any }) { + const rosIP = args.rosIP.toString(); + this.rosbridgeIP = "ws://" + rosIP + ":9090"; + // log.log("ROS: Attempting to connect to rosbridge at " + this.rosbridgeIP); + + if (!this.connected) { + this.ros = new ROSLIB.Ros({ + url: this.rosbridgeIP, + }); + + // If connection is successful + let connect_cb_factory = function (self: Scratch3Jibo) { + return function () { + self.connected = true; + // send jibo welcome message + let welcomeText = `Hello there. I am ready for you to program me.`; + self.jiboTTSFn(welcomeText); + }; + }; + let connect_cb = connect_cb_factory(this); + this.ros.on("connection", function () { + connect_cb(); + // log.info('ROS: Connected to websocket server.'); + }); + + // If connection fails + let error_cb_factory = function (self: Scratch3Jibo) { + return function () { + self.connected = false; + }; + }; + let error_cb = error_cb_factory(this); + this.ros.on("error", function (error: any) { + error_cb(); + // log.error('ROS: Error connecting to websocket server: ', error); + }); + + // If connection ends + let disconnect_cb_factory = function (self: Scratch3Jibo) { + return function () { + self.connected = false; + }; + }; + let disconnect_cb = disconnect_cb_factory(this); + this.ros.on("close", function () { + disconnect_cb(); + // log.info('ROS: Connection to websocket server closed.'); + }); + } + this.JiboState(); + this.JiboPublish({ + do_attention_mode: true, + attention_mode: 1, + do_anim_transition: true, + anim_transition: 0, + do_led: true, + led_color: { x: 0, y: 0, z: 0 }, + }); + this.JiboASR_receive(); + return this.connected; + } + + async jiboTTSFn(text: string) { + var jibo_msg = { + // readyForNext: false, + msg_type: "JiboAction", + do_tts: true, + tts_text: text, + do_lookat: false, + do_motion: false, + do_sound_playback: false, + volume: parseFloat(this.jbVolume), + }; + + // write to firebase + await queue.pushToFirebase(jibo_msg, queue.animFinished); + + await this.JiboPublish(jibo_msg); + } + + // TODO figure out why Jibo seems to ignore this value + async jiboVolumeFn(volume: string) { + // update Jibo's volume + this.jbVolume = volume; + } + + async jiboAskFn(text: string) { + // say question + await this.jiboTTSFn(text); + // making the ASR request + await this.JiboASR_request(); + + // wait for sensor to return + this.asr_out = await queue.ASR_received(); + } + async jiboListenFn() { + if (jiboName === "") return this.virtualJibo.answer; + return this.asr_out; + } + + async jiboLEDFn(color: string) { + let ledValue = colorDef[color].value; + if (color === "random") { + const randomColorIdx = Math.floor( + // exclude random and off + Math.random() * (Object.keys(colorDef).length - 2) + ); + const randomColor = Object.keys(colorDef)[randomColorIdx]; + ledValue = colorDef[randomColor].value; + } + + // must be "var" does not work with "let" + var jibo_msg = { + // readyForNext: false, + msg_type: "JiboAction", + do_led: true, + led_color: ledValue, + }; + + // write to firebase + var timer = () => new Promise((resolve, reject) => { + setTimeout(resolve, 500); + }); + await queue.pushToFirebase(jibo_msg, + () => queue.timedFinish(timer) + ); // set 500ms time limit on led command + + await this.JiboPublish(jibo_msg); + } + + // there is no message when the look finishes. Just using a set time to finish block + async jiboLookFn(dir: string) { + let coords = directionDef[dir].value; + let jibo_msg = { + do_lookat: true, + lookat: { + x: coords.x, + y: coords.y, + z: coords.z, + }, + }; + + // write to firebase + var timer = () => new Promise((resolve, reject) => { + setTimeout(resolve, 1000); // wait a second for movement to complete + }); + await queue.pushToFirebase(jibo_msg, timer) + + await this.JiboPublish(jibo_msg); + } + + async jiboAnimFn(animation_key: string, delay: number) { + console.log("the animation file is: " + animation_key); // debug statement + var jibo_msg = { + // readyForNext: false, + msg_type: "JiboAction", + do_motion: true, + do_sound_playback: false, + do_tts: false, + do_lookat: false, + motion: animation_key, + }; + + // write to firebase + var timer = (delay) => new Promise((resolve, reject) => { + setTimeout(resolve, delay); // using timer because animFinished does not seem to be reliable + }); + await queue.pushToFirebase(jibo_msg, timer.bind(delay)); // delay before next command + + await this.JiboPublish(jibo_msg); + } + + async jiboDanceFn(animation_key: string, delay: number) { + await this.jiboAnimFn(animation_key, delay); + // transition back to neutral + var timer = () => new Promise((resolve, reject) => { + setTimeout(resolve, 500); + }); + await queue.pushToFirebase({ + do_anim_transition: true, + anim_transition: 0 + }, timer); + + await this.JiboPublish({ do_anim_transition: true, anim_transition: 0 }); + } + + async jiboAudioFn(audio_file: string) { + console.log("the audio file is: " + audio_file); // debug statement + var jibo_msg = { + // readyForNext: false, + msg_type: "JiboAction", + do_motion: false, + do_sound_playback: true, + do_tts: false, + do_lookat: false, + audio_filename: audio_file, + }; + + // write to firebase + await queue.pushToFirebase(jibo_msg, queue.animFinished); + + await this.JiboPublish(jibo_msg); + } + + async JiboPublish(msg: any) { + if (!this.connected) { + console.log("ROS is not connected"); + return false; + } + var cmdVel = new ROSLIB.Topic({ + ros: this.ros, + name: "/jibo", + messageType: "jibo_msgs/JiboAction", + }); + // console.log(msg); + var jibo_msg = new ROSLIB.Message(msg); + cmdVel.publish(jibo_msg); + await new Promise((r) => setTimeout(r, 500)); + } + + JiboState() { + // Subscribing to a Topic + // ---------------------- + + console.log("listening..."); + + var state_listener = new ROSLIB.Topic({ + ros: this.ros, + name: "/jibo_state", + messageType: "jibo_msgs/JiboState", + }); + + state_listener.subscribe(function (message: any) { + console.log("Received message on " + state_listener.name + ": "); + console.log(message); + state_listener.unsubscribe(); + }); + } + async JiboASR_request() { + // if (!this.connected) { + // console.log("ROS is not connetced"); + // return false; + // } + // var cmdVel = new ROSLIB.Topic({ + // ros: this.ros, + // name: "/jibo_asr_command", + // messageType: "jibo_msgs/JiboAsrCommand", + // }); + // var jibo_msg = new ROSLIB.Message({ heyjibo: false, command: 1 }); + var jibo_msg = { + msg_type: "JiboAsrCommand", + command: 1, + heyjibo: false, + detectend: false, + continuous: false, + incremental: false, + alternatives: false, + rule: "", + }; + + var timer = () => new Promise((resolve, reject) => { + setTimeout(resolve, 500); + }); + await queue.pushToFirebase(jibo_msg, timer); // delay a bit before next command + // cmdVel.publish(jibo_msg); + } + + async JiboASR_receive(): Promise { + return new Promise((resolve) => { + var asr_listener = new ROSLIB.Topic({ + ros: this.ros, + name: "/jibo_asr_result", + messageType: "jibo_msgs/JiboAsrResult", + }); + + asr_listener.subscribe(function (message: { transcription: unknown }) { + console.log("Received message on " + asr_listener.name + ": "); + console.log(message); + asr_listener.unsubscribe(); + //this.asr_out = message.transcription; + resolve(message.transcription); + // return readAsrAnswer(message.transcription); + }); + }); + + } +} diff --git a/extensions/src/scratch3_jibo/jiboNameModal.svelte b/extensions/src/scratch3_jibo/jiboNameModal.svelte new file mode 100644 index 000000000..d41def6df --- /dev/null +++ b/extensions/src/scratch3_jibo/jiboNameModal.svelte @@ -0,0 +1,108 @@ + + +
+
Please enter Jibo's name below:
+ + {#if errorVisible} +

+ Jibo's name should be four words separated by dashes. For example: robot-explore-circuit-play. +

+ {/if} + + + +
+ + diff --git a/extensions/src/scratch3_jibo/jiboUtils/AnimDef.ts b/extensions/src/scratch3_jibo/jiboUtils/AnimDef.ts new file mode 100644 index 000000000..f6f90ecba --- /dev/null +++ b/extensions/src/scratch3_jibo/jiboUtils/AnimDef.ts @@ -0,0 +1,255 @@ +type AnimFileType = { + file: string; +}; +export const Dance = { + Celebrate: "Celebrate", + CircuitSaver: "Techno", + HappyDance: "Happy Dance", + SlowDance: "Slow Dance", + RobotDance: "The Robot", + Twerk: "Twerk", + Waltz: "Waltz", + Disco: "Disco", +} as const; +export type DanceType = typeof Dance[keyof typeof Dance]; + +export const danceFiles: Record = { + [Dance.Celebrate]: { + file: "Dances/Celebrate_01.keys", + }, + [Dance.CircuitSaver]: { + file: "Dances/dance_circuit_saver_00.keys", + }, + [Dance.HappyDance]: { + file: "Dances/Happy_Lucky_01_01.keys", + }, + [Dance.SlowDance]: { + file: "Dances/Prom_Night_01_01.keys", + }, + [Dance.RobotDance]: { + file: "Dances/Robotic_01_01.keys", + }, + [Dance.Twerk]: { + file: "Dances/Twerking_01.keys", + }, + [Dance.Waltz]: { + file: "Dances/Waltz_01_01.keys", + }, + [Dance.Disco]: { + file: "Dances/dance_disco_00.keys", + }, +}; + +export const Emotion = { + Frustrated: `Frustrated`, + Laugh: `Laugh`, + Sad: `Sad`, + Thinking: `Thinking`, + Happy: `Happy`, + SadEyes: `SadEyes`, + Curious: `Curious`, + No: `No`, + Yes: `Yes`, + Puzzled: `Puzzled`, + Success: `Success`, +} as const; +export type EmotionType = typeof Emotion[keyof typeof Emotion]; + +export const emotionFiles: Record = { + [Emotion.Frustrated]: { + file: "Misc/Frustrated_01_04.keys", + }, + [Emotion.Laugh]: { + file: "Misc/Laughter_01_03.keys", + }, + [Emotion.Sad]: { + file: "Misc/Sad_03.keys", + }, + [Emotion.Thinking]: { + file: "Misc/thinking_08.keys", + }, + [Emotion.Happy]: { + file: "Misc/Eye_to_Happy_02.keys", + }, + [Emotion.SadEyes]: { + file: "Misc/Eye_Sad_03_02.keys", + }, + [Emotion.Curious]: { + file: "Misc/Question_01_02.keys", + }, + [Emotion.No]: { + file: "Misc/no_4.keys", + }, + [Emotion.Yes]: { + file: "Misc/yep_02.keys", + }, + [Emotion.Puzzled]: { + file: "Misc/puzzled_01_02.keys", + }, + [Emotion.Success]: { + file: "Misc/success_02.keys", + }, +}; + +export const Icon = { + Airplane: `Airplane`, + Apple: `Apple`, + Art: `Art`, + Bowling: `Bowling`, + Checkmark: `Checkmark`, + ExclamationPoint: `ExclamationPoint`, + Football: `Football`, + Heart: `Heart`, + Magic: `Magic`, + Ocean: `Ocean`, + Penguin: `Penguin`, + Rainbow: `Rainbow`, + Robot: `Robot`, + Rocket: `Rocket`, + Snowflake: `Snowflake`, + Taco: `Taco`, + VideoGame: `VideoGame`, +} as const; +export type IconType = typeof Icon[keyof typeof Icon]; + +export const iconFiles: Record = { + [Icon.Airplane]: { + file: "Emoji/Emoji_Airplane_01_01.keys", + }, + [Icon.Apple]: { + file: "Emoji/Emoji_AppleRed_01_01.keys", + }, + [Icon.Art]: { + file: "Emoji/Emoji_Art_01_01.keys", + }, + [Icon.Bowling]: { + file: "Emoji/Emoji_Bowling.keys", + }, + [Icon.Checkmark]: { + file: "Emoji/Emoji_Checkmark_01_01.keys", + }, + [Icon.ExclamationPoint]: { + file: "Emoji/Emoji_ExclamationYellow.keys", + }, + [Icon.Football]: { + file: "Emoji/Emoji_Football_01_01.keys", + }, + [Icon.Heart]: { + file: "Emoji/Emoji_HeartArrow_01_01.keys", + }, + [Icon.Magic]: { + file: "Emoji/Emoji_Magic_01_02.keys", + }, + [Icon.Ocean]: { + file: "Emoji/Emoji_Ocean_01_01.keys", + }, + [Icon.Penguin]: { + file: "Emoji/Emoji_Penguin_01_01.keys", + }, + [Icon.Rainbow]: { + file: "Emoji/Emoji_Rainbow_01_01.keys", + }, + [Icon.Robot]: { + file: "Emoji/Emoji_Robot_01_01.keys", + }, + [Icon.Rocket]: { + file: "Emoji/Emoji_Rocket_01_01.keys", + }, + [Icon.Snowflake]: { + file: "Emoji/Emoji_Snowflake_01_01.keys", + }, + [Icon.Taco]: { + file: "Emoji/Emoji_Taco_01_01.keys", + }, + [Icon.VideoGame]: { + file: "Emoji/Emoji_VideoGame_01_01.keys", + }, +}; + +// new audio files start +export const Audio = { + Bawhoop: "Bawhoop", + Bleep: "Bleep", + Blip: "Blip", + Bloop: "Bloop", + BootUp: "Bubble Up", + DoYouWantToPlay: "Robot Chitter", + FillingUp: "Filling Up", + PowerOn: "Power On", + Holyhappiness: "Totter", + ImBroken: "I'm Broken", + PeekABoo: "Peek-A-Boo", + Whistle: "Whistle", + CheckmarkButton: "Checkmark", + TurnTakingOff: "Off", + TurnTakingOn: "On", + Aww: "Aww", + Confirm: "Confirm", + Disappointed: "Disappointed", + Hello: "Hello", + Belly_Dance_00: "Belly Dance", +} as const; +export type AudioType = typeof Audio[keyof typeof Audio]; + +export const audioFiles: Record = { + [Audio.Bawhoop]: { + file: "FX_Bawhoop.mp3", + }, + [Audio.Bleep]: { + file: "FX_Bleep.mp3", + }, + [Audio.Blip]: { + file: "FX_Blip.mp3", + }, + [Audio.Bloop]: { + file: "FX_Bloop.mp3", + }, + [Audio.BootUp]: { + file: "FX_BootUp.mp3", + }, + [Audio.DoYouWantToPlay]: { + file: "FX_DoYouWantToPlay_01.mp3", + }, + [Audio.FillingUp]: { + file: "FX_FillingUp_01.mp3", + }, + [Audio.PowerOn]: { + file: "FX_GoodJob_01.mp3", + }, + [Audio.Holyhappiness]: { + file: "FX_Holyhappiness.mp3", + }, + [Audio.ImBroken]: { + file: "FX_ImBroken_01.mp3", + }, + [Audio.PeekABoo]: { + file: "FX_PeekABoo_01.mp3", + }, + [Audio.Whistle]: { + file: "FX_Whistle.mp3", + }, + [Audio.CheckmarkButton]: { + file: "SFX_Global_CheckmarkButton.m4a", + }, + [Audio.TurnTakingOff]: { + file: "SFX_Global_TurnTakingOff.m4a", + }, + [Audio.TurnTakingOn]: { + file: "SFX_Global_TurnTakingOn.m4a", + }, + [Audio.Aww]: { + file: "SSA_aww.m4a", + }, + [Audio.Confirm]: { + file: "SSA_confirm.m4a", + }, + [Audio.Disappointed]: { + file: "SSA_disappointed.m4a", + }, + [Audio.Hello]: { + file: "SSA_hello.wav", + }, + [Audio.Belly_Dance_00]: { + file: "music/music_belly_dance_00.m4a", + }, +}; \ No newline at end of file diff --git a/extensions/src/scratch3_jibo/jiboUtils/ColorDef.ts b/extensions/src/scratch3_jibo/jiboUtils/ColorDef.ts new file mode 100644 index 000000000..527b9a312 --- /dev/null +++ b/extensions/src/scratch3_jibo/jiboUtils/ColorDef.ts @@ -0,0 +1,51 @@ +type RGB = { + x: number; + y: number; + z: number; +}; + +export const Color = { + Red: "red", + Yellow: "yellow", + Green: "green", + Cyan: "cyan", + Blue: "blue", + Magenta: "magenta", + White: "white", + Random: "random", + Off: "off", +} as const; +export type ColorType = typeof Color[keyof typeof Color]; +type ColorDefType = { + value: RGB; +}; + +export const colorDef: Record = { + [Color.Red]: { + value: { x: 255, y: 0, z: 0 }, + }, + [Color.Yellow]: { + value: { x: 255, y: 69, z: 0 }, + }, + [Color.Green]: { + value: { x: 0, y: 167, z: 0 }, + }, + [Color.Cyan]: { + value: { x: 0, y: 167, z: 48 }, + }, + [Color.Blue]: { + value: { x: 0, y: 0, z: 255 }, + }, + [Color.Magenta]: { + value: { x: 255, y: 0, z: 163 }, + }, + [Color.White]: { + value: { x: 255, y: 255, z: 255 }, + }, + [Color.Random]: { + value: { x: -1, y: -1, z: -1 }, + }, + [Color.Off]: { + value: { x: 0, y: 0, z: 0 }, + }, +}; \ No newline at end of file diff --git a/extensions/src/scratch3_jibo/jiboUtils/LookAtDef.ts b/extensions/src/scratch3_jibo/jiboUtils/LookAtDef.ts new file mode 100644 index 000000000..68e504463 --- /dev/null +++ b/extensions/src/scratch3_jibo/jiboUtils/LookAtDef.ts @@ -0,0 +1,38 @@ + +type Coords = { + x: number; + y: number; + z: number; +}; +export const Direction = { + up: `up`, + down: `down`, + right: `right`, + left: `left`, + forward: `forward`, + //backward: `backward`, +} as const; +export type DirType = typeof Direction[keyof typeof Direction]; +type DirDefType = { + value: Coords; +}; +export const directionDef: Record = { + [Direction.up]: { + value: { x: 500, y: 100, z: 500 }, + }, + [Direction.down]: { + value: { x: 500, y: 100, z: -500 }, + }, + [Direction.left]: { + value: { x: 100, y: 500, z: 100 }, + }, + [Direction.right]: { + value: { x: 100, y: -500, z: 100 }, + }, + [Direction.forward]: { + value: { x: 500, y: 100, z: 100 }, + }, + /*[Direction.backward]: { + value: { x: -500, y: 100, z: 100 }, + },*/ +}; \ No newline at end of file diff --git a/extensions/src/scratch3_jibo/jibo_icon.png b/extensions/src/scratch3_jibo/jibo_icon.png new file mode 100644 index 000000000..c727764a4 Binary files /dev/null and b/extensions/src/scratch3_jibo/jibo_icon.png differ diff --git a/extensions/src/scratch3_jibo/jibo_inset_icon.png b/extensions/src/scratch3_jibo/jibo_inset_icon.png new file mode 100644 index 000000000..7733e4c57 Binary files /dev/null and b/extensions/src/scratch3_jibo/jibo_inset_icon.png differ diff --git a/extensions/src/scratch3_jibo/package.json b/extensions/src/scratch3_jibo/package.json new file mode 100644 index 000000000..de26891ee --- /dev/null +++ b/extensions/src/scratch3_jibo/package.json @@ -0,0 +1,20 @@ +{ + "name": "scratch3_jibo-extension", + "version": "1.0.0", + "description": "An extension created using the PRG AI Blocks framework", + "main": "index.ts", + "scripts": { + "directory": "echo scratch3_jibo", + "dev": "npm run dev --prefix ../../../ -- only=scratch3_jibo", + "test": "npm run test --prefix ../../ -- scratch3_jibo/index.test.ts" + }, + "author": "", + "license": "ISC", + "dependencies": { + "firebase": "^9.22.2", + "roslib": "^1.3.0" + }, + "devDependencies": { + "@types/roslib": "^1.3.0" + } +} diff --git a/extensions/src/scratch3_jibo/pnpm-lock.yaml b/extensions/src/scratch3_jibo/pnpm-lock.yaml new file mode 100644 index 000000000..6db568c10 --- /dev/null +++ b/extensions/src/scratch3_jibo/pnpm-lock.yaml @@ -0,0 +1,1181 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + firebase: + specifier: ^9.22.2 + version: 9.23.0 + roslib: + specifier: ^1.3.0 + version: 1.4.1 + devDependencies: + '@types/roslib': + specifier: ^1.3.0 + version: 1.3.5 + +packages: + + '@firebase/analytics-compat@0.2.6': + resolution: {integrity: sha512-4MqpVLFkGK7NJf/5wPEEP7ePBJatwYpyjgJ+wQHQGHfzaCDgntOnl9rL2vbVGGKCnRqWtZDIWhctB86UWXaX2Q==} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/analytics-types@0.8.0': + resolution: {integrity: sha512-iRP+QKI2+oz3UAh4nPEq14CsEjrjD6a5+fuypjScisAh9kXKFvdJOZJDwk7kikLvWVLGEs9+kIUS4LPQV7VZVw==} + + '@firebase/analytics@0.10.0': + resolution: {integrity: sha512-Locv8gAqx0e+GX/0SI3dzmBY5e9kjVDtD+3zCFLJ0tH2hJwuCAiL+5WkHuxKj92rqQj/rvkBUCfA1ewlX2hehg==} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/app-check-compat@0.3.7': + resolution: {integrity: sha512-cW682AxsyP1G+Z0/P7pO/WT2CzYlNxoNe5QejVarW2o5ZxeWSSPAiVEwpEpQR/bUlUmdeWThYTMvBWaopdBsqw==} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/app-check-interop-types@0.3.0': + resolution: {integrity: sha512-xAxHPZPIgFXnI+vb4sbBjZcde7ZluzPPaSK7Lx3/nmuVk4TjZvnL8ONnkd4ERQKL8WePQySU+pRcWkh8rDf5Sg==} + + '@firebase/app-check-types@0.5.0': + resolution: {integrity: sha512-uwSUj32Mlubybw7tedRzR24RP8M8JUVR3NPiMk3/Z4bCmgEKTlQBwMXrehDAZ2wF+TsBq0SN1c6ema71U/JPyQ==} + + '@firebase/app-check@0.8.0': + resolution: {integrity: sha512-dRDnhkcaC2FspMiRK/Vbp+PfsOAEP6ZElGm9iGFJ9fDqHoPs0HOPn7dwpJ51lCFi1+2/7n5pRPGhqF/F03I97g==} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/app-compat@0.2.13': + resolution: {integrity: sha512-j6ANZaWjeVy5zg6X7uiqh6lM6o3n3LD1+/SJFNs9V781xyryyZWXe+tmnWNWPkP086QfJoNkWN9pMQRqSG4vMg==} + + '@firebase/app-types@0.9.0': + resolution: {integrity: sha512-AeweANOIo0Mb8GiYm3xhTEBVCmPwTYAu9Hcd2qSkLuga/6+j9b1Jskl5bpiSQWy9eJ/j5pavxj6eYogmnuzm+Q==} + + '@firebase/app@0.9.13': + resolution: {integrity: sha512-GfiI1JxJ7ecluEmDjPzseRXk/PX31hS7+tjgBopL7XjB2hLUdR+0FTMXy2Q3/hXezypDvU6or7gVFizDESrkXw==} + + '@firebase/auth-compat@0.4.2': + resolution: {integrity: sha512-Q30e77DWXFmXEt5dg5JbqEDpjw9y3/PcP9LslDPR7fARmAOTIY9MM6HXzm9KC+dlrKH/+p6l8g9ifJiam9mc4A==} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/auth-interop-types@0.2.1': + resolution: {integrity: sha512-VOaGzKp65MY6P5FI84TfYKBXEPi6LmOCSMMzys6o2BN2LOsqy7pCuZCup7NYnfbk5OkkQKzvIfHOzTm0UDpkyg==} + + '@firebase/auth-types@0.12.0': + resolution: {integrity: sha512-pPwaZt+SPOshK8xNoiQlK5XIrS97kFYc3Rc7xmy373QsOJ9MmqXxLaYssP5Kcds4wd2qK//amx/c+A8O2fVeZA==} + peerDependencies: + '@firebase/app-types': 0.x + '@firebase/util': 1.x + + '@firebase/auth@0.23.2': + resolution: {integrity: sha512-dM9iJ0R6tI1JczuGSxXmQbXAgtYie0K4WvKcuyuSTCu9V8eEDiz4tfa1sO3txsfvwg7nOY3AjoCyMYEdqZ8hdg==} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/component@0.6.4': + resolution: {integrity: sha512-rLMyrXuO9jcAUCaQXCMjCMUsWrba5fzHlNK24xz5j2W6A/SRmK8mZJ/hn7V0fViLbxC0lPMtrK1eYzk6Fg03jA==} + + '@firebase/database-compat@0.3.4': + resolution: {integrity: sha512-kuAW+l+sLMUKBThnvxvUZ+Q1ZrF/vFJ58iUY9kAcbX48U03nVzIF6Tmkf0p3WVQwMqiXguSgtOPIB6ZCeF+5Gg==} + + '@firebase/database-types@0.10.4': + resolution: {integrity: sha512-dPySn0vJ/89ZeBac70T+2tWWPiJXWbmRygYv0smT5TfE3hDrQ09eKMF3Y+vMlTdrMWq7mUdYW5REWPSGH4kAZQ==} + + '@firebase/database@0.14.4': + resolution: {integrity: sha512-+Ea/IKGwh42jwdjCyzTmeZeLM3oy1h0mFPsTy6OqCWzcu/KFqRAr5Tt1HRCOBlNOdbh84JPZC47WLU18n2VbxQ==} + + '@firebase/firestore-compat@0.3.12': + resolution: {integrity: sha512-mazuNGAx5Kt9Nph0pm6ULJFp/+j7GSsx+Ncw1GrnKl+ft1CQ4q2LcUssXnjqkX2Ry0fNGqUzC1mfIUrk9bYtjQ==} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/firestore-types@2.5.1': + resolution: {integrity: sha512-xG0CA6EMfYo8YeUxC8FeDzf6W3FX1cLlcAGBYV6Cku12sZRI81oWcu61RSKM66K6kUENP+78Qm8mvroBcm1whw==} + peerDependencies: + '@firebase/app-types': 0.x + '@firebase/util': 1.x + + '@firebase/firestore@3.13.0': + resolution: {integrity: sha512-NwcnU+madJXQ4fbLkGx1bWvL612IJN/qO6bZ6dlPmyf7QRyu5azUosijdAN675r+bOOJxMtP1Bv981bHBXAbUg==} + engines: {node: '>=10.10.0'} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/functions-compat@0.3.5': + resolution: {integrity: sha512-uD4jwgwVqdWf6uc3NRKF8cSZ0JwGqSlyhPgackyUPe+GAtnERpS4+Vr66g0b3Gge0ezG4iyHo/EXW/Hjx7QhHw==} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/functions-types@0.6.0': + resolution: {integrity: sha512-hfEw5VJtgWXIRf92ImLkgENqpL6IWpYaXVYiRkFY1jJ9+6tIhWM7IzzwbevwIIud/jaxKVdRzD7QBWfPmkwCYw==} + + '@firebase/functions@0.10.0': + resolution: {integrity: sha512-2U+fMNxTYhtwSpkkR6WbBcuNMOVaI7MaH3cZ6UAeNfj7AgEwHwMIFLPpC13YNZhno219F0lfxzTAA0N62ndWzA==} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/installations-compat@0.2.4': + resolution: {integrity: sha512-LI9dYjp0aT9Njkn9U4JRrDqQ6KXeAmFbRC0E7jI7+hxl5YmRWysq5qgQl22hcWpTk+cm3es66d/apoDU/A9n6Q==} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/installations-types@0.5.0': + resolution: {integrity: sha512-9DP+RGfzoI2jH7gY4SlzqvZ+hr7gYzPODrbzVD82Y12kScZ6ZpRg/i3j6rleto8vTFC8n6Len4560FnV1w2IRg==} + peerDependencies: + '@firebase/app-types': 0.x + + '@firebase/installations@0.6.4': + resolution: {integrity: sha512-u5y88rtsp7NYkCHC3ElbFBrPtieUybZluXyzl7+4BsIz4sqb4vSAuwHEUgCgCeaQhvsnxDEU6icly8U9zsJigA==} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/logger@0.4.0': + resolution: {integrity: sha512-eRKSeykumZ5+cJPdxxJRgAC3G5NknY2GwEbKfymdnXtnT0Ucm4pspfR6GT4MUQEDuJwRVbVcSx85kgJulMoFFA==} + + '@firebase/messaging-compat@0.2.4': + resolution: {integrity: sha512-lyFjeUhIsPRYDPNIkYX1LcZMpoVbBWXX4rPl7c/rqc7G+EUea7IEtSt4MxTvh6fDfPuzLn7+FZADfscC+tNMfg==} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/messaging-interop-types@0.2.0': + resolution: {integrity: sha512-ujA8dcRuVeBixGR9CtegfpU4YmZf3Lt7QYkcj693FFannwNuZgfAYaTmbJ40dtjB81SAu6tbFPL9YLNT15KmOQ==} + + '@firebase/messaging@0.12.4': + resolution: {integrity: sha512-6JLZct6zUaex4g7HI3QbzeUrg9xcnmDAPTWpkoMpd/GoSVWH98zDoWXMGrcvHeCAIsLpFMe4MPoZkJbrPhaASw==} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/performance-compat@0.2.4': + resolution: {integrity: sha512-nnHUb8uP9G8islzcld/k6Bg5RhX62VpbAb/Anj7IXs/hp32Eb2LqFPZK4sy3pKkBUO5wcrlRWQa6wKOxqlUqsg==} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/performance-types@0.2.0': + resolution: {integrity: sha512-kYrbr8e/CYr1KLrLYZZt2noNnf+pRwDq2KK9Au9jHrBMnb0/C9X9yWSXmZkFt4UIdsQknBq8uBB7fsybZdOBTA==} + + '@firebase/performance@0.6.4': + resolution: {integrity: sha512-HfTn/bd8mfy/61vEqaBelNiNnvAbUtME2S25A67Nb34zVuCSCRIX4SseXY6zBnOFj3oLisaEqhVcJmVPAej67g==} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/remote-config-compat@0.2.4': + resolution: {integrity: sha512-FKiki53jZirrDFkBHglB3C07j5wBpitAaj8kLME6g8Mx+aq7u9P7qfmuSRytiOItADhWUj7O1JIv7n9q87SuwA==} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/remote-config-types@0.3.0': + resolution: {integrity: sha512-RtEH4vdcbXZuZWRZbIRmQVBNsE7VDQpet2qFvq6vwKLBIQRQR5Kh58M4ok3A3US8Sr3rubYnaGqZSurCwI8uMA==} + + '@firebase/remote-config@0.4.4': + resolution: {integrity: sha512-x1ioTHGX8ZwDSTOVp8PBLv2/wfwKzb4pxi0gFezS5GCJwbLlloUH4YYZHHS83IPxnua8b6l0IXUaWd0RgbWwzQ==} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/storage-compat@0.3.2': + resolution: {integrity: sha512-wvsXlLa9DVOMQJckbDNhXKKxRNNewyUhhbXev3t8kSgoCotd1v3MmqhKKz93ePhDnhHnDs7bYHy+Qa8dRY6BXw==} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/storage-types@0.8.0': + resolution: {integrity: sha512-isRHcGrTs9kITJC0AVehHfpraWFui39MPaU7Eo8QfWlqW7YPymBmRgjDrlOgFdURh6Cdeg07zmkLP5tzTKRSpg==} + peerDependencies: + '@firebase/app-types': 0.x + '@firebase/util': 1.x + + '@firebase/storage@0.11.2': + resolution: {integrity: sha512-CtvoFaBI4hGXlXbaCHf8humajkbXhs39Nbh6MbNxtwJiCqxPy9iH3D3CCfXAvP0QvAAwmJUTK3+z9a++Kc4nkA==} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/util@1.9.3': + resolution: {integrity: sha512-DY02CRhOZwpzO36fHpuVysz6JZrscPiBXD0fXp6qSrL9oNOx5KWICKdR95C0lSITzxp0TZosVyHqzatE8JbcjA==} + + '@firebase/webchannel-wrapper@0.10.1': + resolution: {integrity: sha512-Dq5rYfEpdeel0bLVN+nfD1VWmzCkK+pJbSjIawGE+RY4+NIJqhbUDDQjvV0NUK84fMfwxvtFoCtEe70HfZjFcw==} + + '@grpc/grpc-js@1.7.3': + resolution: {integrity: sha512-H9l79u4kJ2PVSxUNA08HMYAnUBLj9v6KjYQ7SQ71hOZcEXhShE/y5iQCesP8+6/Ik/7i2O0a10bPquIcYfufog==} + engines: {node: ^8.13.0 || >=10.10.0} + + '@grpc/proto-loader@0.6.13': + resolution: {integrity: sha512-FjxPYDRTn6Ec3V0arm1FtSpmP6V50wuph2yILpyvTKzjc76oDdoihXqM1DzOW5ubvCC8GivfCnNtfaRE8myJ7g==} + engines: {node: '>=6'} + hasBin: true + + '@grpc/proto-loader@0.7.13': + resolution: {integrity: sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==} + engines: {node: '>=6'} + hasBin: true + + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.4': + resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} + + '@protobufjs/eventemitter@1.1.0': + resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} + + '@protobufjs/fetch@1.1.0': + resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/inquire@1.1.0': + resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.0': + resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + + '@socket.io/component-emitter@3.1.2': + resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} + + '@types/cors@2.8.17': + resolution: {integrity: sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==} + + '@types/long@4.0.2': + resolution: {integrity: sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==} + + '@types/node@22.13.2': + resolution: {integrity: sha512-Z+r8y3XL9ZpI2EY52YYygAFmo2/oWfNSj4BCpAXE2McAexDk8VcnBMGC9Djn9gTKt4d2T/hhXqmPzo4hfIXtTg==} + + '@types/roslib@1.3.5': + resolution: {integrity: sha512-rye0xL6oZQFUaC79PXpM6zhYflpHuMTiEdEYkra5psBbTQ+m049UKMXzBFci8UgptULG+CB86wJBjD9q3WB5rw==} + + '@xmldom/xmldom@0.8.10': + resolution: {integrity: sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==} + engines: {node: '>=10.0.0'} + + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + base64id@2.0.0: + resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==} + engines: {node: ^4.5.0 || >= 5.9} + + cbor-js@0.1.0: + resolution: {integrity: sha512-7sQ/TvDZPl7csT1Sif9G0+MA0I0JOVah8+wWlJVQdVEgIbCzlN/ab3x+uvMNsc34TUvO6osQTAmB2ls80JX6tw==} + + cliui@7.0.4: + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + cors@2.8.5: + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + engines: {node: '>= 0.10'} + + debug@4.3.7: + resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + engine.io-parser@5.2.3: + resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==} + engines: {node: '>=10.0.0'} + + engine.io@6.6.4: + resolution: {integrity: sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==} + engines: {node: '>=10.2.0'} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + eventemitter2@6.4.9: + resolution: {integrity: sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==} + + faye-websocket@0.11.4: + resolution: {integrity: sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==} + engines: {node: '>=0.8.0'} + + firebase@9.23.0: + resolution: {integrity: sha512-/4lUVY0lUvBDIaeY1q6dUYhS8Sd18Qb9CgWkPZICUo9IXpJNCEagfNZXBBFCkMTTN5L5gx2Hjr27y21a9NzUcA==} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + http-parser-js@0.5.9: + resolution: {integrity: sha512-n1XsPy3rXVxlqxVioEWdC+0+M+SQw0DpJynwtOPo1X+ZlvdzTLtDBIJJlDQTnwZIFJrZSzSGmIOUdP8tu+SgLw==} + + idb@7.0.1: + resolution: {integrity: sha512-UUxlE7vGWK5RfB/fDwEGgRf84DY/ieqNha6msMV99UsEMQhJ1RwbCd8AYBj3QMgnE3VZnfQvm4oKVCJTYlqIgg==} + + idb@7.1.1: + resolution: {integrity: sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + + long@4.0.0: + resolution: {integrity: sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==} + + long@5.3.0: + resolution: {integrity: sha512-5vvY5yF1zF/kXk+L94FRiTDa1Znom46UjPCH6/XbSvS8zBKMFBHTJk8KDMqJ+2J6QezQFi7k1k8v21ClJYHPaw==} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + + node-fetch@2.6.7: + resolution: {integrity: sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + pngparse@2.0.1: + resolution: {integrity: sha512-RyB1P0BBwt3CNIZ5wT53lR1dT3CUtopnMOuP8xZdHjPhI/uXNNRnkx1yQb/3MMMyyMeo6p19fiIRHcLopWIkxA==} + + protobufjs@6.11.4: + resolution: {integrity: sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw==} + hasBin: true + + protobufjs@7.4.0: + resolution: {integrity: sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==} + engines: {node: '>=12.0.0'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + roslib@1.4.1: + resolution: {integrity: sha512-l3BOHqG99RHb73XROykj8o2rRaUqqYwN0E6C1EkH+R1GIfDjMaUGPaCNEoKKmsXT0Vu0EOyL1BudQtdVlMsgjA==} + engines: {node: '>=0.10'} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + socket.io-adapter@2.5.5: + resolution: {integrity: sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==} + + socket.io-parser@4.2.4: + resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==} + engines: {node: '>=10.0.0'} + + socket.io@4.8.1: + resolution: {integrity: sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==} + engines: {node: '>=10.2.0'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + undici-types@6.20.0: + resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + websocket-driver@0.7.4: + resolution: {integrity: sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==} + engines: {node: '>=0.8.0'} + + websocket-extensions@0.1.4: + resolution: {integrity: sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==} + engines: {node: '>=0.8.0'} + + webworkify-webpack@2.1.5: + resolution: {integrity: sha512-2akF8FIyUvbiBBdD+RoHpoTbHMQF2HwjcxfDvgztAX5YwbZNyrtfUMgvfgFVsgDhDPVTlkbb5vyasqDHfIDPQw==} + + webworkify@1.5.0: + resolution: {integrity: sha512-AMcUeyXAhbACL8S2hqqdqOLqvJ8ylmIbNwUIqQujRSouf4+eUFaXbG6F1Rbu+srlJMmxQWsiU7mOJi0nMBfM1g==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + ws@8.17.1: + resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + ws@8.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yargs-parser@20.2.9: + resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} + engines: {node: '>=10'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@16.2.0: + resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} + engines: {node: '>=10'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + +snapshots: + + '@firebase/analytics-compat@0.2.6(@firebase/app-compat@0.2.13)(@firebase/app@0.9.13)': + dependencies: + '@firebase/analytics': 0.10.0(@firebase/app@0.9.13) + '@firebase/analytics-types': 0.8.0 + '@firebase/app-compat': 0.2.13 + '@firebase/component': 0.6.4 + '@firebase/util': 1.9.3 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + + '@firebase/analytics-types@0.8.0': {} + + '@firebase/analytics@0.10.0(@firebase/app@0.9.13)': + dependencies: + '@firebase/app': 0.9.13 + '@firebase/component': 0.6.4 + '@firebase/installations': 0.6.4(@firebase/app@0.9.13) + '@firebase/logger': 0.4.0 + '@firebase/util': 1.9.3 + tslib: 2.8.1 + + '@firebase/app-check-compat@0.3.7(@firebase/app-compat@0.2.13)(@firebase/app@0.9.13)': + dependencies: + '@firebase/app-check': 0.8.0(@firebase/app@0.9.13) + '@firebase/app-check-types': 0.5.0 + '@firebase/app-compat': 0.2.13 + '@firebase/component': 0.6.4 + '@firebase/logger': 0.4.0 + '@firebase/util': 1.9.3 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + + '@firebase/app-check-interop-types@0.3.0': {} + + '@firebase/app-check-types@0.5.0': {} + + '@firebase/app-check@0.8.0(@firebase/app@0.9.13)': + dependencies: + '@firebase/app': 0.9.13 + '@firebase/component': 0.6.4 + '@firebase/logger': 0.4.0 + '@firebase/util': 1.9.3 + tslib: 2.8.1 + + '@firebase/app-compat@0.2.13': + dependencies: + '@firebase/app': 0.9.13 + '@firebase/component': 0.6.4 + '@firebase/logger': 0.4.0 + '@firebase/util': 1.9.3 + tslib: 2.8.1 + + '@firebase/app-types@0.9.0': {} + + '@firebase/app@0.9.13': + dependencies: + '@firebase/component': 0.6.4 + '@firebase/logger': 0.4.0 + '@firebase/util': 1.9.3 + idb: 7.1.1 + tslib: 2.8.1 + + '@firebase/auth-compat@0.4.2(@firebase/app-compat@0.2.13)(@firebase/app-types@0.9.0)(@firebase/app@0.9.13)': + dependencies: + '@firebase/app-compat': 0.2.13 + '@firebase/auth': 0.23.2(@firebase/app@0.9.13) + '@firebase/auth-types': 0.12.0(@firebase/app-types@0.9.0)(@firebase/util@1.9.3) + '@firebase/component': 0.6.4 + '@firebase/util': 1.9.3 + node-fetch: 2.6.7 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + - '@firebase/app-types' + - encoding + + '@firebase/auth-interop-types@0.2.1': {} + + '@firebase/auth-types@0.12.0(@firebase/app-types@0.9.0)(@firebase/util@1.9.3)': + dependencies: + '@firebase/app-types': 0.9.0 + '@firebase/util': 1.9.3 + + '@firebase/auth@0.23.2(@firebase/app@0.9.13)': + dependencies: + '@firebase/app': 0.9.13 + '@firebase/component': 0.6.4 + '@firebase/logger': 0.4.0 + '@firebase/util': 1.9.3 + node-fetch: 2.6.7 + tslib: 2.8.1 + transitivePeerDependencies: + - encoding + + '@firebase/component@0.6.4': + dependencies: + '@firebase/util': 1.9.3 + tslib: 2.8.1 + + '@firebase/database-compat@0.3.4': + dependencies: + '@firebase/component': 0.6.4 + '@firebase/database': 0.14.4 + '@firebase/database-types': 0.10.4 + '@firebase/logger': 0.4.0 + '@firebase/util': 1.9.3 + tslib: 2.8.1 + + '@firebase/database-types@0.10.4': + dependencies: + '@firebase/app-types': 0.9.0 + '@firebase/util': 1.9.3 + + '@firebase/database@0.14.4': + dependencies: + '@firebase/auth-interop-types': 0.2.1 + '@firebase/component': 0.6.4 + '@firebase/logger': 0.4.0 + '@firebase/util': 1.9.3 + faye-websocket: 0.11.4 + tslib: 2.8.1 + + '@firebase/firestore-compat@0.3.12(@firebase/app-compat@0.2.13)(@firebase/app-types@0.9.0)(@firebase/app@0.9.13)': + dependencies: + '@firebase/app-compat': 0.2.13 + '@firebase/component': 0.6.4 + '@firebase/firestore': 3.13.0(@firebase/app@0.9.13) + '@firebase/firestore-types': 2.5.1(@firebase/app-types@0.9.0)(@firebase/util@1.9.3) + '@firebase/util': 1.9.3 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + - '@firebase/app-types' + - encoding + + '@firebase/firestore-types@2.5.1(@firebase/app-types@0.9.0)(@firebase/util@1.9.3)': + dependencies: + '@firebase/app-types': 0.9.0 + '@firebase/util': 1.9.3 + + '@firebase/firestore@3.13.0(@firebase/app@0.9.13)': + dependencies: + '@firebase/app': 0.9.13 + '@firebase/component': 0.6.4 + '@firebase/logger': 0.4.0 + '@firebase/util': 1.9.3 + '@firebase/webchannel-wrapper': 0.10.1 + '@grpc/grpc-js': 1.7.3 + '@grpc/proto-loader': 0.6.13 + node-fetch: 2.6.7 + tslib: 2.8.1 + transitivePeerDependencies: + - encoding + + '@firebase/functions-compat@0.3.5(@firebase/app-compat@0.2.13)(@firebase/app@0.9.13)': + dependencies: + '@firebase/app-compat': 0.2.13 + '@firebase/component': 0.6.4 + '@firebase/functions': 0.10.0(@firebase/app@0.9.13) + '@firebase/functions-types': 0.6.0 + '@firebase/util': 1.9.3 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + - encoding + + '@firebase/functions-types@0.6.0': {} + + '@firebase/functions@0.10.0(@firebase/app@0.9.13)': + dependencies: + '@firebase/app': 0.9.13 + '@firebase/app-check-interop-types': 0.3.0 + '@firebase/auth-interop-types': 0.2.1 + '@firebase/component': 0.6.4 + '@firebase/messaging-interop-types': 0.2.0 + '@firebase/util': 1.9.3 + node-fetch: 2.6.7 + tslib: 2.8.1 + transitivePeerDependencies: + - encoding + + '@firebase/installations-compat@0.2.4(@firebase/app-compat@0.2.13)(@firebase/app-types@0.9.0)(@firebase/app@0.9.13)': + dependencies: + '@firebase/app-compat': 0.2.13 + '@firebase/component': 0.6.4 + '@firebase/installations': 0.6.4(@firebase/app@0.9.13) + '@firebase/installations-types': 0.5.0(@firebase/app-types@0.9.0) + '@firebase/util': 1.9.3 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + - '@firebase/app-types' + + '@firebase/installations-types@0.5.0(@firebase/app-types@0.9.0)': + dependencies: + '@firebase/app-types': 0.9.0 + + '@firebase/installations@0.6.4(@firebase/app@0.9.13)': + dependencies: + '@firebase/app': 0.9.13 + '@firebase/component': 0.6.4 + '@firebase/util': 1.9.3 + idb: 7.0.1 + tslib: 2.8.1 + + '@firebase/logger@0.4.0': + dependencies: + tslib: 2.8.1 + + '@firebase/messaging-compat@0.2.4(@firebase/app-compat@0.2.13)(@firebase/app@0.9.13)': + dependencies: + '@firebase/app-compat': 0.2.13 + '@firebase/component': 0.6.4 + '@firebase/messaging': 0.12.4(@firebase/app@0.9.13) + '@firebase/util': 1.9.3 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + + '@firebase/messaging-interop-types@0.2.0': {} + + '@firebase/messaging@0.12.4(@firebase/app@0.9.13)': + dependencies: + '@firebase/app': 0.9.13 + '@firebase/component': 0.6.4 + '@firebase/installations': 0.6.4(@firebase/app@0.9.13) + '@firebase/messaging-interop-types': 0.2.0 + '@firebase/util': 1.9.3 + idb: 7.0.1 + tslib: 2.8.1 + + '@firebase/performance-compat@0.2.4(@firebase/app-compat@0.2.13)(@firebase/app@0.9.13)': + dependencies: + '@firebase/app-compat': 0.2.13 + '@firebase/component': 0.6.4 + '@firebase/logger': 0.4.0 + '@firebase/performance': 0.6.4(@firebase/app@0.9.13) + '@firebase/performance-types': 0.2.0 + '@firebase/util': 1.9.3 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + + '@firebase/performance-types@0.2.0': {} + + '@firebase/performance@0.6.4(@firebase/app@0.9.13)': + dependencies: + '@firebase/app': 0.9.13 + '@firebase/component': 0.6.4 + '@firebase/installations': 0.6.4(@firebase/app@0.9.13) + '@firebase/logger': 0.4.0 + '@firebase/util': 1.9.3 + tslib: 2.8.1 + + '@firebase/remote-config-compat@0.2.4(@firebase/app-compat@0.2.13)(@firebase/app@0.9.13)': + dependencies: + '@firebase/app-compat': 0.2.13 + '@firebase/component': 0.6.4 + '@firebase/logger': 0.4.0 + '@firebase/remote-config': 0.4.4(@firebase/app@0.9.13) + '@firebase/remote-config-types': 0.3.0 + '@firebase/util': 1.9.3 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + + '@firebase/remote-config-types@0.3.0': {} + + '@firebase/remote-config@0.4.4(@firebase/app@0.9.13)': + dependencies: + '@firebase/app': 0.9.13 + '@firebase/component': 0.6.4 + '@firebase/installations': 0.6.4(@firebase/app@0.9.13) + '@firebase/logger': 0.4.0 + '@firebase/util': 1.9.3 + tslib: 2.8.1 + + '@firebase/storage-compat@0.3.2(@firebase/app-compat@0.2.13)(@firebase/app-types@0.9.0)(@firebase/app@0.9.13)': + dependencies: + '@firebase/app-compat': 0.2.13 + '@firebase/component': 0.6.4 + '@firebase/storage': 0.11.2(@firebase/app@0.9.13) + '@firebase/storage-types': 0.8.0(@firebase/app-types@0.9.0)(@firebase/util@1.9.3) + '@firebase/util': 1.9.3 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + - '@firebase/app-types' + - encoding + + '@firebase/storage-types@0.8.0(@firebase/app-types@0.9.0)(@firebase/util@1.9.3)': + dependencies: + '@firebase/app-types': 0.9.0 + '@firebase/util': 1.9.3 + + '@firebase/storage@0.11.2(@firebase/app@0.9.13)': + dependencies: + '@firebase/app': 0.9.13 + '@firebase/component': 0.6.4 + '@firebase/util': 1.9.3 + node-fetch: 2.6.7 + tslib: 2.8.1 + transitivePeerDependencies: + - encoding + + '@firebase/util@1.9.3': + dependencies: + tslib: 2.8.1 + + '@firebase/webchannel-wrapper@0.10.1': {} + + '@grpc/grpc-js@1.7.3': + dependencies: + '@grpc/proto-loader': 0.7.13 + '@types/node': 22.13.2 + + '@grpc/proto-loader@0.6.13': + dependencies: + '@types/long': 4.0.2 + lodash.camelcase: 4.3.0 + long: 4.0.0 + protobufjs: 6.11.4 + yargs: 16.2.0 + + '@grpc/proto-loader@0.7.13': + dependencies: + lodash.camelcase: 4.3.0 + long: 5.3.0 + protobufjs: 7.4.0 + yargs: 17.7.2 + + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.4': {} + + '@protobufjs/eventemitter@1.1.0': {} + + '@protobufjs/fetch@1.1.0': + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/inquire': 1.1.0 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/inquire@1.1.0': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.0': {} + + '@socket.io/component-emitter@3.1.2': {} + + '@types/cors@2.8.17': + dependencies: + '@types/node': 22.13.2 + + '@types/long@4.0.2': {} + + '@types/node@22.13.2': + dependencies: + undici-types: 6.20.0 + + '@types/roslib@1.3.5': + dependencies: + eventemitter2: 6.4.9 + + '@xmldom/xmldom@0.8.10': {} + + accepts@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + base64id@2.0.0: {} + + cbor-js@0.1.0: {} + + cliui@7.0.4: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + cookie@0.7.2: {} + + cors@2.8.5: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + debug@4.3.7: + dependencies: + ms: 2.1.3 + + emoji-regex@8.0.0: {} + + engine.io-parser@5.2.3: {} + + engine.io@6.6.4: + dependencies: + '@types/cors': 2.8.17 + '@types/node': 22.13.2 + accepts: 1.3.8 + base64id: 2.0.0 + cookie: 0.7.2 + cors: 2.8.5 + debug: 4.3.7 + engine.io-parser: 5.2.3 + ws: 8.17.1 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + escalade@3.2.0: {} + + eventemitter2@6.4.9: {} + + faye-websocket@0.11.4: + dependencies: + websocket-driver: 0.7.4 + + firebase@9.23.0: + dependencies: + '@firebase/analytics': 0.10.0(@firebase/app@0.9.13) + '@firebase/analytics-compat': 0.2.6(@firebase/app-compat@0.2.13)(@firebase/app@0.9.13) + '@firebase/app': 0.9.13 + '@firebase/app-check': 0.8.0(@firebase/app@0.9.13) + '@firebase/app-check-compat': 0.3.7(@firebase/app-compat@0.2.13)(@firebase/app@0.9.13) + '@firebase/app-compat': 0.2.13 + '@firebase/app-types': 0.9.0 + '@firebase/auth': 0.23.2(@firebase/app@0.9.13) + '@firebase/auth-compat': 0.4.2(@firebase/app-compat@0.2.13)(@firebase/app-types@0.9.0)(@firebase/app@0.9.13) + '@firebase/database': 0.14.4 + '@firebase/database-compat': 0.3.4 + '@firebase/firestore': 3.13.0(@firebase/app@0.9.13) + '@firebase/firestore-compat': 0.3.12(@firebase/app-compat@0.2.13)(@firebase/app-types@0.9.0)(@firebase/app@0.9.13) + '@firebase/functions': 0.10.0(@firebase/app@0.9.13) + '@firebase/functions-compat': 0.3.5(@firebase/app-compat@0.2.13)(@firebase/app@0.9.13) + '@firebase/installations': 0.6.4(@firebase/app@0.9.13) + '@firebase/installations-compat': 0.2.4(@firebase/app-compat@0.2.13)(@firebase/app-types@0.9.0)(@firebase/app@0.9.13) + '@firebase/messaging': 0.12.4(@firebase/app@0.9.13) + '@firebase/messaging-compat': 0.2.4(@firebase/app-compat@0.2.13)(@firebase/app@0.9.13) + '@firebase/performance': 0.6.4(@firebase/app@0.9.13) + '@firebase/performance-compat': 0.2.4(@firebase/app-compat@0.2.13)(@firebase/app@0.9.13) + '@firebase/remote-config': 0.4.4(@firebase/app@0.9.13) + '@firebase/remote-config-compat': 0.2.4(@firebase/app-compat@0.2.13)(@firebase/app@0.9.13) + '@firebase/storage': 0.11.2(@firebase/app@0.9.13) + '@firebase/storage-compat': 0.3.2(@firebase/app-compat@0.2.13)(@firebase/app-types@0.9.0)(@firebase/app@0.9.13) + '@firebase/util': 1.9.3 + transitivePeerDependencies: + - encoding + + get-caller-file@2.0.5: {} + + http-parser-js@0.5.9: {} + + idb@7.0.1: {} + + idb@7.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + lodash.camelcase@4.3.0: {} + + long@4.0.0: {} + + long@5.3.0: {} + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + ms@2.1.3: {} + + negotiator@0.6.3: {} + + node-fetch@2.6.7: + dependencies: + whatwg-url: 5.0.0 + + object-assign@4.1.1: {} + + pngparse@2.0.1: {} + + protobufjs@6.11.4: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/long': 4.0.2 + '@types/node': 22.13.2 + long: 4.0.0 + + protobufjs@7.4.0: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/node': 22.13.2 + long: 5.3.0 + + require-directory@2.1.1: {} + + roslib@1.4.1: + dependencies: + '@xmldom/xmldom': 0.8.10 + cbor-js: 0.1.0 + eventemitter2: 6.4.9 + object-assign: 4.1.1 + pngparse: 2.0.1 + socket.io: 4.8.1 + webworkify: 1.5.0 + webworkify-webpack: 2.1.5 + ws: 8.18.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + safe-buffer@5.2.1: {} + + socket.io-adapter@2.5.5: + dependencies: + debug: 4.3.7 + ws: 8.17.1 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + socket.io-parser@4.2.4: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.7 + transitivePeerDependencies: + - supports-color + + socket.io@4.8.1: + dependencies: + accepts: 1.3.8 + base64id: 2.0.0 + cors: 2.8.5 + debug: 4.3.7 + engine.io: 6.6.4 + socket.io-adapter: 2.5.5 + socket.io-parser: 4.2.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + tr46@0.0.3: {} + + tslib@2.8.1: {} + + undici-types@6.20.0: {} + + vary@1.1.2: {} + + webidl-conversions@3.0.1: {} + + websocket-driver@0.7.4: + dependencies: + http-parser-js: 0.5.9 + safe-buffer: 5.2.1 + websocket-extensions: 0.1.4 + + websocket-extensions@0.1.4: {} + + webworkify-webpack@2.1.5: {} + + webworkify@1.5.0: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + ws@8.17.1: {} + + ws@8.18.0: {} + + y18n@5.0.8: {} + + yargs-parser@20.2.9: {} + + yargs-parser@21.1.1: {} + + yargs@16.2.0: + dependencies: + cliui: 7.0.4 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 20.2.9 + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 diff --git a/extensions/src/scratch3_jibo/virtualJibo/jiboBody/black.png b/extensions/src/scratch3_jibo/virtualJibo/jiboBody/black.png new file mode 100644 index 000000000..98cacd183 Binary files /dev/null and b/extensions/src/scratch3_jibo/virtualJibo/jiboBody/black.png differ diff --git a/extensions/src/scratch3_jibo/virtualJibo/jiboBody/blue.png b/extensions/src/scratch3_jibo/virtualJibo/jiboBody/blue.png new file mode 100644 index 000000000..5de60f65e Binary files /dev/null and b/extensions/src/scratch3_jibo/virtualJibo/jiboBody/blue.png differ diff --git a/extensions/src/scratch3_jibo/virtualJibo/jiboBody/cyan.png b/extensions/src/scratch3_jibo/virtualJibo/jiboBody/cyan.png new file mode 100644 index 000000000..0c8cc279d Binary files /dev/null and b/extensions/src/scratch3_jibo/virtualJibo/jiboBody/cyan.png differ diff --git a/extensions/src/scratch3_jibo/virtualJibo/jiboBody/green.png b/extensions/src/scratch3_jibo/virtualJibo/jiboBody/green.png new file mode 100644 index 000000000..6d8b14215 Binary files /dev/null and b/extensions/src/scratch3_jibo/virtualJibo/jiboBody/green.png differ diff --git a/extensions/src/scratch3_jibo/virtualJibo/jiboBody/magenta.png b/extensions/src/scratch3_jibo/virtualJibo/jiboBody/magenta.png new file mode 100644 index 000000000..82490b787 Binary files /dev/null and b/extensions/src/scratch3_jibo/virtualJibo/jiboBody/magenta.png differ diff --git a/extensions/src/scratch3_jibo/virtualJibo/jiboBody/red.png b/extensions/src/scratch3_jibo/virtualJibo/jiboBody/red.png new file mode 100644 index 000000000..4306a6a71 Binary files /dev/null and b/extensions/src/scratch3_jibo/virtualJibo/jiboBody/red.png differ diff --git a/extensions/src/scratch3_jibo/virtualJibo/jiboBody/white.png b/extensions/src/scratch3_jibo/virtualJibo/jiboBody/white.png new file mode 100644 index 000000000..e73e6d851 Binary files /dev/null and b/extensions/src/scratch3_jibo/virtualJibo/jiboBody/white.png differ diff --git a/extensions/src/scratch3_jibo/virtualJibo/jiboBody/yellow.png b/extensions/src/scratch3_jibo/virtualJibo/jiboBody/yellow.png new file mode 100644 index 000000000..29994ed8d Binary files /dev/null and b/extensions/src/scratch3_jibo/virtualJibo/jiboBody/yellow.png differ diff --git a/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Airplane.png b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Airplane.png new file mode 100644 index 000000000..4fe2cefba Binary files /dev/null and b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Airplane.png differ diff --git a/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Apple.png b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Apple.png new file mode 100644 index 000000000..0a79e4f3d Binary files /dev/null and b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Apple.png differ diff --git a/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Art.png b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Art.png new file mode 100644 index 000000000..f300ce857 Binary files /dev/null and b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Art.png differ diff --git a/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Bowling.png b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Bowling.png new file mode 100644 index 000000000..18ad11829 Binary files /dev/null and b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Bowling.png differ diff --git a/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Checkmark.png b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Checkmark.png new file mode 100644 index 000000000..13bb43d88 Binary files /dev/null and b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Checkmark.png differ diff --git a/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Curious.png b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Curious.png new file mode 100644 index 000000000..726c6801a Binary files /dev/null and b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Curious.png differ diff --git a/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Exclamation.png b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Exclamation.png new file mode 100644 index 000000000..45d87bfcb Binary files /dev/null and b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Exclamation.png differ diff --git a/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Eye1.svg b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Eye1.svg new file mode 100644 index 000000000..12eafb75d --- /dev/null +++ b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Eye1.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Eye2.svg b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Eye2.svg new file mode 100644 index 000000000..4690d1788 --- /dev/null +++ b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Eye2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Eye3.svg b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Eye3.svg new file mode 100644 index 000000000..47a4209ba --- /dev/null +++ b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Eye3.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Eye4.svg b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Eye4.svg new file mode 100644 index 000000000..1940edf56 --- /dev/null +++ b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Eye4.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Eye5.svg b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Eye5.svg new file mode 100644 index 000000000..acdaec94c --- /dev/null +++ b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Eye5.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Football.png b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Football.png new file mode 100644 index 000000000..b11d472be Binary files /dev/null and b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Football.png differ diff --git a/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Frustrated.png b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Frustrated.png new file mode 100644 index 000000000..ad6cea0dd Binary files /dev/null and b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Frustrated.png differ diff --git a/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Happy.png b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Happy.png new file mode 100644 index 000000000..c1723d912 Binary files /dev/null and b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Happy.png differ diff --git a/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Heart.png b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Heart.png new file mode 100644 index 000000000..5c9877f11 Binary files /dev/null and b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Heart.png differ diff --git a/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Laugh.png b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Laugh.png new file mode 100644 index 000000000..30dec8243 Binary files /dev/null and b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Laugh.png differ diff --git a/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Magic.png b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Magic.png new file mode 100644 index 000000000..02fd05de0 Binary files /dev/null and b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Magic.png differ diff --git a/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Music.png b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Music.png new file mode 100644 index 000000000..03f08d3fb Binary files /dev/null and b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Music.png differ diff --git a/extensions/src/scratch3_jibo/virtualJibo/jiboEye/No.png b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/No.png new file mode 100644 index 000000000..fd9d2fc9f Binary files /dev/null and b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/No.png differ diff --git a/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Ocean.png b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Ocean.png new file mode 100644 index 000000000..2a680f573 Binary files /dev/null and b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Ocean.png differ diff --git a/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Penguin.png b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Penguin.png new file mode 100644 index 000000000..f5d32e1d1 Binary files /dev/null and b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Penguin.png differ diff --git a/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Puzzled.png b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Puzzled.png new file mode 100644 index 000000000..5f02a2501 Binary files /dev/null and b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Puzzled.png differ diff --git a/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Rainbow.png b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Rainbow.png new file mode 100644 index 000000000..7a8605c7e Binary files /dev/null and b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Rainbow.png differ diff --git a/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Robot.png b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Robot.png new file mode 100644 index 000000000..cae6f2cb2 Binary files /dev/null and b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Robot.png differ diff --git a/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Rocket.png b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Rocket.png new file mode 100644 index 000000000..50856395a Binary files /dev/null and b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Rocket.png differ diff --git a/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Sad.png b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Sad.png new file mode 100644 index 000000000..b060f4225 Binary files /dev/null and b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Sad.png differ diff --git a/extensions/src/scratch3_jibo/virtualJibo/jiboEye/SadEyes.png b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/SadEyes.png new file mode 100644 index 000000000..433352195 Binary files /dev/null and b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/SadEyes.png differ diff --git a/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Snowflake.png b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Snowflake.png new file mode 100644 index 000000000..133645583 Binary files /dev/null and b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Snowflake.png differ diff --git a/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Success.png b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Success.png new file mode 100644 index 000000000..7567267b2 Binary files /dev/null and b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Success.png differ diff --git a/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Taco.png b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Taco.png new file mode 100644 index 000000000..f94755efd Binary files /dev/null and b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Taco.png differ diff --git a/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Thinking.png b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Thinking.png new file mode 100644 index 000000000..e045a345c Binary files /dev/null and b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Thinking.png differ diff --git a/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Videogame.png b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Videogame.png new file mode 100644 index 000000000..c35e731d7 Binary files /dev/null and b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Videogame.png differ diff --git a/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Yes.png b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Yes.png new file mode 100644 index 000000000..c1e367b8b Binary files /dev/null and b/extensions/src/scratch3_jibo/virtualJibo/jiboEye/Yes.png differ diff --git a/extensions/src/scratch3_jibo/virtualJibo/virtualJibo.ts b/extensions/src/scratch3_jibo/virtualJibo/virtualJibo.ts new file mode 100644 index 000000000..284efb3bd --- /dev/null +++ b/extensions/src/scratch3_jibo/virtualJibo/virtualJibo.ts @@ -0,0 +1,550 @@ +import Target from "$scratch-vm/engine/target"; +import type RenderedTarget from "$scratch-vm/sprites/rendered-target"; +import MockBitmapAdapter from "$common/extension/mixins/optional/addCostumes/MockBitmapAdapter"; +import { getUrlHelper } from "$common/extension/mixins/optional/addCostumes/utils"; + +import { Color, ColorType } from "../jiboUtils/ColorDef"; +import { Direction, DirType } from "../jiboUtils/LookAtDef"; +import { + Dance, DanceType, + Emotion, EmotionType, + Icon, IconType, + Audio, AudioType +} from "../jiboUtils/AnimDef"; + +import jiboBodyBlack from "./jiboBody/black.png"; +import jiboBodyRed from "./jiboBody/red.png"; +import jiboBodyYellow from "./jiboBody/yellow.png"; +import jiboBodyGreen from "./jiboBody/green.png"; +import jiboBodyCyan from "./jiboBody/cyan.png"; +import jiboBodyBlue from "./jiboBody/blue.png"; +import jiboBodyMagenta from "./jiboBody/magenta.png"; +import jiboBodyWhite from "./jiboBody/white.png"; + +import jiboEyeAirplane from "./jiboEye/Airplane.png"; +import jiboEyeApple from "./jiboEye/Apple.png"; +import jiboEyeArt from "./jiboEye/Art.png"; +import jiboEyeBowling from "./jiboEye/Bowling.png"; +import jiboEyeCheckmark from "./jiboEye/Checkmark.png"; +import jiboEyeExclamation from "./jiboEye/Exclamation.png"; +import jiboEyeFootball from "./jiboEye/Football.png"; +import jiboEyeHeart from "./jiboEye/Heart.png"; +import jiboEyeMagic from "./jiboEye/Magic.png"; +import jiboEyeOcean from "./jiboEye/Ocean.png"; +import jiboEyePenguin from "./jiboEye/Penguin.png"; +import jiboEyeRainbow from "./jiboEye/Rainbow.png"; +import jiboEyeRobot from "./jiboEye/Robot.png"; +import jiboEyeRocket from "./jiboEye/Rocket.png"; +import jiboEyeSnowflake from "./jiboEye/Snowflake.png"; +import jiboEyeTaco from "./jiboEye/Taco.png"; +import jiboEyeVideoGame from "./jiboEye/Videogame.png"; +import jiboEyeCurious from "./jiboEye/Curious.png"; +import jiboEyeFrustrated from "./jiboEye/Frustrated.png"; +import jiboEyeHappy from "./jiboEye/Happy.png"; +import jiboEyeLaugh from "./jiboEye/Laugh.png"; +import jiboEyeNo from "./jiboEye/No.png"; +import jiboEyePuzzled from "./jiboEye/Puzzled.png"; +import jiboEyeSad from "./jiboEye/Sad.png"; +import jiboEyeSadEyes from "./jiboEye/SadEyes.png"; +import jiboEyeSuccess from "./jiboEye/Success.png"; +import jiboEyeThinking from "./jiboEye/Thinking.png"; +import jiboEyeYes from "./jiboEye/Yes.png"; + +import jiboEye1 from "./jiboEye/Eye1.svg"; +import jiboEye2 from "./jiboEye/Eye2.svg"; +import jiboEye3 from "./jiboEye/Eye3.svg"; +import jiboEye4 from "./jiboEye/Eye4.svg"; +import jiboEye5 from "./jiboEye/Eye5.svg"; + +import Runtime from "$root/packages/scratch-vm/src/engine/runtime"; + +let bitmapAdapter: MockBitmapAdapter; +let urlHelper: ReturnType; + +const rendererKey: keyof RenderedTarget = "renderer"; +const isRenderedTarget = (target: Target | RenderedTarget): target is RenderedTarget => rendererKey in target; + +const JIBO_BODY = "jibo-body"; +const JIBO_EYE = "jibo-eye"; + +const DEFAULT_JIBO_EYE = { + dx: 0, // jibo eye = jibo body + dx * jibo body size + dy: .76, // jibo eye = (jibo body) + dy * jibo body size + dsize: .65, // jibo eye = jibo body * dsize + diconSize: .35, +} + +type Coords = { + dx: number; + dy: number; +}; +type DirDefType = { + value: Coords; +}; +const directionDef: Record = { + [Direction.up]: { + value: { dx: 0, dy: 1 }, + }, + [Direction.down]: { + value: { dx: 0, dy: 0.45 }, + }, + [Direction.left]: { + value: { dx: 0.45, dy: 0.76 }, + }, + [Direction.right]: { + value: { dx: -0.45, dy: 0.76 }, + }, + [Direction.forward]: { + value: { dx: 0, dy: 0.76 }, + }, +}; + +type ImageDefType = { + imageData: string; +}; + +const jiboEyeDef: Record = { + "Eye1": { + imageData: jiboEye1, + }, + "Eye2": { + imageData: jiboEye2, + }, + "Eye3": { + imageData: jiboEye3, + }, + "Eye4": { + imageData: jiboEye4, + }, + "Eye5": { + imageData: jiboEye5, + }, +}; +const JIBO_EYE_ANIM = [ + "Eye1", "Eye2", "Eye2", "Eye3", "Eye4", "Eye5", "Eye3", "Eye2", "Eye1" +]; + +const colorDef: Record = { + [Color.Red]: { + imageData: jiboBodyRed, + }, + [Color.Yellow]: { + imageData: jiboBodyYellow, + }, + [Color.Green]: { + imageData: jiboBodyGreen, + }, + [Color.Cyan]: { + imageData: jiboBodyCyan, + }, + [Color.Blue]: { + imageData: jiboBodyBlue, + }, + [Color.Magenta]: { + imageData: jiboBodyMagenta, + }, + [Color.White]: { + imageData: jiboBodyWhite, + }, + [Color.Random]: { + imageData: "" + }, + [Color.Off]: { + imageData: jiboBodyBlack, + }, +}; + +type AnimFileType = { + imageData: string; +}; +const iconFiles: Record = { + [Emotion.Curious]: { + imageData: jiboEyeCurious, + }, + [Emotion.Frustrated]: { + imageData: jiboEyeFrustrated, + }, + [Emotion.Happy]: { + imageData: jiboEyeHappy, + }, + [Emotion.Laugh]: { + imageData: jiboEyeLaugh, + }, + [Emotion.No]: { + imageData: jiboEyeNo, + }, + [Emotion.Puzzled]: { + imageData: jiboEyePuzzled, + }, + [Emotion.Sad]: { + imageData: jiboEyeSad, + }, + [Emotion.SadEyes]: { + imageData: jiboEyeSadEyes, + }, + [Emotion.Success]: { + imageData: jiboEyeSuccess, + }, + [Emotion.Thinking]: { + imageData: jiboEyeThinking, + }, + [Emotion.Yes]: { + imageData: jiboEyeYes, + }, + [Icon.Airplane]: { + imageData: jiboEyeAirplane, + }, + [Icon.Apple]: { + imageData: jiboEyeApple, + }, + [Icon.Art]: { + imageData: jiboEyeArt, + }, + [Icon.Bowling]: { + imageData: jiboEyeBowling, + }, + [Icon.Checkmark]: { + imageData: jiboEyeCheckmark, + }, + [Icon.ExclamationPoint]: { + imageData: jiboEyeExclamation, + }, + [Icon.Football]: { + imageData: jiboEyeFootball, + }, + [Icon.Heart]: { + imageData: jiboEyeHeart, + }, + [Icon.Magic]: { + imageData: jiboEyeMagic, + }, + [Icon.Ocean]: { + imageData: jiboEyeOcean, + }, + [Icon.Penguin]: { + imageData: jiboEyePenguin, + }, + [Icon.Rainbow]: { + imageData: jiboEyeRainbow, + }, + [Icon.Robot]: { + imageData: jiboEyeRobot, + }, + [Icon.Rocket]: { + imageData: jiboEyeRocket, + }, + [Icon.Snowflake]: { + imageData: jiboEyeSnowflake, + }, + [Icon.Taco]: { + imageData: jiboEyeTaco, + }, + [Icon.VideoGame]: { + imageData: jiboEyeVideoGame, + }, +}; + +export default class Scratch3VirtualJibo { + runtime: Runtime; + answer: string; + + init(runtime: Runtime) { + this.runtime = runtime; + this.answer = ""; + } + + resetJiboEyeTarget(target: Target, type: string = "eye") { + let bodyTarget = this.getJiboBodyTarget(target); + let eyeTarget = this.getJiboEyeTarget(target); + + if (!isRenderedTarget(bodyTarget) || !isRenderedTarget(eyeTarget)) { + console.warn("Eye could not be reset as the supplied target didn't lead to rendered eye and body targets"); + return false; + } + + if (eyeTarget) { + let mult = type === "eye" ? + 1 : + DEFAULT_JIBO_EYE.diconSize / DEFAULT_JIBO_EYE.dsize; + let newX = bodyTarget.x + DEFAULT_JIBO_EYE.dx * bodyTarget.size; + let newY = bodyTarget.y + DEFAULT_JIBO_EYE.dy * bodyTarget.size; + let newSize = bodyTarget.size * DEFAULT_JIBO_EYE.dsize * mult; + eyeTarget.setXY(newX, newY, null); + eyeTarget.setSize(newSize); + eyeTarget.goToFront(); + } + } + async setSpriteCostume(target: Target, name: string, imageDataURI: string) { + // try to set the costume of the target by name + let foundCostume = this.setCostumeByName(target, name); + + if (!foundCostume) { + console.log("Did not find the costume we wanted. Adding new one"); + // if not, add and set the costume + await this.addCostumeBitmap(target, imageDataURI, "add and set", name); + } + } + getJiboBodyTarget(currentTarget: Target): Target { + // find the jibo-body sprite + let spriteTarget; + if (currentTarget.getName().startsWith(JIBO_BODY)) { + // first see if the current target is a Jibo body + // if so, assume this is the one we want to edit + spriteTarget = currentTarget; + } else if (currentTarget.getName().startsWith(JIBO_EYE)) { + // next see if this is a Jibo eye, and select the corresponding jibo body (same suffix) + let jiboEyeName = currentTarget.getName(); + let suffix = jiboEyeName.substring(jiboEyeName.indexOf(JIBO_EYE) + JIBO_EYE.length); + let matches = this.runtime.targets.filter((target) => target.getName().startsWith(JIBO_BODY + suffix)); + if (matches.length > 0) spriteTarget = matches[0]; + } else { + // otherwise, pick the first Jibo body you see + let matches = this.runtime.targets.filter((target) => target.getName().startsWith(JIBO_BODY)); + if (matches.length > 0) spriteTarget = matches[0]; + } + return spriteTarget; + } + getJiboEyeTarget(currentTarget: Target): Target { + // find the jibo-eye sprite + let spriteTarget; + if (currentTarget.getName().startsWith(JIBO_EYE)) { + // first see if the current target is a Jibo eye + // if so, assume this is the one we want to edit + spriteTarget = currentTarget; + } else if (currentTarget.getName().startsWith(JIBO_BODY)) { + // next see if this is a Jibo body, and select the corresponding jibo eye (same suffix) + let jiboBodyName = currentTarget.getName(); + let suffix = jiboBodyName.substring(jiboBodyName.indexOf(JIBO_BODY) + JIBO_BODY.length); + let matches = this.runtime.targets.filter((target) => target.getName().startsWith(JIBO_EYE + suffix)); + if (matches.length > 0) spriteTarget = matches[0]; + } else { + // otherwise, pick the first Jibo eye you see + let matches = this.runtime.targets.filter((target) => target.getName().startsWith(JIBO_EYE)); + if (matches.length > 0) spriteTarget = matches[0]; + } + return spriteTarget; + } + + async say(text: string, currentTarget: Target) { + let spriteTarget = this.getJiboBodyTarget(currentTarget); + if (spriteTarget) { + // emit the say function + this.runtime.emit('SAY', spriteTarget, 'say', text); + // wait for a bit of time + let wordCount = text.match(/\S+/g).length; + await new Promise((r) => setTimeout(r, 500 * wordCount)); + this.runtime.emit('SAY', spriteTarget, 'say', ''); + } else { + console.log("No Jibo body found"); + } + } + async ask(text: string) { + // wait for stage to get answer + this.runtime.emit('QUESTION', text); + this.answer = await this.answer_receive(); + } + answer_receive(): Promise { + return new Promise((resolve, reject) => { + this.runtime.once('ANSWER', (answer) => { + // TODO this introduces a bug with the sensing blocks, improve if possible + resolve(answer); + }); + }); + } + + /* update the appearance of virtual Jibo's LED*/ + async setLED(color: string, currentTarget: Target) { + // find the jibo-body sprite to edit + let spriteTarget = this.getJiboBodyTarget(currentTarget); + if (spriteTarget) { + // change the Sprite costume + if (color == "random") { + const randomColorIdx = Math.floor( + // exclude random and off + Math.random() * (Object.keys(colorDef).length - 2) + ); + color = Object.keys(colorDef)[randomColorIdx]; + } + + let imageData = colorDef[color].imageData; + await this.setSpriteCostume(spriteTarget, color, imageData); + } else { + console.log("No Jibo body found"); + } + } + + async blink(jiboEye: Target) { + this.resetJiboEyeTarget(jiboEye); + for (let i = 0; i < JIBO_EYE_ANIM.length; i++) { + let costumeName = JIBO_EYE_ANIM[i]; + let imageData = jiboEyeDef[costumeName].imageData; + await this.setSpriteCostume(jiboEye, costumeName, imageData); + await new Promise((r) => setTimeout(r, 50)); + } + } + async jumpTransition(jiboEye: Target, newAnim: string, imageData: string) { + let type = newAnim.includes("Eye") ? "eye" : "icon"; + if (!isRenderedTarget(jiboEye)) { + console.warn("Eye could not be reset as the supplied target wasn't a rendered target"); + return false; + } + + // move up 5 loops + for (let i = 0; i < 5; i++) { + jiboEye.setXY(jiboEye.x, jiboEye.y + 5, null) + await new Promise((r) => setTimeout(r, 50)); + } + // move eye down 7 loops + for (let i = 0; i < 7; i++) { + jiboEye.setXY(jiboEye.x, jiboEye.y - 5, null) + await new Promise((r) => setTimeout(r, 50)); + } + // switch costume + await this.setSpriteCostume(jiboEye, newAnim, imageData); + this.resetJiboEyeTarget(jiboEye, type); + // move up 4 loops + for (let i = 0; i < 4; i++) { + jiboEye.setXY(jiboEye.x, jiboEye.y + 5, null) + await new Promise((r) => setTimeout(r, 50)); + } + // move down 2 loops + for (let i = 0; i < 2; i++) { + jiboEye.setXY(jiboEye.x, jiboEye.y - 5, null) + await new Promise((r) => setTimeout(r, 50)); + } + } + async anim(animation: string, commandType: string, currentTarget: Target) { + // find the jibo-eye sprite to edit + let spriteTarget = this.getJiboEyeTarget(currentTarget); + if (!isRenderedTarget(spriteTarget)) { + console.warn("No rendered jibo-eye target could be found"); + return false; + } + + // change the Sprite costume + let imageDataURI; + //if (commandType == "dance") imageDataURI = danceFiles[animation].imageData; + if (commandType == "emotion" || commandType == "icon") { + imageDataURI = iconFiles[animation].imageData; + await this.jumpTransition(spriteTarget, animation, imageDataURI); + await new Promise((r) => setTimeout(r, 3000)); + await this.jumpTransition(spriteTarget, "Eye1", jiboEyeDef["Eye1"].imageData); + // finish a blink + await this.blink(spriteTarget); + } + } + async lookAt(direction: string, currentTarget: Target) { + // find the jibo-body and jibo-eye sprites to edit + let eyeTarget = this.getJiboEyeTarget(currentTarget); + let bodyTarget = this.getJiboBodyTarget(currentTarget); + if (!isRenderedTarget(eyeTarget) || !(isRenderedTarget(bodyTarget))) { + console.warn("Eye could not be reset as the supplied target wasn't a rendered target"); + return false; + } + + let coords = directionDef[direction].value; + let newX = bodyTarget.x + coords.dx * bodyTarget.size; + let newY = bodyTarget.y + coords.dy * bodyTarget.size; + let xStepSize = (newX - eyeTarget.x) / 10; + let yStepSize = (newY - eyeTarget.y) / 10; + for (let i = 0; i < 10; i++) { + eyeTarget.setXY( + eyeTarget.x + xStepSize, + eyeTarget.y + yStepSize, + null + ); + await new Promise((r) => setTimeout(r, 50)); + } + } + + + // Copied code from /workspace/prg-extension-boilerplate/extensions/src/common/extension/mixins/optional/addCostumes/index.ts + // until I figure out a better way + + /** + * Add a costume to the current sprite based on some image data + * @param {Target} target (e.g. `util.target`) + * @param {ImageData} image What image to use to create the costume + * @param {"add only" | "add and set"} action What action should be applied + * - **_add only_**: generates the costume and append it it to the sprite's costume library + * - **_add and set_**: Both generate the costume (adding it to the sprite's costume library) and set it as the sprite's current costume + * @param {string?} name optional name to attach to the costume + */ + async addCostume(target: Target, image: ImageData, action: "add only" | "add and set", name?: string) { + if (!isRenderedTarget(target)) return console.warn("Costume could not be added as the supplied target wasn't a rendered target"); + + name ??= `virtualJibo_generated_${Date.now()}`; + bitmapAdapter ??= new MockBitmapAdapter(); + urlHelper ??= getUrlHelper(image); + + // storage is of type: https://github.com/LLK/scratch-storage/blob/develop/src/ScratchStorage.js + const { storage } = this.runtime; + const dataFormat = storage.DataFormat.PNG; + const assetType = storage.AssetType.ImageBitmap; + const dataBuffer = await bitmapAdapter.importBitmap(urlHelper.getDataURL(image)); + + const asset = storage.createAsset(assetType, dataFormat, dataBuffer, null, true); + const { assetId } = asset; + const costume = { name, dataFormat, asset, md5: `${assetId}.${dataFormat}`, assetId }; + + await this.runtime.addCostume(costume); + + const { length } = target.getCostumes(); + + target.addCostume(costume, length); + if (action === "add and set") target.setCostume(length); + } + + /** + * Add a costume to the current sprite based on a bitmpa input + * @param {Target} target (e.g. `util.target`) + * @param {string} bitmapImage What image to use to create the costume + * @param {"add only" | "add and set"} action What action should be applied + * - **_add only_**: generates the costume and append it it to the sprite's costume library + * - **_add and set_**: Both generate the costume (adding it to the sprite's costume library) and set it as the sprite's current costume + * @param {string?} name optional name to attach to the costume + */ + async addCostumeBitmap(target: Target, bitmapImage: string, action: "add only" | "add and set", name?: string) { + if (!isRenderedTarget(target)) return console.warn("Costume could not be added as the supplied target wasn't a rendered target"); + + name ??= `virtualJibo_generated_${Date.now()}`; + bitmapAdapter ??= new MockBitmapAdapter(); + //urlHelper ??= getUrlHelper(image); + + // storage is of type: https://github.com/LLK/scratch-storage/blob/develop/src/ScratchStorage.js + const { storage } = this.runtime; + const dataFormat = storage.DataFormat.PNG; + const assetType = storage.AssetType.ImageBitmap; + const dataBuffer = await bitmapAdapter.importBitmap(bitmapImage); + + const asset = storage.createAsset(assetType, dataFormat, dataBuffer, null, true); + const { assetId } = asset; + const costume = { name, dataFormat, asset, md5: `${assetId}.${dataFormat}`, assetId }; + + await this.runtime.addCostume(costume); + + const { length } = target.getCostumes(); + + target.addCostume(costume, length); + if (action === "add and set") target.setCostume(length); + } + + /** + * Add a costume to the current sprite based on same image data + * @param {Target} target (e.g. `util.target`) + * @param {string?} name costume name to look for + */ + setCostumeByName(target: Target, name: string): boolean { + if (!isRenderedTarget(target)) { + console.warn("Costume could not be set as the supplied target wasn't a rendered target"); + return false; + } + + let costumeIdx = target.getCostumeIndexByName(name); + if (costumeIdx >= 0) { + target.setCostume(costumeIdx); + return true; + } + return false; + } +} \ No newline at end of file