commit 11bd5aa72b5d6c35acd2e30f5926f214d6059316 Author: arc360 Date: Mon Jan 19 23:44:32 2026 -0600 Init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/README.md b/README.md new file mode 100644 index 0000000..69bd336 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +# NeoIDE +NeoIDE is a web-based platform that allows to create games or projects using visual block-coding, inspired by Scratch. + +[Website](https://NeoIDE.vercel.app/) | [Documentation](https://NeoIDE-docs.vercel.app/) diff --git a/editor.html b/editor.html new file mode 100644 index 0000000..452d5b8 --- /dev/null +++ b/editor.html @@ -0,0 +1,564 @@ + + + + + + + Editor - NeoIDE + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ +
+
+ + + + +
+
+ +
+
+
+ + + +
+ +
+
+
+ +
+

Costumes

+
+ +
+ +
+

Sounds

+
+ +
+
+ +
+
+
+ + + +
+ +
+
+
+
+ +
+

Loading...

+
+ +
+

Sprites

+
+
+ + +
+
+
+

Backdrops

+
+
+ + +
+ +
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/ext.js b/ext.js new file mode 100644 index 0000000..aacaabc --- /dev/null +++ b/ext.js @@ -0,0 +1,101 @@ +class TextSpriteExtension { + id = "textSprite"; + + registerCategory() { + return { + name: "Text Sprite", + color: "#8E44AD", + }; + } + + registerBlocks() { + return [ + { + type: "statement", + id: "createTextSprite", + text: "create text sprite [text] font [font] size [size] color [color]", + tooltip: "Create a new sprite rendered from text", + fields: { + text: { kind: "value", type: "String", default: "Hello!" }, + font: { + kind: "menu", + items: [ + "Arial", + "Verdana", + "Courier New", + "Times New Roman", + "Comic Sans MS", + ], + }, + size: { kind: "value", type: "Number", default: "48" }, + color: { kind: "value", type: "String", default: "#000000" }, + }, + }, + ]; + } + + registerCode() { + return { + createTextSprite: (inputs) => { + if (!window.app || !window.PIXI) return; + + const text = String(inputs.text ?? ""); + const font = inputs.font || "Arial"; + const size = Number(inputs.size || 48); + const color = String(inputs.color || "#000000"); + + /* ---------- Canvas ---------- */ + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + + ctx.font = `${size}px ${font}`; + const metrics = ctx.measureText(text); + + canvas.width = Math.ceil(metrics.width) + 20; + canvas.height = size + 20; + + ctx.font = `${size}px ${font}`; + ctx.fillStyle = color; + ctx.textBaseline = "top"; + ctx.fillText(text, 10, 10); + + /* ---------- PIXI Texture ---------- */ + const baseTexture = new PIXI.BaseTexture(canvas); + const texture = new PIXI.Texture(baseTexture); + baseTexture.update(); + + /* ---------- Create Sprite (same as addSprite) ---------- */ + const pixiSprite = new PIXI.Sprite(texture); + pixiSprite.anchor.set(0.5); + pixiSprite.x = 0; + pixiSprite.y = 0; + pixiSprite.scale._parentScaleEvent = pixiSprite; + + app.stage.addChild(pixiSprite); + + const id = "text-sprite-" + Date.now(); + + const spriteData = { + id, + pixiSprite, + code: "", + costumes: [{ name: "text", texture }], + sounds: [], + }; + + sprites.push(spriteData); + + // Register globally + if (!window.projectCostumes.includes("text")) { + window.projectCostumes.push("text"); + } + + // Select it + setActiveSprite(spriteData); + renderSpritesList(true); + }, + }; + } +} + +registerExtension(TextSpriteExtension); diff --git a/index.html b/index.html new file mode 100644 index 0000000..2b102dc --- /dev/null +++ b/index.html @@ -0,0 +1,92 @@ + + + + + + + Home - NeoIDE + + + + + + + + + + + + + + + + + + +
+
+ + + +
+
+ +
+
+ +
+

What is NeoIDE?

+

+ NeoIDE is a web-based platform that allows to create games or projects + using visual block-coding, inspired by Scratch. +

+ +

Features

+
+
+

Visual Block Editor

+

Write code by snapping blocks together, no typing required.

+
+
+

Custom Extensions

+

+ Extend your projects with custom blocks, new categories, and + powerful features. +

+
+
+

Themes

+

Switch between light and dark modes to match your style.

+
+
+
+ +
+

Ready to build your first project?

+ +
+ + + + + + + \ No newline at end of file diff --git a/login.html b/login.html new file mode 100644 index 0000000..95216bf --- /dev/null +++ b/login.html @@ -0,0 +1,46 @@ + + + + + + + Login - NeoIDE + + + + + + + + + + + + + + + + + + +
+ NeoIDE logo + Create an account + +

Username

+ + +

Password

+ + + +
+ + + + + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..76fa927 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2667 @@ +{ + "name": "rarry-vite", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "rarry-vite", + "version": "0.0.0", + "dependencies": { + "@blockly/plugin-strict-connection-checker": "^6.0.1", + "@fortawesome/fontawesome-free": "^7.0.1", + "blockly": "^12.2.0", + "jszip": "^3.10.1", + "pako": "^2.1.0", + "pixi.js-legacy": "^7.4.3", + "socket.io-client": "^4.8.1" + }, + "devDependencies": { + "vite": "^7.0.4" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@blockly/plugin-strict-connection-checker": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@blockly/plugin-strict-connection-checker/-/plugin-strict-connection-checker-6.0.1.tgz", + "integrity": "sha512-r59BoafH7WRDSjef/8xAjbEhWQlQl65ZoKdV/E2ykUBXPHUznTtSID0sgCbD4Ut3X9qcBPPsWakrm55Y+7FSXg==", + "license": "Apache 2.0", + "engines": { + "node": ">=8.17.0" + }, + "peerDependencies": { + "blockly": "^12.0.0" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz", + "integrity": "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.10.tgz", + "integrity": "sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.0.2", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz", + "integrity": "sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.6.tgz", + "integrity": "sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.6.tgz", + "integrity": "sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.6.tgz", + "integrity": "sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.6.tgz", + "integrity": "sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.6.tgz", + "integrity": "sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.6.tgz", + "integrity": "sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.6.tgz", + "integrity": "sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.6.tgz", + "integrity": "sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.6.tgz", + "integrity": "sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.6.tgz", + "integrity": "sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.6.tgz", + "integrity": "sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.6.tgz", + "integrity": "sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.6.tgz", + "integrity": "sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.6.tgz", + "integrity": "sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.6.tgz", + "integrity": "sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.6.tgz", + "integrity": "sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.6.tgz", + "integrity": "sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.6.tgz", + "integrity": "sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.6.tgz", + "integrity": "sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.6.tgz", + "integrity": "sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.6.tgz", + "integrity": "sha512-+SqBcAWoB1fYKmpWoQP4pGtx+pUUC//RNYhFdbcSA16617cchuryuhOCRpPsjCblKukAckWsV+aQ3UKT/RMPcA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.6.tgz", + "integrity": "sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.6.tgz", + "integrity": "sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.6.tgz", + "integrity": "sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.6.tgz", + "integrity": "sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@fortawesome/fontawesome-free": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-7.0.1.tgz", + "integrity": "sha512-RLmb9U6H2rJDnGxEqXxzy7ANPrQz7WK2/eTjdZqyU9uRU5W+FkAec9uU5gTYzFBH7aoXIw2WTJSCJR4KPlReQw==", + "license": "(CC-BY-4.0 AND OFL-1.1 AND MIT)", + "engines": { + "node": ">=6" + } + }, + "node_modules/@pixi/accessibility": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@pixi/accessibility/-/accessibility-7.4.3.tgz", + "integrity": "sha512-tCr0yeWpMe0yucFvEPidy5a7gVJGpTjqGrDpSEBYT/kbScfUwcoX49RrckCCCiXDlyO4WRh9lVVuHXTvqRLIMg==", + "license": "MIT", + "peerDependencies": { + "@pixi/core": "7.4.3", + "@pixi/display": "7.4.3", + "@pixi/events": "7.4.3" + } + }, + "node_modules/@pixi/app": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@pixi/app/-/app-7.4.3.tgz", + "integrity": "sha512-opyWMuO0Ir8pf1DYUR++wAA6ZfNU+nIX2z95R2OD172HbcdhB4/HD7leLIIAny/LciEdMqlWEBhXK7N93YWbdg==", + "license": "MIT", + "peerDependencies": { + "@pixi/core": "7.4.3", + "@pixi/display": "7.4.3" + } + }, + "node_modules/@pixi/assets": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@pixi/assets/-/assets-7.4.3.tgz", + "integrity": "sha512-StvjiJBSp/j9hHkGu8AFHNvwYUazXq64WhyhytztyDMRkg/l/cL7EcttY5T0qZNWlIpccdr60LUKrWDOuMpkiw==", + "license": "MIT", + "dependencies": { + "@types/css-font-loading-module": "^0.0.12" + }, + "peerDependencies": { + "@pixi/core": "7.4.3" + } + }, + "node_modules/@pixi/canvas-display": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@pixi/canvas-display/-/canvas-display-7.4.3.tgz", + "integrity": "sha512-Y33a6DL2s6RFIPKfC1tCw8eimVnaQVrzhOUgSZ03jE5B2pC7FwcOIZ64sDi2VpVToXMl2f5EgkgtEXYkWjTYHg==", + "license": "MIT", + "peerDependencies": { + "@pixi/display": "7.4.3" + } + }, + "node_modules/@pixi/canvas-extract": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@pixi/canvas-extract/-/canvas-extract-7.4.3.tgz", + "integrity": "sha512-sSpIf1C7nlEO22sW/bcVPZzUJyyh+4QcekhwzyYVbr1LVz0zk1WPOXu82MgiRxaHkyCfWdxes7drKQ2XHYk3eA==", + "license": "MIT", + "peerDependencies": { + "@pixi/canvas-renderer": "7.4.3", + "@pixi/core": "7.4.3", + "@pixi/display": "7.4.3", + "@pixi/extract": "7.4.3" + } + }, + "node_modules/@pixi/canvas-graphics": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@pixi/canvas-graphics/-/canvas-graphics-7.4.3.tgz", + "integrity": "sha512-E3kizZDI6m8WZg1AeHryanEhVGqMVsu75NZIwWu6mWkjHqGqvj/vG17Ua0jl9ThSFfPc4Y/xI9eAewni12IG8A==", + "license": "MIT", + "peerDependencies": { + "@pixi/canvas-display": "7.4.3", + "@pixi/canvas-renderer": "7.4.3", + "@pixi/core": "7.4.3", + "@pixi/graphics": "7.4.3" + } + }, + "node_modules/@pixi/canvas-mesh": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@pixi/canvas-mesh/-/canvas-mesh-7.4.3.tgz", + "integrity": "sha512-0yBalm1bRIfozdbuOqRcCWYN4XxxsDLcBA0NjpPFQn8LKImUpYxPayGbraxfIu8kF+z5rFubk5JiAGZc5UHorw==", + "license": "MIT", + "peerDependencies": { + "@pixi/canvas-display": "7.4.3", + "@pixi/canvas-renderer": "7.4.3", + "@pixi/core": "7.4.3", + "@pixi/mesh": "7.4.3", + "@pixi/mesh-extras": "7.4.3" + } + }, + "node_modules/@pixi/canvas-particle-container": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@pixi/canvas-particle-container/-/canvas-particle-container-7.4.3.tgz", + "integrity": "sha512-0gBIt2q8NtX9ZrHiY371AVM2Vmip1azQEy1ruOXq3b2PZgIaegZT7eAQ0997ljynheXWf3DBY/9Z6m3ejoV3jw==", + "license": "MIT", + "peerDependencies": { + "@pixi/particle-container": "7.4.3" + } + }, + "node_modules/@pixi/canvas-prepare": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@pixi/canvas-prepare/-/canvas-prepare-7.4.3.tgz", + "integrity": "sha512-ATCvDO9sMpteZl4LvZkFlOjCAAwa2i756oXQBDWiAEF/WkkyZ4y2O3CX4w5oYg+9G+S8b1iEDfKLcWr4HHk9yA==", + "license": "MIT", + "peerDependencies": { + "@pixi/canvas-renderer": "7.4.3", + "@pixi/core": "7.4.3", + "@pixi/prepare": "7.4.3" + } + }, + "node_modules/@pixi/canvas-renderer": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@pixi/canvas-renderer/-/canvas-renderer-7.4.3.tgz", + "integrity": "sha512-mZnx/tUaNlREh6Yn/z54g0NRvLEdlWIgA5qioeZbJA2HeG+zKEio7hUEOsNIu2o6xzliUoFGzFr7jaS+m7onfg==", + "license": "MIT", + "peerDependencies": { + "@pixi/core": "7.4.3" + } + }, + "node_modules/@pixi/canvas-sprite": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@pixi/canvas-sprite/-/canvas-sprite-7.4.3.tgz", + "integrity": "sha512-Qr17o6AgDihXJGg+lZr8HV5mhvWW46v/+15AY7TFQhJytPv32Y6tJMfojVWquOpzREczMqryun1K5vJpAuinQA==", + "license": "MIT", + "peerDependencies": { + "@pixi/canvas-display": "7.4.3", + "@pixi/canvas-renderer": "7.4.3", + "@pixi/core": "7.4.3", + "@pixi/sprite": "7.4.3" + } + }, + "node_modules/@pixi/canvas-sprite-tiling": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@pixi/canvas-sprite-tiling/-/canvas-sprite-tiling-7.4.3.tgz", + "integrity": "sha512-o8PVghNynaFr2bVcxFj1TB6BrnK27yHDLyHaBhw67cijl0Fjf1uSy5ywVTDCP9ihcK3vadY65tKhQNytuu7uFA==", + "license": "MIT", + "peerDependencies": { + "@pixi/canvas-renderer": "7.4.3", + "@pixi/canvas-sprite": "7.4.3", + "@pixi/core": "7.4.3", + "@pixi/sprite-tiling": "7.4.3" + } + }, + "node_modules/@pixi/canvas-text": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@pixi/canvas-text/-/canvas-text-7.4.3.tgz", + "integrity": "sha512-tbh4+k3Q0rEAhK42KvDhrEA6qSU1rFT4Y/Nj3kvK2QOqO6m+LwkbFPFnoSVRvN6jGJ+wA3RZG1LKS0kxAxoxTA==", + "license": "MIT", + "peerDependencies": { + "@pixi/canvas-sprite": "7.4.3", + "@pixi/sprite": "7.4.3", + "@pixi/text": "7.4.3" + } + }, + "node_modules/@pixi/color": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@pixi/color/-/color-7.4.3.tgz", + "integrity": "sha512-a6R+bXKeXMDcRmjYQoBIK+v2EYqxSX49wcjAY579EYM/WrFKS98nSees6lqVUcLKrcQh2DT9srJHX7XMny3voQ==", + "license": "MIT", + "dependencies": { + "@pixi/colord": "^2.9.6" + } + }, + "node_modules/@pixi/colord": { + "version": "2.9.6", + "resolved": "https://registry.npmjs.org/@pixi/colord/-/colord-2.9.6.tgz", + "integrity": "sha512-nezytU2pw587fQstUu1AsJZDVEynjskwOL+kibwcdxsMBFqPsFFNA7xl0ii/gXuDi6M0xj3mfRJj8pBSc2jCfA==", + "license": "MIT" + }, + "node_modules/@pixi/compressed-textures": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@pixi/compressed-textures/-/compressed-textures-7.4.3.tgz", + "integrity": "sha512-uJ3CC+lNX4HIxs6IxEESO50/0A1KxSVm6CO9UlkXzTsNj9ynmdy5BkJ1dzii7LCdqGcHIXHO01yvKuUbJBBQtw==", + "license": "MIT", + "peerDependencies": { + "@pixi/assets": "7.4.3", + "@pixi/core": "7.4.3" + } + }, + "node_modules/@pixi/constants": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@pixi/constants/-/constants-7.4.3.tgz", + "integrity": "sha512-QGmwJUNQy/vVEHzL6VGQvnwawLZ1wceZMI8HwJAT4/I2uAzbBeFDdmCS8WsTpSWLZjF/DszDc1D8BFp4pVJ5UQ==", + "license": "MIT" + }, + "node_modules/@pixi/core": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@pixi/core/-/core-7.4.3.tgz", + "integrity": "sha512-5YDs11faWgVVTL8VZtLU05/Fl47vaP5Tnsbf+y/WRR0VSW3KhRRGTBU1J3Gdc2xEWbJhUK07KGP7eSZpvtPVgA==", + "license": "MIT", + "dependencies": { + "@pixi/color": "7.4.3", + "@pixi/constants": "7.4.3", + "@pixi/extensions": "7.4.3", + "@pixi/math": "7.4.3", + "@pixi/runner": "7.4.3", + "@pixi/settings": "7.4.3", + "@pixi/ticker": "7.4.3", + "@pixi/utils": "7.4.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/pixijs" + } + }, + "node_modules/@pixi/display": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@pixi/display/-/display-7.4.3.tgz", + "integrity": "sha512-b5m2dAaoNAVdxz1oDaxl3XZ059NEOcNtGkxTOZ4EYCw/jcp9sZXkgSROHRzsGn4k+NugH7+9MP4Id2Z0kkdUhw==", + "license": "MIT", + "peerDependencies": { + "@pixi/core": "7.4.3" + } + }, + "node_modules/@pixi/events": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@pixi/events/-/events-7.4.3.tgz", + "integrity": "sha512-o3j/5Dxq6WDVS6eHfURB/cf/MP+NcsF/eC5PnbSHjXxJmDE7PoTVwLvxexm5uuvNRpFh/6/Fn0V8Vl4gV8sc8w==", + "license": "MIT", + "peerDependencies": { + "@pixi/core": "7.4.3", + "@pixi/display": "7.4.3" + } + }, + "node_modules/@pixi/extensions": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@pixi/extensions/-/extensions-7.4.3.tgz", + "integrity": "sha512-FhoiYkHQEDYHUE7wXhqfsTRz6KxLXjuMbSiAwnLb9uG1vAgp6q6qd6HEsf4X30YaZbLFY8a4KY6hFZWjF+4Fdw==", + "license": "MIT" + }, + "node_modules/@pixi/extract": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@pixi/extract/-/extract-7.4.3.tgz", + "integrity": "sha512-HNvGNrEVaeVsbcnIO1MsHpjZbTwo9nIlaOEBzDGcL6JWwzuB1RnzUke7WUCndCUt91sGUdvPnvgCvy9/NNFg3w==", + "license": "MIT", + "peerDependencies": { + "@pixi/core": "7.4.3" + } + }, + "node_modules/@pixi/filter-alpha": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@pixi/filter-alpha/-/filter-alpha-7.4.3.tgz", + "integrity": "sha512-YFdUB1I53USQb+9TEhS849dV2KZhbnNGIoBbOSThUJfXQc4pDguIFWMagVToAQYgmZ4C4AtYfVjaSEELrMcCdA==", + "license": "MIT", + "peerDependencies": { + "@pixi/core": "7.4.3" + } + }, + "node_modules/@pixi/filter-blur": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@pixi/filter-blur/-/filter-blur-7.4.3.tgz", + "integrity": "sha512-ZFzS9L/whdRbs5A/EUgF3yQaBcxNarmbuwaMgrfnpQ84mRczkGByqDLGToadiufyals07ufTrXBGRle9lbtEDA==", + "license": "MIT", + "peerDependencies": { + "@pixi/core": "7.4.3" + } + }, + "node_modules/@pixi/filter-color-matrix": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@pixi/filter-color-matrix/-/filter-color-matrix-7.4.3.tgz", + "integrity": "sha512-TNu0h20SrzjUWIb5v19dAp1vPpqtG0w2XF9kIHN91bMNaf3R1jzhpWG6TtaVO9eo1IolWcEJLw38jIohyC+KNw==", + "license": "MIT", + "peerDependencies": { + "@pixi/core": "7.4.3" + } + }, + "node_modules/@pixi/filter-displacement": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@pixi/filter-displacement/-/filter-displacement-7.4.3.tgz", + "integrity": "sha512-ax+cFA2mEnKgqf9F8qInpv09GNWzjwnASLETpwPXzWBtlAlNCeHV2tCv3+SlMdEKUkwG9sA7AmjjjC2JBUyt+Q==", + "license": "MIT", + "peerDependencies": { + "@pixi/core": "7.4.3" + } + }, + "node_modules/@pixi/filter-fxaa": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@pixi/filter-fxaa/-/filter-fxaa-7.4.3.tgz", + "integrity": "sha512-y9jhho5cCflhEsPtNqqsd+XJHsb+/ysht4rG/VHQ8Z6pScHYpbgiEpowryGq8uSMQQwx6zKNS2DPiXdiOHPZsg==", + "license": "MIT", + "peerDependencies": { + "@pixi/core": "7.4.3" + } + }, + "node_modules/@pixi/filter-noise": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@pixi/filter-noise/-/filter-noise-7.4.3.tgz", + "integrity": "sha512-rwgSO3BKe1jW/P5CaOcfLKjfpl674aBEo/igi/3QLxA3ORhILNqWRsKkOwP8xF/ejI5NE4rMEkdv0LScbdGFhA==", + "license": "MIT", + "peerDependencies": { + "@pixi/core": "7.4.3" + } + }, + "node_modules/@pixi/graphics": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@pixi/graphics/-/graphics-7.4.3.tgz", + "integrity": "sha512-wWLivD8/URb8A7X4TqCZGG39C91IE+aOuWY/z9NCz5Z6WvA/VWnsc5fLTlO+ggjGHgKF0cSucCXZfUe1wm0AOQ==", + "license": "MIT", + "peerDependencies": { + "@pixi/core": "7.4.3", + "@pixi/display": "7.4.3", + "@pixi/sprite": "7.4.3" + } + }, + "node_modules/@pixi/math": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@pixi/math/-/math-7.4.3.tgz", + "integrity": "sha512-/uJOVhR2DOZ+zgdI6Bs/CwcXT4bNRKsS+TqX3ekRIxPCwaLra+Qdm7aDxT5cTToDzdxbKL5+rwiLu3Y1egILDw==", + "license": "MIT" + }, + "node_modules/@pixi/mesh": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@pixi/mesh/-/mesh-7.4.3.tgz", + "integrity": "sha512-CikqFPtKvU3Zj986/MSoC8X39CWv5CEpiEW/tYp47p4tgQNDSkNWYnDiNYgb+4VX6pNsBrgX4DALLdTR17SlSA==", + "license": "MIT", + "peerDependencies": { + "@pixi/core": "7.4.3", + "@pixi/display": "7.4.3" + } + }, + "node_modules/@pixi/mesh-extras": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@pixi/mesh-extras/-/mesh-extras-7.4.3.tgz", + "integrity": "sha512-EqpxpVZoTObyupxMSzuUsCGmWPQioW84n9EO9Ajawkk/HYA+qKFZ5viKiEThIUBYgv4Apn/7c0U3Feg7Ez4uQQ==", + "license": "MIT", + "peerDependencies": { + "@pixi/core": "7.4.3", + "@pixi/mesh": "7.4.3" + } + }, + "node_modules/@pixi/mixin-cache-as-bitmap": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@pixi/mixin-cache-as-bitmap/-/mixin-cache-as-bitmap-7.4.3.tgz", + "integrity": "sha512-NgvDdgSgd2tfcTSc+SWF12JJjVVz5ZrkSlhX0idSp/LSako82AiFJlD2xqH9GUsEcA6sqBBlnu7nrGkPTHQdhA==", + "license": "MIT", + "peerDependencies": { + "@pixi/core": "7.4.3", + "@pixi/display": "7.4.3", + "@pixi/sprite": "7.4.3" + } + }, + "node_modules/@pixi/mixin-get-child-by-name": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@pixi/mixin-get-child-by-name/-/mixin-get-child-by-name-7.4.3.tgz", + "integrity": "sha512-HLhDxHwafQT+CxbqQx9w9ivJIyAOg9JJ/6m4fNymVuDWeuMGcxDxBD7DukdUYIieT+RD/RlxdPEmq8YoromlFA==", + "license": "MIT", + "peerDependencies": { + "@pixi/display": "7.4.3" + } + }, + "node_modules/@pixi/mixin-get-global-position": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@pixi/mixin-get-global-position/-/mixin-get-global-position-7.4.3.tgz", + "integrity": "sha512-k09kvkS379EypCIWgXMY7uiXtWk1BsaJyTYlV16Co0AsmNPdFd+wUozMx1xV6rxcGiWXsxr/1k9fbETuYkcXCQ==", + "license": "MIT", + "peerDependencies": { + "@pixi/core": "7.4.3", + "@pixi/display": "7.4.3" + } + }, + "node_modules/@pixi/particle-container": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@pixi/particle-container/-/particle-container-7.4.3.tgz", + "integrity": "sha512-0DfJF5C0XTfuI2FsLYvMKCOtqWjXWGOWfA6m4l0W/Ke/qw5zKIOEhgjPLw4qNRtOhmEfkVKJUGp66Ap/ya2YzA==", + "license": "MIT", + "peerDependencies": { + "@pixi/core": "7.4.3", + "@pixi/display": "7.4.3", + "@pixi/sprite": "7.4.3" + } + }, + "node_modules/@pixi/prepare": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@pixi/prepare/-/prepare-7.4.3.tgz", + "integrity": "sha512-OjJHGKXPzwP5OLKxBnTBnKMOktHynLvO0TQPqTYgNtmGQzY109mypCqM4M+s/V+uYmBo/T+sXvBahj98q/f1tA==", + "license": "MIT", + "peerDependencies": { + "@pixi/core": "7.4.3", + "@pixi/display": "7.4.3", + "@pixi/graphics": "7.4.3", + "@pixi/text": "7.4.3" + } + }, + "node_modules/@pixi/runner": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@pixi/runner/-/runner-7.4.3.tgz", + "integrity": "sha512-TJyfp7y23u5vvRAyYhVSa7ytq0PdKSvPLXu4G3meoFh1oxTLHH6g/RIzLuxUAThPG2z7ftthuW3qWq6dRV+dhw==", + "license": "MIT" + }, + "node_modules/@pixi/settings": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@pixi/settings/-/settings-7.4.3.tgz", + "integrity": "sha512-SmGK8smc0PxRB9nr0UJioEtE9hl4gvj9OedCvZx3bxBwA3omA5BmP3CyhQfN8XJ29+o2OUL01r3zAPVol4l4lA==", + "license": "MIT", + "dependencies": { + "@pixi/constants": "7.4.3", + "@types/css-font-loading-module": "^0.0.12", + "ismobilejs": "^1.1.0" + } + }, + "node_modules/@pixi/sprite": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@pixi/sprite/-/sprite-7.4.3.tgz", + "integrity": "sha512-iNBrpOFF9nXDT6m2jcyYy6l/sRzklLDDck1eFHprHZwvNquY2nzRfh+RGBCecxhBcijiLJ3fsZN33fP0LDXkvw==", + "license": "MIT", + "peerDependencies": { + "@pixi/core": "7.4.3", + "@pixi/display": "7.4.3" + } + }, + "node_modules/@pixi/sprite-animated": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@pixi/sprite-animated/-/sprite-animated-7.4.3.tgz", + "integrity": "sha512-mw5YIec8KfO1Jv9qrDNvGoD7Dlmcgww5YlMtd+ARi7Zzo+6ziNw899LXtoaKX1+3BXdZbYNyJAx3C5r30NtwXA==", + "license": "MIT", + "peerDependencies": { + "@pixi/core": "7.4.3", + "@pixi/sprite": "7.4.3" + } + }, + "node_modules/@pixi/sprite-tiling": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@pixi/sprite-tiling/-/sprite-tiling-7.4.3.tgz", + "integrity": "sha512-kUa9cEcMsGXSIZoXA7LhW4oo0eWa30w0KYd7mZ0bqalBMfOcvsGZMN701Lc5lpE8URw+8yu5bnyGLbrxhWBTuw==", + "license": "MIT", + "peerDependencies": { + "@pixi/core": "7.4.3", + "@pixi/display": "7.4.3", + "@pixi/sprite": "7.4.3" + } + }, + "node_modules/@pixi/spritesheet": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@pixi/spritesheet/-/spritesheet-7.4.3.tgz", + "integrity": "sha512-Ce4xZzUxUSKfiROUjjVCBYNLuCcDEWKJ822bSV9rkgVHItu3q04VnEww0DXO+9K0hKv4Ukjjk8aP6Pz0LgPm7A==", + "license": "MIT", + "peerDependencies": { + "@pixi/assets": "7.4.3", + "@pixi/core": "7.4.3" + } + }, + "node_modules/@pixi/text": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@pixi/text/-/text-7.4.3.tgz", + "integrity": "sha512-IAF0iu04rPg3oiL0HZsEZI44fpJxq3UZ4xTmx8l1RyhhSXiElLvvSlSH57vt/BKMQZtCs+AqEit7yn8heK2+nQ==", + "license": "MIT", + "peerDependencies": { + "@pixi/core": "7.4.3", + "@pixi/sprite": "7.4.3" + } + }, + "node_modules/@pixi/text-bitmap": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@pixi/text-bitmap/-/text-bitmap-7.4.3.tgz", + "integrity": "sha512-TnBocJm7f5nMAYwYcsojc62uCrOYauAGH26o3pNrlqmHDRDQ7FzPOGvkYZGYFREbUycloLSRlYpSy0FB9ZdV4Q==", + "license": "MIT", + "peerDependencies": { + "@pixi/assets": "7.4.3", + "@pixi/core": "7.4.3", + "@pixi/display": "7.4.3", + "@pixi/mesh": "7.4.3", + "@pixi/text": "7.4.3" + } + }, + "node_modules/@pixi/text-html": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@pixi/text-html/-/text-html-7.4.3.tgz", + "integrity": "sha512-nm9K9gjSZAU8ETwQZBE3kMGNdO1IzyghxoRTcJCWKhekiGDpUQhopfNhqieNZ7reVJpvhpFQWjbyaHDehndUaQ==", + "license": "MIT", + "peerDependencies": { + "@pixi/core": "7.4.3", + "@pixi/display": "7.4.3", + "@pixi/sprite": "7.4.3", + "@pixi/text": "7.4.3" + } + }, + "node_modules/@pixi/ticker": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@pixi/ticker/-/ticker-7.4.3.tgz", + "integrity": "sha512-tHsAD0iOUb6QSGGw+c8cyRBvxsq/NlfzIFBZLEHhWZ+Bx4a0MmXup6I/yJDGmyPCYE+ctCcAfY13wKAzdiVFgQ==", + "license": "MIT", + "dependencies": { + "@pixi/extensions": "7.4.3", + "@pixi/settings": "7.4.3", + "@pixi/utils": "7.4.3" + } + }, + "node_modules/@pixi/utils": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@pixi/utils/-/utils-7.4.3.tgz", + "integrity": "sha512-NO3Y9HAn2UKS1YdxffqsPp+kDpVm8XWvkZcS/E+rBzY9VTLnNOI7cawSRm+dacdET3a8Jad3aDKEDZ0HmAqAFA==", + "license": "MIT", + "dependencies": { + "@pixi/color": "7.4.3", + "@pixi/constants": "7.4.3", + "@pixi/settings": "7.4.3", + "@types/earcut": "^2.1.0", + "earcut": "^2.2.4", + "eventemitter3": "^4.0.0", + "url": "^0.11.0" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.45.0.tgz", + "integrity": "sha512-2o/FgACbji4tW1dzXOqAV15Eu7DdgbKsF2QKcxfG4xbh5iwU7yr5RRP5/U+0asQliSYv5M4o7BevlGIoSL0LXg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.45.0.tgz", + "integrity": "sha512-PSZ0SvMOjEAxwZeTx32eI/j5xSYtDCRxGu5k9zvzoY77xUNssZM+WV6HYBLROpY5CkXsbQjvz40fBb7WPwDqtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.45.0.tgz", + "integrity": "sha512-BA4yPIPssPB2aRAWzmqzQ3y2/KotkLyZukVB7j3psK/U3nVJdceo6qr9pLM2xN6iRP/wKfxEbOb1yrlZH6sYZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.45.0.tgz", + "integrity": "sha512-Pr2o0lvTwsiG4HCr43Zy9xXrHspyMvsvEw4FwKYqhli4FuLE5FjcZzuQ4cfPe0iUFCvSQG6lACI0xj74FDZKRA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.45.0.tgz", + "integrity": "sha512-lYE8LkE5h4a/+6VnnLiL14zWMPnx6wNbDG23GcYFpRW1V9hYWHAw9lBZ6ZUIrOaoK7NliF1sdwYGiVmziUF4vA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.45.0.tgz", + "integrity": "sha512-PVQWZK9sbzpvqC9Q0GlehNNSVHR+4m7+wET+7FgSnKG3ci5nAMgGmr9mGBXzAuE5SvguCKJ6mHL6vq1JaJ/gvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.45.0.tgz", + "integrity": "sha512-hLrmRl53prCcD+YXTfNvXd776HTxNh8wPAMllusQ+amcQmtgo3V5i/nkhPN6FakW+QVLoUUr2AsbtIRPFU3xIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.45.0.tgz", + "integrity": "sha512-XBKGSYcrkdiRRjl+8XvrUR3AosXU0NvF7VuqMsm7s5nRy+nt58ZMB19Jdp1RdqewLcaYnpk8zeVs/4MlLZEJxw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.45.0.tgz", + "integrity": "sha512-fRvZZPUiBz7NztBE/2QnCS5AtqLVhXmUOPj9IHlfGEXkapgImf4W9+FSkL8cWqoAjozyUzqFmSc4zh2ooaeF6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.45.0.tgz", + "integrity": "sha512-Btv2WRZOcUGi8XU80XwIvzTg4U6+l6D0V6sZTrZx214nrwxw5nAi8hysaXj/mctyClWgesyuxbeLylCBNauimg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.45.0.tgz", + "integrity": "sha512-Li0emNnwtUZdLwHjQPBxn4VWztcrw/h7mgLyHiEI5Z0MhpeFGlzaiBHpSNVOMB/xucjXTTcO+dhv469Djr16KA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.45.0.tgz", + "integrity": "sha512-sB8+pfkYx2kvpDCfd63d5ScYT0Fz1LO6jIb2zLZvmK9ob2D8DeVqrmBDE0iDK8KlBVmsTNzrjr3G1xV4eUZhSw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.45.0.tgz", + "integrity": "sha512-5GQ6PFhh7E6jQm70p1aW05G2cap5zMOvO0se5JMecHeAdj5ZhWEHbJ4hiKpfi1nnnEdTauDXxPgXae/mqjow9w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.45.0.tgz", + "integrity": "sha512-N/euLsBd1rekWcuduakTo/dJw6U6sBP3eUq+RXM9RNfPuWTvG2w/WObDkIvJ2KChy6oxZmOSC08Ak2OJA0UiAA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.45.0.tgz", + "integrity": "sha512-2l9sA7d7QdikL0xQwNMO3xURBUNEWyHVHfAsHsUdq+E/pgLTUcCE+gih5PCdmyHmfTDeXUWVhqL0WZzg0nua3g==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.45.0.tgz", + "integrity": "sha512-XZdD3fEEQcwG2KrJDdEQu7NrHonPxxaV0/w2HpvINBdcqebz1aL+0vM2WFJq4DeiAVT6F5SUQas65HY5JDqoPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.45.0.tgz", + "integrity": "sha512-7ayfgvtmmWgKWBkCGg5+xTQ0r5V1owVm67zTrsEY1008L5ro7mCyGYORomARt/OquB9KY7LpxVBZes+oSniAAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.45.0.tgz", + "integrity": "sha512-B+IJgcBnE2bm93jEW5kHisqvPITs4ddLOROAcOc/diBgrEiQJJ6Qcjby75rFSmH5eMGrqJryUgJDhrfj942apQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.45.0.tgz", + "integrity": "sha512-+CXwwG66g0/FpWOnP/v1HnrGVSOygK/osUbu3wPRy8ECXjoYKjRAyfxYpDQOfghC5qPJYLPH0oN4MCOjwgdMug==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.45.0.tgz", + "integrity": "sha512-SRf1cytG7wqcHVLrBc9VtPK4pU5wxiB/lNIkNmW2ApKXIg+RpqwHfsaEK+e7eH4A1BpI6BX/aBWXxZCIrJg3uA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "node_modules/@types/css-font-loading-module": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@types/css-font-loading-module/-/css-font-loading-module-0.0.12.tgz", + "integrity": "sha512-x2tZZYkSxXqWvTDgveSynfjq/T2HyiZHXb00j/+gy19yp70PHCizM48XFdjBCWH7eHBD0R5i/pw9yMBP/BH5uA==", + "license": "MIT" + }, + "node_modules/@types/earcut": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@types/earcut/-/earcut-2.1.4.tgz", + "integrity": "sha512-qp3m9PPz4gULB9MhjGID7wpo3gJ4bTGXm7ltNDsmOvsPduTeHp8wSW9YckBj3mljeOh4F0m2z/0JKAALRKbmLQ==", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/blockly": { + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/blockly/-/blockly-12.2.0.tgz", + "integrity": "sha512-s4QL9ogEMzc4Pxfe8Oi3Kmu6SQ0ts2thzmRYjdnMSEIVZFpBZ4OUuNKvpFICqujO0yfAo99zON8KzxAFw8hA1w==", + "license": "Apache-2.0", + "dependencies": { + "jsdom": "26.1.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "license": "MIT" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/earcut": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz", + "integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==", + "license": "ISC" + }, + "node_modules/engine.io-client": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz", + "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io-client/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "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 + } + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz", + "integrity": "sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.6", + "@esbuild/android-arm": "0.25.6", + "@esbuild/android-arm64": "0.25.6", + "@esbuild/android-x64": "0.25.6", + "@esbuild/darwin-arm64": "0.25.6", + "@esbuild/darwin-x64": "0.25.6", + "@esbuild/freebsd-arm64": "0.25.6", + "@esbuild/freebsd-x64": "0.25.6", + "@esbuild/linux-arm": "0.25.6", + "@esbuild/linux-arm64": "0.25.6", + "@esbuild/linux-ia32": "0.25.6", + "@esbuild/linux-loong64": "0.25.6", + "@esbuild/linux-mips64el": "0.25.6", + "@esbuild/linux-ppc64": "0.25.6", + "@esbuild/linux-riscv64": "0.25.6", + "@esbuild/linux-s390x": "0.25.6", + "@esbuild/linux-x64": "0.25.6", + "@esbuild/netbsd-arm64": "0.25.6", + "@esbuild/netbsd-x64": "0.25.6", + "@esbuild/openbsd-arm64": "0.25.6", + "@esbuild/openbsd-x64": "0.25.6", + "@esbuild/openharmony-arm64": "0.25.6", + "@esbuild/sunos-x64": "0.25.6", + "@esbuild/win32-arm64": "0.25.6", + "@esbuild/win32-ia32": "0.25.6", + "@esbuild/win32-x64": "0.25.6" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", + "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "license": "MIT" + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/ismobilejs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ismobilejs/-/ismobilejs-1.1.1.tgz", + "integrity": "sha512-VaFW53yt8QO61k2WJui0dHf4SlL8lxBofUuUmwBo0ljPk0Drz2TiuDW4jo3wDcv41qy/SxrJ+VAzJ/qYqsmzRw==", + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/nwsapi": { + "version": "2.2.20", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz", + "integrity": "sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==", + "license": "MIT" + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "license": "(MIT AND Zlib)" + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pixi.js": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/pixi.js/-/pixi.js-7.4.3.tgz", + "integrity": "sha512-uIWdH0EI2dVgNoqN9aFaHCmR0V65OEhMkXs2sek3c/QP2ItV6UoM+ouX9esSv3ibo20F+J5D1XwnQhUZI6wqeQ==", + "license": "MIT", + "dependencies": { + "@pixi/accessibility": "7.4.3", + "@pixi/app": "7.4.3", + "@pixi/assets": "7.4.3", + "@pixi/compressed-textures": "7.4.3", + "@pixi/core": "7.4.3", + "@pixi/display": "7.4.3", + "@pixi/events": "7.4.3", + "@pixi/extensions": "7.4.3", + "@pixi/extract": "7.4.3", + "@pixi/filter-alpha": "7.4.3", + "@pixi/filter-blur": "7.4.3", + "@pixi/filter-color-matrix": "7.4.3", + "@pixi/filter-displacement": "7.4.3", + "@pixi/filter-fxaa": "7.4.3", + "@pixi/filter-noise": "7.4.3", + "@pixi/graphics": "7.4.3", + "@pixi/mesh": "7.4.3", + "@pixi/mesh-extras": "7.4.3", + "@pixi/mixin-cache-as-bitmap": "7.4.3", + "@pixi/mixin-get-child-by-name": "7.4.3", + "@pixi/mixin-get-global-position": "7.4.3", + "@pixi/particle-container": "7.4.3", + "@pixi/prepare": "7.4.3", + "@pixi/sprite": "7.4.3", + "@pixi/sprite-animated": "7.4.3", + "@pixi/sprite-tiling": "7.4.3", + "@pixi/spritesheet": "7.4.3", + "@pixi/text": "7.4.3", + "@pixi/text-bitmap": "7.4.3", + "@pixi/text-html": "7.4.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/pixijs" + } + }, + "node_modules/pixi.js-legacy": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/pixi.js-legacy/-/pixi.js-legacy-7.4.3.tgz", + "integrity": "sha512-vFw/CSk05C4eSOtELeXY8cizjoEx444MW4o7xdwcu6Rxw3UJioJXccp6Vkchvb46xVd+v51pVL6KDpcMAsGa+g==", + "license": "MIT", + "dependencies": { + "@pixi/canvas-display": "7.4.3", + "@pixi/canvas-extract": "7.4.3", + "@pixi/canvas-graphics": "7.4.3", + "@pixi/canvas-mesh": "7.4.3", + "@pixi/canvas-particle-container": "7.4.3", + "@pixi/canvas-prepare": "7.4.3", + "@pixi/canvas-renderer": "7.4.3", + "@pixi/canvas-sprite": "7.4.3", + "@pixi/canvas-sprite-tiling": "7.4.3", + "@pixi/canvas-text": "7.4.3", + "pixi.js": "7.4.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/pixijs" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/rollup": { + "version": "4.45.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.45.0.tgz", + "integrity": "sha512-WLjEcJRIo7i3WDDgOIJqVI2d+lAC3EwvOGy+Xfq6hs+GQuAA4Di/H72xmXkOhrIWFg2PFYSKZYfH0f4vfKXN4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.45.0", + "@rollup/rollup-android-arm64": "4.45.0", + "@rollup/rollup-darwin-arm64": "4.45.0", + "@rollup/rollup-darwin-x64": "4.45.0", + "@rollup/rollup-freebsd-arm64": "4.45.0", + "@rollup/rollup-freebsd-x64": "4.45.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.45.0", + "@rollup/rollup-linux-arm-musleabihf": "4.45.0", + "@rollup/rollup-linux-arm64-gnu": "4.45.0", + "@rollup/rollup-linux-arm64-musl": "4.45.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.45.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.45.0", + "@rollup/rollup-linux-riscv64-gnu": "4.45.0", + "@rollup/rollup-linux-riscv64-musl": "4.45.0", + "@rollup/rollup-linux-s390x-gnu": "4.45.0", + "@rollup/rollup-linux-x64-gnu": "4.45.0", + "@rollup/rollup-linux-x64-musl": "4.45.0", + "@rollup/rollup-win32-arm64-msvc": "4.45.0", + "@rollup/rollup-win32-ia32-msvc": "4.45.0", + "@rollup/rollup-win32-x64-msvc": "4.45.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "license": "MIT" + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/socket.io-client": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz", + "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/url": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.4.tgz", + "integrity": "sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==", + "license": "MIT", + "dependencies": { + "punycode": "^1.4.1", + "qs": "^6.12.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/url/node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/vite": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.0.4.tgz", + "integrity": "sha512-SkaSguuS7nnmV7mfJ8l81JGBFV7Gvzp8IzgE8A8t23+AxuNX61Q5H1Tpz5efduSN7NHC8nQXD3sKQKZAu5mNEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.6", + "picomatch": "^4.0.2", + "postcss": "^8.5.6", + "rollup": "^4.40.0", + "tinyglobby": "^0.2.14" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "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 + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "license": "MIT" + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..4b74d14 --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "rarry-vite", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "devDependencies": { + "vite": "^7.0.4" + }, + "dependencies": { + "@blockly/plugin-strict-connection-checker": "^6.0.1", + "@fortawesome/fontawesome-free": "^7.0.1", + "blockly": "^12.2.0", + "jszip": "^3.10.1", + "pako": "^2.1.0", + "pixi.js-legacy": "^7.4.3", + "socket.io-client": "^4.8.1" + } +} diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..26e3283 Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/icons/NeoIDE.svg b/public/icons/NeoIDE.svg new file mode 100644 index 0000000..f394377 --- /dev/null +++ b/public/icons/NeoIDE.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/icons/R-Rarry.svg b/public/icons/R-Rarry.svg new file mode 100644 index 0000000..c464e1f --- /dev/null +++ b/public/icons/R-Rarry.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/blur.png b/public/icons/blur.png new file mode 100644 index 0000000..0554ebf Binary files /dev/null and b/public/icons/blur.png differ diff --git a/public/icons/ddededodediamante.png b/public/icons/ddededodediamante.png new file mode 100644 index 0000000..bf64d69 Binary files /dev/null and b/public/icons/ddededodediamante.png differ diff --git a/public/icons/flag.svg b/public/icons/flag.svg new file mode 100644 index 0000000..835cee2 --- /dev/null +++ b/public/icons/flag.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/fullscreen.svg b/public/icons/fullscreen.svg new file mode 100644 index 0000000..215ddb3 --- /dev/null +++ b/public/icons/fullscreen.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/icons/left.svg b/public/icons/left.svg new file mode 100644 index 0000000..64d5558 --- /dev/null +++ b/public/icons/left.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/pen.svg b/public/icons/pen.svg new file mode 100644 index 0000000..1fd2e62 --- /dev/null +++ b/public/icons/pen.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/play.svg b/public/icons/play.svg new file mode 100644 index 0000000..12b655d --- /dev/null +++ b/public/icons/play.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/right.svg b/public/icons/right.svg new file mode 100644 index 0000000..7592686 --- /dev/null +++ b/public/icons/right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/sets.svg b/public/icons/sets.svg new file mode 100644 index 0000000..8e1b7d1 --- /dev/null +++ b/public/icons/sets.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/smallscreen.svg b/public/icons/smallscreen.svg new file mode 100644 index 0000000..3cba467 --- /dev/null +++ b/public/icons/smallscreen.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/icons/statement.svg b/public/icons/statement.svg new file mode 100644 index 0000000..34c83ee --- /dev/null +++ b/public/icons/statement.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/stop.svg b/public/icons/stop.svg new file mode 100644 index 0000000..08a6f0d --- /dev/null +++ b/public/icons/stop.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/stopAudio.svg b/public/icons/stopAudio.svg new file mode 100644 index 0000000..7e44afb --- /dev/null +++ b/public/icons/stopAudio.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/terminal.svg b/public/icons/terminal.svg new file mode 100644 index 0000000..ccced24 --- /dev/null +++ b/public/icons/terminal.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/trash.svg b/public/icons/trash.svg new file mode 100644 index 0000000..8685ded --- /dev/null +++ b/public/icons/trash.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/tween.svg b/public/icons/tween.svg new file mode 100644 index 0000000..7fa8799 --- /dev/null +++ b/public/icons/tween.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/signup.html b/signup.html new file mode 100644 index 0000000..838641d --- /dev/null +++ b/signup.html @@ -0,0 +1,46 @@ + + + + + + + Login - NeoIDE + + + + + + + + + + + + + + + + + + +
+ NeoIDE logo + Login to existing account + +

Username

+ + +

Password

+ + + +
+ + + + + \ No newline at end of file diff --git a/src/blocks/control.js b/src/blocks/control.js new file mode 100644 index 0000000..7db7fa9 --- /dev/null +++ b/src/blocks/control.js @@ -0,0 +1,187 @@ +import * as Blockly from "blockly"; +import * as BlocklyJS from "blockly/javascript"; + +Blockly.Blocks["wait_one_frame"] = { + init: function () { + this.appendDummyInput().appendField("wait one frame"); + this.setPreviousStatement(true, "default"); + this.setNextStatement(true, "default"); + this.setStyle("control_blocks"); + }, +}; + +Blockly.Blocks["wait_block"] = { + init: function () { + this.appendValueInput("AMOUNT").setCheck("Number").appendField("wait"); + this.appendDummyInput().appendField( + new Blockly.FieldDropdown([ + ["seconds", "1000"], + ["milliseconds", "1"], + ]), + "MENU" + ); + this.setPreviousStatement(true, "default"); + this.setNextStatement(true, "default"); + this.setStyle("control_blocks"); + }, +}; + +BlocklyJS.javascriptGenerator.forBlock["wait_one_frame"] = function (block) { + return `await waitOneFrame();\n`; +}; + +BlocklyJS.javascriptGenerator.forBlock["wait_block"] = function ( + block, + generator +) { + const duration = + generator.valueToCode(block, "AMOUNT", BlocklyJS.Order.ATOMIC) || 0; + const menu = block.getFieldValue("MENU") || 0; + return `await wait(${duration} * ${+menu});\n`; +}; + +Blockly.Blocks["controls_thread_create"] = { + init: function () { + this.appendDummyInput().appendField("create thread"); + this.appendStatementInput("code").setCheck("default"); + this.setTooltip("Create and run the code specified in a new thread"); + this.setPreviousStatement(true, "default"); + this.setNextStatement(true, "default"); + this.setStyle("control_blocks"); + }, +}; + +BlocklyJS.javascriptGenerator.forBlock["controls_thread_create"] = function ( + block, + generator +) { + const code = generator.statementToCode(block, "code"); + return `Thread.getCurrentContext().spawn(async () => {\n${code}});`; +}; + +Blockly.Blocks["controls_thread_current"] = { + init: function () { + this.appendDummyInput().appendField("current thread"); + this.setOutput(true, "ThreadID"); + this.setStyle("control_blocks"); + this.setTooltip("Return the ID of the currently running thread"); + }, +}; + +BlocklyJS.javascriptGenerator.forBlock["controls_thread_current"] = () => [ + `Thread.getCurrentContext().id`, + BlocklyJS.Order.MEMBER, +]; + +Blockly.Blocks["controls_thread_set_var"] = { + init: function () { + this.appendValueInput("NAME") + .setCheck("String") + .appendField("set variable"); + this.appendValueInput("VALUE").appendField("to"); + this.appendValueInput("THREAD") + .setCheck("ThreadID") + .appendField("in thread"); + this.setPreviousStatement(true, "default"); + this.setNextStatement(true, "default"); + this.setInputsInline(true); + this.setStyle("control_blocks"); + this.setTooltip("Set a variable inside the given thread"); + }, +}; + +BlocklyJS.javascriptGenerator.forBlock["controls_thread_set_var"] = function ( + block, + generator +) { + const threadId = + generator.valueToCode(block, "THREAD", BlocklyJS.Order.NONE) || "null"; + const name = + generator.valueToCode(block, "NAME", BlocklyJS.Order.NONE) || '""'; + const value = + generator.valueToCode(block, "VALUE", BlocklyJS.Order.NONE) || "undefined"; + return `Thread.set(${threadId}, ${name}, ${value});\n`; +}; + +Blockly.Blocks["controls_thread_get_var"] = { + init: function () { + this.appendValueInput("NAME") + .setCheck("String") + .appendField("get variable"); + this.appendValueInput("THREAD") + .setCheck("ThreadID") + .appendField("from thread"); + this.setInputsInline(true); + this.setOutput(true, null); + this.setStyle("control_blocks"); + this.setTooltip("Get a variable from the given thread"); + }, +}; + +BlocklyJS.javascriptGenerator.forBlock["controls_thread_get_var"] = function ( + block, + generator +) { + const threadId = + generator.valueToCode(block, "THREAD", BlocklyJS.Order.NONE) || "null"; + const name = + generator.valueToCode(block, "NAME", BlocklyJS.Order.NONE) || '""'; + const code = `Thread.get(${threadId}, ${name})`; + return [code, BlocklyJS.Order.FUNCTION_CALL]; +}; + +Blockly.Blocks["controls_thread_has_var"] = { + init: function () { + this.appendValueInput("NAME").setCheck("String").appendField("variable"); + this.appendValueInput("THREAD") + .setCheck("ThreadID") + .appendField("exists in thread"); + this.setInputsInline(true); + this.setOutput(true, null); + this.setStyle("control_blocks"); + this.setTooltip("Checks if a variable exists in the given thread"); + }, +}; + +BlocklyJS.javascriptGenerator.forBlock["controls_thread_has_var"] = function ( + block, + generator +) { + const threadId = + generator.valueToCode(block, "THREAD", BlocklyJS.Order.NONE) || "null"; + const name = + generator.valueToCode(block, "NAME", BlocklyJS.Order.NONE) || '""'; + const code = `Thread.has(${threadId}, ${name})`; + return [code, BlocklyJS.Order.FUNCTION_CALL]; +}; + +Blockly.Blocks["controls_run_instantly"] = { + init: function () { + this.appendDummyInput().appendField("run instantly"); + this.appendStatementInput("do"); + this.setPreviousStatement(true); + this.setNextStatement(true); + this.setStyle("control_blocks"); + this.setTooltip("Run inside code without frame delay"); + }, +}; + +BlocklyJS.javascriptGenerator.forBlock["controls_run_instantly"] = function ( + block +) { + const branch = BlocklyJS.javascriptGenerator.statementToCode(block, "do"); + return `let _prevFast = fastExecution; +fastExecution = true; +${branch}fastExecution = _prevFast;\n`; +}; + +Blockly.Blocks["controls_stopscript"] = { + init: function () { + this.appendDummyInput().appendField("stop this script"); + this.setPreviousStatement(true, "default"); + this.setStyle("control_blocks"); + }, +}; + +BlocklyJS.javascriptGenerator.forBlock["controls_stopscript"] = () => + 'throw new Error("shouldStop");\n'; diff --git a/src/blocks/event.js b/src/blocks/event.js new file mode 100644 index 0000000..55638a5 --- /dev/null +++ b/src/blocks/event.js @@ -0,0 +1,172 @@ +import * as Blockly from "blockly"; +import * as BlocklyJS from "blockly/javascript"; + +Blockly.Blocks["when_flag_clicked"] = { + init: function () { + this.appendDummyInput() + .appendField("when") + .appendField( + new Blockly.FieldImage("icons/flag.svg", 25, 25, { + alt: "Green flag", + flipRtl: "FALSE", + }) + ) + .appendField("clicked"); + this.appendStatementInput("DO").setCheck("default"); + this.setStyle("events_blocks"); + }, +}; + +BlocklyJS.javascriptGenerator.forBlock["when_flag_clicked"] = function ( + block, + generator +) { + const branch = generator.statementToCode(block, "DO"); + return `registerEvent("flag", null, async () => {\n${branch}});\n`; +}; + +const normalKeys = [ + ..."abcdefghijklmnopqrstuvwxyz", + ..."abcdefghijklmnopqrstuvwxyz0123456789".toUpperCase(), +]; + +Blockly.Blocks["when_key_clicked"] = { + init: function () { + this.appendDummyInput() + .appendField("when") + .appendField( + new Blockly.FieldDropdown([ + ["any", "any"], + ["space", " "], + ["enter", "Enter"], + ["escape", "Escape"], + ["up arrow", "ArrowUp"], + ["down arrow", "ArrowDown"], + ["left arrow", "ArrowLeft"], + ["right arrow", "ArrowRight"], + ...normalKeys.map((i) => [i, i]), + ]), + "KEY" + ) + .appendField("key pressed"); + this.appendStatementInput("DO").setCheck("default"); + this.setStyle("events_blocks"); + }, +}; + +BlocklyJS.javascriptGenerator.forBlock["when_key_clicked"] = function ( + block, + generator +) { + const key = block.getFieldValue("KEY"); + const safeKey = generator.quote_(key); + const branch = generator.statementToCode(block, "DO"); + return `registerEvent("key", ${safeKey}, async () => {\n${branch}});\n`; +}; + +Blockly.Blocks["when_stage_clicked"] = { + init: function () { + this.appendDummyInput() + .appendField("when stage clicked"); + this.appendStatementInput("DO").setCheck("default"); + this.setStyle("events_blocks"); + }, +}; + +BlocklyJS.javascriptGenerator.forBlock["when_stage_clicked"] = function ( + block, + generator +) { + const branch = generator.statementToCode(block, "DO"); + return `registerEvent("stageClick", null, async () => {\n${branch}});\n`; +}; + +Blockly.Blocks["project_timer"] = { + init: function () { + this.appendDummyInput().appendField("project timer"); + this.setOutput(true, "Number"); + this.setStyle("events_blocks"); + }, +}; + +BlocklyJS.javascriptGenerator.forBlock["project_timer"] = function (block) { + return ["projectTime()", BlocklyJS.Order.NONE]; +}; + +Blockly.Blocks["when_timer_reaches"] = { + init: function () { + this.appendDummyInput() + .appendField("when timer reaches") + .appendField(new Blockly.FieldNumber(2, 0), "VALUE") + .appendField("seconds"); + this.appendStatementInput("DO").setCheck("default"); + this.setStyle("events_blocks"); + }, +}; + +BlocklyJS.javascriptGenerator.forBlock["when_timer_reaches"] = function ( + block, + generator +) { + const value = block.getFieldValue("VALUE"); + const branch = generator.statementToCode(block, "DO"); + return `registerEvent("timer", ${value}, async () => {\n${branch}});\n`; +}; + +Blockly.Blocks["every_seconds"] = { + init: function () { + this.appendDummyInput() + .appendField("every") + .appendField(new Blockly.FieldNumber(2, 0.1), "SECONDS") + .appendField("seconds"); + this.appendStatementInput("DO").setCheck("default"); + this.setStyle("events_blocks"); + }, +}; + +BlocklyJS.javascriptGenerator.forBlock["every_seconds"] = function ( + block, + generator +) { + const seconds = block.getFieldValue("SECONDS"); + const branch = generator.statementToCode(block, "DO"); + return `registerEvent("interval", ${seconds}, async () => {\n${branch}});\n`; +}; + +Blockly.Blocks["when_custom_event_triggered"] = { + init: function () { + this.appendDummyInput() + .appendField("when") + .appendField(new Blockly.FieldTextInput("event_name"), "EVENT") + .appendField("triggered"); + this.appendStatementInput("DO").setCheck("default"); + this.setStyle("events_blocks"); + }, +}; + +BlocklyJS.javascriptGenerator.forBlock["when_custom_event_triggered"] = function ( + block, + generator +) { + const event = generator.quote_(block.getFieldValue("EVENT")); + const branch = generator.statementToCode(block, "DO"); + return `registerEvent("custom", ${event}, async () => {\n${branch}});\n`; +}; + +Blockly.Blocks["trigger_custom_event"] = { + init: function () { + this.appendDummyInput() + .appendField("trigger") + .appendField(new Blockly.FieldTextInput("event_name"), "EVENT"); + this.setPreviousStatement(true, "default"); + this.setNextStatement(true, "default"); + this.setStyle("events_blocks"); + }, +}; + +BlocklyJS.javascriptGenerator.forBlock["trigger_custom_event"] = function ( + block +) { + const event = BlocklyJS.javascriptGenerator.quote_(block.getFieldValue("EVENT")); + return `triggerCustomEvent(${event});\n`; +}; diff --git a/src/blocks/functions.js b/src/blocks/functions.js new file mode 100644 index 0000000..f3d5b66 --- /dev/null +++ b/src/blocks/functions.js @@ -0,0 +1,811 @@ +import * as Blockly from "blockly"; +import * as BlocklyJS from "blockly/javascript"; + +const ARG_BLOCK_TYPE = "FunctionsArgumentBlock"; + +class CustomChecker extends Blockly.ConnectionChecker { + canConnect(a, b, isDragging, opt_distance) { + if (!isDragging) { + return super.canConnect(a, b, isDragging, opt_distance); + } + + const existing = b.targetConnection && b.targetConnection.getSourceBlock(); + + if ( + existing && + existing.type === "functions_argument_block" && + existing.isShadow() + ) { + return false; + } + + return super.canConnect(a, b, isDragging, opt_distance); + } +} + +Blockly.registry.register( + Blockly.registry.Type.CONNECTION_CHECKER, + "CustomChecker", + CustomChecker, + true +); + +class DuplicateOnDrag { + constructor(block) { + this.block = block; + } + + isMovable() { + return true; + } + + startDrag(e) { + const ws = this.block.workspace; + + let typeToCreate = this.block.type; + if (this.block.argType_ === "statement") { + typeToCreate = "functions_statement_argument_block"; + } + + let data = this.block.toCopyData(); + if (data?.blockState) { + data.blockState.type = typeToCreate; + } else { + data.blockState = { type: typeToCreate }; + } + + if (this.block.mutationToDom) { + const mutation = this.block.mutationToDom(); + if (mutation) { + data.blockState.extraState = mutation.outerHTML; + } + } + + this.copy = Blockly.clipboard.paste(data, ws); + this.baseStrat = new Blockly.dragging.BlockDragStrategy(this.copy); + this.copy.setDragStrategy(this.baseStrat); + this.baseStrat.startDrag(e); + } + + drag(e) { + this.block.workspace + .getGesture(e) + .getCurrentDragger() + .setDraggable(this.copy); + this.baseStrat.drag(e); + } + + endDrag(e) { + this.baseStrat?.endDrag(e); + } + + revertDrag(e) { + this.copy?.dispose(); + } +} + +function typeToBlocklyCheck(type) { + return ( + { + string: "String", + number: "Number", + boolean: "Boolean", + array: "Array", + object: "Object", + }[type] || null + ); +} + +function findDuplicateArgNames(types, names) { + const used = {}; + const duplicates = []; + + for (let i = 0; i < types.length; i++) { + const key = types[i] + ":" + names[i]; + if (!names[i]) continue; + + if (used[key]) duplicates.push(i); + else used[key] = true; + } + return duplicates; +} + +function isValidIdentifier(name) { + return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name); +} + +Blockly.Blocks["functions_argument_block"] = { + init() { + if (!this.argType_) this.argType_ = "string"; + if (!this.argName_) this.argName_ = "arg"; + + this.setStyle("procedure_blocks"); + this.appendDummyInput().appendField( + new Blockly.FieldLabel(this.argName_), + "ARG_NAME" + ); + + this.setOutput(true, null); + this.setMovable(true); + this.setDeletable(true); + + setTimeout(() => { + if (this.setDragStrategy && this.isShadow()) { + this.setDragStrategy(new DuplicateOnDrag(this)); + } + }); + }, + + mutationToDom: function () { + const container = Blockly.utils.xml.createElement("mutation"); + container.setAttribute("type", this.argType_ || "string"); + container.setAttribute("name", this.argName_ || "arg"); + return container; + }, + + domToMutation: function (xmlElement) { + const type = xmlElement.getAttribute("type") || "string"; + const name = xmlElement.getAttribute("name") || "arg"; + this.updateType_(type); + this.updateName_(name); + }, + + updateType_: function (type) { + this.argType_ = type; + if (type === "statement") { + this.setOutputShape(3); + this.setOutput(true, ARG_BLOCK_TYPE); + } else { + const outputType = typeToBlocklyCheck(type) || "String"; + this.setOutput(true, [outputType, ARG_BLOCK_TYPE]); + } + }, + + updateName_: function (name) { + this.argName_ = name; + if (this.getField("ARG_NAME")) { + this.setFieldValue(name, "ARG_NAME"); + } else { + this.appendDummyInput().appendField( + new Blockly.FieldLabel(name), + "ARG_NAME" + ); + } + }, +}; + +Blockly.Blocks["functions_statement_argument_block"] = { + init() { + if (!this.argName_) this.argName_ = "arg"; + + this.setStyle("procedure_blocks"); + this.appendDummyInput().appendField( + new Blockly.FieldLabel(this.argName_), + "ARG_NAME" + ); + + this.setNextStatement(true, "default"); + this.setPreviousStatement(true, "default"); + }, + + mutationToDom: function () { + const container = Blockly.utils.xml.createElement("mutation"); + container.setAttribute("name", this.argName_ || "arg"); + return container; + }, + + domToMutation: function (xmlElement) { + const name = xmlElement.getAttribute("name") || "arg"; + this.updateName_(name); + }, + + updateName_: function (name) { + this.argName_ = name; + if (this.getField("ARG_NAME")) { + this.setFieldValue(name, "ARG_NAME"); + } else { + this.appendDummyInput().appendField( + new Blockly.FieldLabel(name), + "ARG_NAME" + ); + } + }, +}; + +Blockly.Blocks["functions_definition"] = { + init: function () { + this.setStyle("procedure_blocks"); + this.setTooltip("Function definition with a variable number of inputs."); + this.setInputsInline(true); + + this.functionId_ = Blockly.utils.idGenerator.genUid(); + this.itemCount_ = 0; + this.argTypes_ = []; + this.argNames_ = []; + this.blockShape_ = "statement"; + this.returnTypes_ = []; + + this.updateShape_(); + this.setMutator( + new Blockly.icons.MutatorIcon(["functions_args_generic"], this) + ); + }, + + mutationToDom: function () { + const container = Blockly.utils.xml.createElement("mutation"); + container.setAttribute("functionid", this.functionId_); + container.setAttribute("items", String(this.itemCount_)); + container.setAttribute("shape", this.blockShape_ || "statement"); + + for (let i = 0; i < this.itemCount_; i++) { + const item = Blockly.utils.xml.createElement("item"); + item.setAttribute("type", this.argTypes_[i]); + item.setAttribute("name", this.argNames_[i]); + container.appendChild(item); + } + + return container; + }, + + domToMutation: function (xmlElement) { + const items = xmlElement.getAttribute("items"); + this.itemCount_ = items ? parseInt(items, 10) : 0; + this.argTypes_ = []; + this.argNames_ = []; + + const children = [...xmlElement.children].filter( + n => n.tagName.toLowerCase() === "item" + ); + for (let i = 0; i < children.length; i++) { + this.argTypes_[i] = children[i].getAttribute("type"); + this.argNames_[i] = children[i].getAttribute("name"); + } + + while (this.argTypes_.length < this.itemCount_) + this.argTypes_.push("label"); + while (this.argNames_.length < this.itemCount_) this.argNames_.push("text"); + + this.functionId_ = + xmlElement.getAttribute("functionid") || + Blockly.utils.idGenerator.genUid(); + this.blockShape_ = xmlElement.getAttribute("shape") || "statement"; + this.updateShape_(); + }, + + saveExtraState: function () { + return { + functionId: this.functionId_, + itemCount: this.itemCount_, + argTypes: this.argTypes_, + argNames: this.argNames_, + shape: this.blockShape_, + returnTypes: this.returnTypes_, + }; + }, + + loadExtraState: function (state) { + this.functionId_ = state.functionId || Blockly.utils.idGenerator.genUid(); + this.itemCount_ = state.itemCount || 0; + this.argTypes_ = state.argTypes || []; + this.argNames_ = state.argNames || []; + this.blockShape_ = state.shape || "statement"; + this.returnTypes_ = state.returnTypes || []; + this.updateShape_(); + }, + + createDefaultArgBlock_: function (type, name = "arg") { + Blockly.Events.disable(); + + let block; + try { + const ws = this.workspace; + block = ws.newBlock("functions_argument_block"); + block.setShadow(true); + block.setEditable(false); + block.updateType_(type); + block.updateName_(name); + + if (ws?.rendered) { + block.initSvg(); + block.render(); + } + } catch (_) {} + + Blockly.Events.enable(); + return block; + }, + + updateShape_: function () { + let savedBody = null; + + const bodyInput = this.getInput("BODY"); + if (bodyInput && bodyInput.connection?.targetConnection) { + savedBody = bodyInput.connection.targetConnection; + } + + if (bodyInput) this.removeInput("BODY"); + if (this.getInput("EMPTY")) this.removeInput("EMPTY"); + if (this.getInput("SHAPE")) this.removeInput("SHAPE"); + + [...this.inputList].forEach(input => { + const connection = input.connection?.targetConnection; + if (connection) connection.getSourceBlock()?.dispose(false); + this.removeInput(input.name); + }); + + let firstArgAdded = this.argTypes_[0] === "label"; + + for (let i = 0; i < this.itemCount_; i++) { + const type = this.argTypes_[i]; + const name = this.argNames_[i]; + + if (type === "label") { + this.appendDummyInput().appendField(new Blockly.FieldLabel(name)); + } else { + const input = this.appendValueInput(name).setCheck( + typeToBlocklyCheck(type) + ); + + if (!firstArgAdded) { + input.appendField("my block with"); + firstArgAdded = true; + } + + const reporter = this.createDefaultArgBlock_(type, name); + reporter.setFieldValue(name, "ARG_NAME"); + + try { + reporter.outputConnection.connect(input.connection); + } catch (e) {} + } + } + + if (this.itemCount_ === 0) { + this.appendDummyInput("EMPTY").appendField("my block"); + } + + const newBody = this.appendStatementInput("BODY").setCheck("default"); + if (savedBody) { + try { + newBody.connection.connect(savedBody); + } catch (e) {} + } + }, + + decompose: function (workspace) { + const containerBlock = workspace.newBlock("functions_args_container"); + if (workspace.rendered) containerBlock.initSvg(); + let connection = containerBlock.getInput("STACK").connection; + + for (let i = 0; i < this.itemCount_; i++) { + const type = this.argTypes_[i] || "label"; + const name = this.argNames_[i] || "text"; + const itemBlock = workspace.newBlock("functions_args_generic"); + itemBlock.setFieldValue(type, "ARG_TYPE"); + itemBlock.setFieldValue(name, "ARG_NAME"); + if (workspace.rendered) itemBlock.initSvg(); + itemBlock.valueConnection_ = null; + + connection.connect(itemBlock.previousConnection); + connection = itemBlock.nextConnection; + } + + containerBlock.setFieldValue(this.blockShape_, "SHAPEMENU"); + + return containerBlock; + }, + + compose: function (containerBlock) { + const newTypes = []; + const newNames = []; + + let itemBlock = containerBlock.getInputTargetBlock("STACK"); + while (itemBlock) { + if (!(itemBlock.isInsertionMarker && itemBlock.isInsertionMarker())) { + const type = itemBlock.getFieldValue("ARG_TYPE"); + const name = itemBlock.getFieldValue("ARG_NAME"); + newTypes.push(type); + newNames.push(name); + } + itemBlock = itemBlock.getNextBlock(); + } + + const dups = findDuplicateArgNames(newTypes, newNames); + + const invalid = []; + for (let i = 0; i < newTypes.length; i++) { + const type = newTypes[i]; + const name = newNames[i]; + if (type !== "label") { + if (!isValidIdentifier(name)) { + invalid.push(i); + } + } + } + + itemBlock = containerBlock.getInputTargetBlock("STACK"); + let index = 0; + while (itemBlock) { + if (!(itemBlock.isInsertionMarker && itemBlock.isInsertionMarker())) { + if (dups.includes(index)) { + itemBlock.setWarningText( + "This argument name is already used for this type." + ); + } else if (invalid.includes(index)) { + itemBlock.setWarningText("This argument name is not a valid."); + } else { + itemBlock.setWarningText(null); + } + + index++; + } + itemBlock = itemBlock.getNextBlock(); + } + + const newBlockShape = + containerBlock.getFieldValue("SHAPEMENU") || "statement"; + + if (dups.length > 0 || invalid.length > 0) return; + + this.itemCount_ = newTypes.length; + this.argTypes_ = newTypes; + this.argNames_ = newNames; + this.blockShape_ = newBlockShape; + + this.updateShape_(); + }, + + saveConnections: function (containerBlock) { + let itemBlock = containerBlock.getInputTargetBlock("STACK"); + let i = 0; + while (itemBlock) { + if (!(itemBlock.isInsertionMarker && itemBlock.isInsertionMarker())) { + const key = this.argTypes_[i] + "_" + this.argNames_[i]; + const input = this.getInput(key); + itemBlock.valueConnection_ = + input && input.connection && input.connection.targetConnection; + i++; + } + itemBlock = itemBlock.getNextBlock(); + } + }, + + updateReturnState_: function () { + const body = this.getInputTargetBlock("BODY"); + const types = new Set(); + + function walk(block) { + if (!block) return; + + if (block?.childBlocks_?.length > 0) block?.childBlocks_.forEach(walk); + + if (block.type === "functions_return") { + const val = block.getInputTargetBlock("VALUE"); + const checks = val?.outputConnection?.check; + if (checks !== undefined) { + (Array.isArray(checks) ? checks : [checks]).forEach(t => + types.add(t) + ); + } + } + + walk(block.getNextBlock()); + } + walk(body); + + if (types.size === 0) this.returnTypes_ = []; + else this.returnTypes_ = [...types]; + }, +}; + +Blockly.Blocks["functions_args_container"] = { + init: function () { + this.setStyle("procedure_blocks"); + this.appendDummyInput().appendField("arguments"); + this.appendStatementInput("STACK"); + this.appendDummyInput() + .appendField("shape") + .appendField( + new Blockly.FieldDropdown([ + [ + { + src: "icons/statement.svg", + width: 98 * 0.6, + height: 57 * 0.6, + alt: "A block with top and bottom connections", + }, + "statement", + ], + [ + { + src: "icons/terminal.svg", + width: 98 * 0.6, + height: 48 * 0.6, + alt: "A block with only a top connection", + }, + "terminal", + ], + ]), + "SHAPEMENU" + ); + this.contextMenu = false; + }, +}; + +Blockly.Blocks["functions_args_generic"] = { + init() { + this.setStyle("procedure_blocks"); + + this.appendDummyInput() + .appendField("argument") + .appendField( + new Blockly.FieldDropdown([ + ["label", "label"], + ["string", "string"], + ["number", "number"], + ["boolean", "boolean"], + ["array", "array"], + ["object", "object"], + ["statement", "statement"], + ]), + "ARG_TYPE" + ) + .appendField(new Blockly.FieldTextInput("arg"), "ARG_NAME"); + + this.setPreviousStatement(true); + this.setNextStatement(true); + this.contextMenu = false; + this.valueConnection_ = null; + }, +}; + +Blockly.Blocks["functions_call"] = { + init: function () { + this.setStyle("procedure_blocks"); + this.setInputsInline(true); + + this.functionId_ = null; + this.blockShape_ = null; + this.argTypes_ = []; + this.argNames_ = []; + this.previousArgTypes_ = []; + this.previousArgNames_ = []; + this.returnTypes_ = []; + + this.updateShape_(); + }, + + mutationToDom: function () { + const container = Blockly.utils.xml.createElement("mutation"); + container.setAttribute("functionid", this.functionId_); + container.setAttribute("items", this.argTypes_.length); + container.setAttribute("shape", this.blockShape_ || "statement"); + container.setAttribute( + "returntypes", + JSON.stringify(this.returnTypes_ || []) + ); + + for (let i = 0; i < this.argTypes_.length; i++) { + const item = Blockly.utils.xml.createElement("item"); + item.setAttribute("type", this.argTypes_[i]); + item.setAttribute("name", this.argNames_[i]); + container.appendChild(item); + } + + return container; + }, + + domToMutation: function (xmlElement) { + this.functionId_ = xmlElement.getAttribute("functionid"); + this.blockShape_ = xmlElement.getAttribute("shape") || "statement"; + this.previousArgTypes_ = [...this.argTypes_]; + this.previousArgNames_ = [...this.argNames_]; + this.argTypes_ = []; + this.argNames_ = []; + + this.returnTypes_; + try { + this.returnTypes_ = JSON.parse( + xmlElement.getAttribute("returntypes") || "[]" + ); + } catch { + this.returnTypes_ = []; + } + + const items = parseInt(xmlElement.getAttribute("items") || "0", 10); + for (let i = 0; i < items; i++) { + const item = xmlElement.children[i]; + this.argTypes_[i] = item.getAttribute("type"); + this.argNames_[i] = item.getAttribute("name"); + } + + this.updateShape_(); + }, + + matchDefinition: function (defBlock) { + this.functionId_ = defBlock.functionId_; + this.previousArgTypes_ = [...this.argTypes_]; + this.previousArgNames_ = [...this.argNames_]; + this.argTypes_ = [...defBlock.argTypes_]; + this.argNames_ = [...defBlock.argNames_]; + this.blockShape_ = defBlock.blockShape_; + this.returnTypes_ = [...defBlock.returnTypes_]; + + this.updateShape_(); + if (defBlock.workspace.rendered) this.render(); + }, + + updateShape_: function () { + const oldConnections = {}; + + [...this.inputList].forEach(input => { + if (input.connection && input.connection.targetBlock()) { + oldConnections[input.name] = input.connection.targetConnection; + } + this.removeInput(input.name); + }); + + const shape = this.blockShape_ || "statement"; + const nextConn = this.nextConnection; + const prevConn = this.previousConnection; + const outputConn = this.outputConnection; + const returnTypes = this.returnTypes_ || []; + + if (returnTypes?.length > 0) { + if (prevConn && prevConn.isConnected()) { + const blockAbove = prevConn.targetBlock(); + blockAbove.unplug(true); + } + if (nextConn && nextConn.isConnected()) { + const blockBelow = nextConn.targetBlock(); + blockBelow.unplug(true); + } + + this.setPreviousStatement(false); + this.setNextStatement(false); + this.setOutput(true, returnTypes); + } else { + if (outputConn && outputConn.isConnected()) { + outputConn.disconnect(); + } + + if (shape === "statement") { + this.setPreviousStatement(true, "default"); + this.setNextStatement(true, "default"); + this.setOutput(false); + } else if (shape === "terminal") { + if (nextConn && nextConn.isConnected()) { + nextConn.targetBlock().unplug(true); + } + + this.setNextStatement(false); + this.setPreviousStatement(true, "default"); + this.setOutput(false); + } + } + + if (!this.argTypes_ || this.argTypes_.length === 0) { + this.appendDummyInput("EMPTY").appendField("my block"); + return; + } + + let firstLabel = this.argTypes_[0] === "label"; + + for (let i = 0; i < this.argTypes_.length; i++) { + const type = this.argTypes_[i]; + const name = this.argNames_[i]; + + if (!type || !name) continue; + + if (type === "label") { + this.appendDummyInput().appendField(name); + continue; + } + + if (!firstLabel) { + this.appendDummyInput().appendField("my block with"); + firstLabel = true; + } + + let input; + const key = type + "_" + name; + if (type === "statement") { + input = this.appendStatementInput(key).setCheck("default"); + } else { + input = this.appendValueInput(key).setCheck(typeToBlocklyCheck(type)); + } + + if (oldConnections[key]) { + try { + input.connection.connect( + oldConnections[key].targetBlock()?.outputConnection || + oldConnections[key] + ); + } catch (e) {} + } + } + }, +}; + +Blockly.Blocks["functions_return"] = { + init() { + this.setStyle("procedure_blocks"); + this.appendValueInput("VALUE").appendField("return"); + this.setPreviousStatement(true, "default"); + this.setNextStatement(false); + this.setInputsInline(true); + }, + + update_() { + const def = this.getSurroundParent(); + if (!def || def.type !== "functions_definition") return; + + def.updateReturnState_(); + def.workspace.updateAllFunctionCalls(); + }, + + onchange(e) { + if (e.isUiEvent || e.isBlank) return; + + this.update_(); + }, +}; + +BlocklyJS.javascriptGenerator.forBlock["functions_argument_block"] = block => [ + block.argType_ + "_" + block.argName_, + BlocklyJS.Order.NONE, +]; + +BlocklyJS.javascriptGenerator.forBlock["functions_statement_argument_block"] = + block => "statement_" + block.argName_ + "();\n"; + +BlocklyJS.javascriptGenerator.forBlock["functions_definition"] = function ( + block, + generator +) { + const params = block.argTypes_ + .map((type, i) => { + if (type === "label") return null; + return type + "_" + block.argNames_[i]; + }) + .filter(Boolean); + + const body = BlocklyJS.javascriptGenerator.statementToCode(block, "BODY"); + return `MyFunctions[${generator.quote_( + block.functionId_ + )}] = async (${params.join(", ")}) => {\n${body}};\n`; +}; + +BlocklyJS.javascriptGenerator.forBlock["functions_call"] = function ( + block, + generator +) { + const args = []; + + for (let i = 0; i < block.argTypes_.length; i++) { + const type = block.argTypes_[i]; + const name = block.argNames_[i]; + const key = `${type}_${name}`; + + if (type === "label") continue; + + if (type === "statement") + args.push(`async () => {${generator.statementToCode(block, key)}}`); + else + args.push( + generator.valueToCode(block, key, BlocklyJS.Order.NONE) || "null" + ); + } + + return `await MyFunctions[${generator.quote_(block.functionId_)}](${args.join( + ", " + )});\n`; +}; + +BlocklyJS.javascriptGenerator.forBlock["functions_return"] = function ( + block, + generator +) { + const value = generator.valueToCode(block, "VALUE", BlocklyJS.Order.NONE); + return `return ${value || "null"};\n`; +}; diff --git a/src/blocks/json.js b/src/blocks/json.js new file mode 100644 index 0000000..2fc014c --- /dev/null +++ b/src/blocks/json.js @@ -0,0 +1,512 @@ +import * as Blockly from "blockly"; +import * as BlocklyJS from "blockly/javascript"; +const xmlUtils = Blockly.utils.xml; + +Blockly.Blocks["json_get"] = { + init: function () { + this.appendValueInput("KEY").setCheck("String").appendField("value of"); + this.appendValueInput("OBJECT").setCheck("Object").appendField("in object"); + this.setOutput(true); + this.setInputsInline(true); + this.setStyle("json_category"); + this.setTooltip("Returns the value of a key from a JSON object."); + }, +}; + +BlocklyJS.javascriptGenerator.forBlock["json_get"] = function (block) { + const obj = + BlocklyJS.javascriptGenerator.valueToCode( + block, + "OBJECT", + BlocklyJS.Order.MEMBER + ) || "{}"; + const key = + BlocklyJS.javascriptGenerator.valueToCode( + block, + "KEY", + BlocklyJS.Order.NONE + ) || '""'; + return [`${obj}[${key}]`, BlocklyJS.Order.MEMBER]; +}; + +Blockly.Blocks["json_set"] = { + init: function () { + this.appendValueInput("OBJECT").setCheck("Object").appendField("in object"); + this.appendValueInput("KEY").setCheck("String").appendField("set"); + this.appendValueInput("VALUE").appendField("to"); + this.setPreviousStatement(true, "default"); + this.setNextStatement(true, "default"); + this.setInputsInline(true); + this.setStyle("json_category"); + this.setTooltip("Sets a value to a key in a JSON object."); + }, +}; + +BlocklyJS.javascriptGenerator.forBlock["json_set"] = function (block) { + const obj = + BlocklyJS.javascriptGenerator.valueToCode( + block, + "OBJECT", + BlocklyJS.Order.MEMBER + ) || "{}"; + const key = + BlocklyJS.javascriptGenerator.valueToCode( + block, + "KEY", + BlocklyJS.Order.NONE + ) || '""'; + const value = + BlocklyJS.javascriptGenerator.valueToCode( + block, + "VALUE", + BlocklyJS.Order.ASSIGNMENT + ) || "null"; + return `${obj}[${key}] = ${value};\n`; +}; + +Blockly.Blocks["json_delete"] = { + init: function () { + this.appendValueInput("OBJECT").setCheck("Object").appendField("in object"); + this.appendValueInput("KEY").setCheck("String").appendField("remove"); + this.setPreviousStatement(true); + this.setNextStatement(true); + this.setInputsInline(true); + this.setStyle("json_category"); + this.setTooltip("Deletes a key from a JSON object."); + }, +}; + +BlocklyJS.javascriptGenerator.forBlock["json_delete"] = function (block) { + const obj = + BlocklyJS.javascriptGenerator.valueToCode( + block, + "OBJECT", + BlocklyJS.Order.MEMBER + ) || "{}"; + const key = + BlocklyJS.javascriptGenerator.valueToCode( + block, + "KEY", + BlocklyJS.Order.NONE + ) || '""'; + return `delete ${obj}[${key}];\n`; +}; + +Blockly.Blocks["json_create_item"] = { + init: function () { + this.appendDummyInput().appendField("key and value"); + this.setPreviousStatement(true); + this.setNextStatement(true); + this.setStyle("json_category"); + this.setTooltip("Add a key with a value to the object."); + this.contextMenu = false; + }, +}; + +/* --- start deprecated --- */ +Blockly.Blocks["json_key_value"] = { + init: function () { + this.appendValueInput("KEY").setCheck("String").appendField("key"); + this.appendValueInput("VALUE").setCheck(null).appendField("value"); + this.setInputsInline(true); + this.setStyle("json_category"); + this.setTooltip("A single key with a value."); + this.setOutput(true, "ObjectItem"); + this.setInputsInline(true); + }, +}; + +BlocklyJS.javascriptGenerator.forBlock["json_key_value"] = function (block) { + const keyCode = + BlocklyJS.javascriptGenerator.valueToCode( + block, + "KEY", + BlocklyJS.Order.NONE + ) || '""'; + const valCode = + BlocklyJS.javascriptGenerator.valueToCode( + block, + "VALUE", + BlocklyJS.Order.ATOMIC + ) || "null"; + const code = `${keyCode}: ${valCode}`; + return [code, BlocklyJS.Order.ATOMIC]; +}; + +Blockly.Blocks["json_create"] = { + init: function () { + this.setOutput(true, "Object"); + this.setStyle("json_category"); + this.itemCount_ = 0; + this.updateShape_(); + this.setMutator(new Blockly.icons.MutatorIcon(["json_create_item"], this)); + this.setTooltip("Create a JSON object with any number of keys."); + this.setInputsInline(false); + }, + mutationToDom: function () { + const container = xmlUtils.createElement("mutation"); + container.setAttribute("items", String(this.itemCount_)); + return container; + }, + domToMutation: function (xmlElement) { + const items = xmlElement.getAttribute("items"); + if (!items) throw new TypeError("element did not have items"); + this.itemCount_ = parseInt(items, 10); + this.updateShape_(); + }, + saveExtraState: function () { + return { + itemCount: this.itemCount_, + }; + }, + loadExtraState: function (state) { + this.itemCount_ = state["itemCount"]; + this.updateShape_(); + }, + saveConnections: function (containerBlock) { + let itemBlock = containerBlock.getInputTargetBlock("STACK"); + let i = 0; + while (itemBlock) { + const input = this.getInput("ITEM" + i); + itemBlock.valueConnection_ = input && input.connection.targetConnection; + itemBlock = itemBlock.nextConnection?.targetBlock(); + i++; + } + }, + compose: function (containerBlock) { + let itemBlock = containerBlock.getInputTargetBlock("STACK"); + const connections = []; + while (itemBlock) { + if (itemBlock.isInsertionMarker()) { + itemBlock = itemBlock.getNextBlock(); + continue; + } + connections.push(itemBlock.valueConnection_); + itemBlock = itemBlock.getNextBlock(); + } + for (let i = 0; i < this.itemCount_; i++) { + const connection = this.getInput("ADD" + i)?.connection?.targetConnection; + if (connection && !connections.includes(connection)) { + connection.disconnect(); + } + } + this.itemCount_ = connections.length; + this.updateShape_(); + for (let i = 0; i < this.itemCount_; i++) { + connections[i]?.reconnect(this, "ADD" + i); + } + }, + decompose: function (workspace) { + const containerBlock = workspace.newBlock("json_create_container"); + containerBlock.initSvg(); + let connection = containerBlock.getInput("STACK").connection; + for (let i = 0; i < this.itemCount_; i++) { + const itemBlock = workspace.newBlock("json_create_item"); + itemBlock.initSvg(); + if (!itemBlock.previousConnection) { + throw new Error("itemBlock has no previousConnection"); + } + connection.connect(itemBlock.previousConnection); + connection = itemBlock.nextConnection; + } + return containerBlock; + }, + saveConnections: function (containerBlock) { + let itemBlock = containerBlock.getInputTargetBlock("STACK"); + let i = 0; + while (itemBlock) { + if (itemBlock.isInsertionMarker()) { + itemBlock = itemBlock.getNextBlock(); + continue; + } + const input = this.getInput("ADD" + i); + itemBlock.valueConnection_ = input?.connection?.targetConnection; + itemBlock = itemBlock?.getNextBlock(); + i++; + } + }, + updateShape_: function () { + if (this.itemCount_ && this.getInput("EMPTY")) { + this.removeInput("EMPTY"); + } else if (!this.itemCount_ && !this.getInput("EMPTY")) { + this.appendDummyInput("EMPTY").appendField("create empty object"); + } + + for (let i = 0; i < this.itemCount_; i++) { + if (!this.getInput("ADD" + i)) { + const input = this.appendValueInput("ADD" + i).setAlign( + Blockly.inputs.Align.RIGHT + ); + input.setCheck("ObjectItem"); + if (i === 0) input.appendField("create object with"); + } + } + + for (let i = this.itemCount_; this.getInput("ADD" + i); i++) { + this.removeInput("ADD" + i); + } + }, +}; + +Blockly.Blocks["json_create_container"] = { + init: function () { + this.appendDummyInput().appendField("object"); + this.appendStatementInput("STACK"); + this.setStyle("json_category"); + this.setTooltip( + "Add, remove, or reorder sections to configure this object block." + ); + this.contextMenu = false; + }, +}; + +BlocklyJS.javascriptGenerator.forBlock["json_create"] = function (block) { + const entries = []; + for (let i = 0; i < block.itemCount_; i++) { + const pairCode = BlocklyJS.javascriptGenerator.valueToCode( + block, + "ADD" + i, + BlocklyJS.Order.NONE + ); + if (pairCode) { + entries.push(pairCode || ""); + } + } + const code = `{ ${entries.join(", ")} }`; + return [code, BlocklyJS.Order.ATOMIC]; +}; +/* --- end deprecated --- */ + +Blockly.Blocks["json_create_statement"] = { + init: function () { + this.appendDummyInput().appendField("create object"); + this.appendStatementInput("STACK").setCheck("json_key_value"); + this.setOutput(true, "Object"); + this.setStyle("json_category"); + this.setTooltip("Create a JSON object using stacked key/value pairs."); + }, +}; + +BlocklyJS.javascriptGenerator.forBlock["json_create_statement"] = function ( + block +) { + const statements = BlocklyJS.javascriptGenerator.statementToCode( + block, + "STACK" + ); + return [`{\n${statements}}`, BlocklyJS.Order.ATOMIC]; +}; + +Blockly.Blocks["json_key_value_statement"] = { + init: function () { + this.appendValueInput("KEY").setCheck("String").appendField("key"); + this.appendValueInput("VALUE").appendField("value"); + this.setPreviousStatement(true, "json_key_value"); + this.setNextStatement(true, "json_key_value"); + this.setInputsInline(true); + this.setStyle("json_category"); + this.setTooltip("A single key/value pair for a JSON object."); + }, +}; + +BlocklyJS.javascriptGenerator.forBlock["json_key_value_statement"] = function ( + block, + generator +) { + const key = + BlocklyJS.javascriptGenerator.valueToCode( + block, + "KEY", + BlocklyJS.Order.ATOMIC + ) || ""; + const value = + BlocklyJS.javascriptGenerator.valueToCode( + block, + "VALUE", + BlocklyJS.Order.ATOMIC + ) || "null"; + + if (key) return `${key}: ${value},\n`; + else return ""; +}; + +Blockly.Blocks["json_has_key"] = { + init: function () { + this.appendValueInput("OBJECT") + .setCheck("Object") + .appendField("does object"); + this.appendValueInput("KEY").setCheck("String").appendField("have"); + this.setOutput(true, "Boolean"); + this.setInputsInline(true); + this.setStyle("json_category"); + this.setTooltip("Returns true if the key exists in the object."); + }, +}; + +BlocklyJS.javascriptGenerator.forBlock["json_has_key"] = function (block) { + const obj = + BlocklyJS.javascriptGenerator.valueToCode( + block, + "OBJECT", + BlocklyJS.Order.MEMBER + ) || "{}"; + const key = + BlocklyJS.javascriptGenerator.valueToCode( + block, + "KEY", + BlocklyJS.Order.NONE + ) || '""'; + return [`(${key} in ${obj})`, BlocklyJS.Order.RELATIONAL]; +}; + +Blockly.Blocks["json_property_list"] = { + init: function () { + this.appendValueInput("OBJECT") + .setCheck("Object") + .appendField( + new Blockly.FieldDropdown([ + ["keys", "KEYS"], + ["values", "VALUES"], + ["entries", "ENTRIES"], + ]), + "MODE" + ) + .appendField("of"); + this.setOutput(true, "Array"); + this.setStyle("json_category"); + this.setTooltip("Gets keys, values, or entries of the JSON object."); + }, +}; + +BlocklyJS.javascriptGenerator.forBlock["json_property_list"] = function ( + block +) { + const obj = + BlocklyJS.javascriptGenerator.valueToCode( + block, + "OBJECT", + BlocklyJS.Order.FUNCTION_CALL + ) || "{}"; + const mode = block.getFieldValue("MODE"); + + let code; + switch (mode) { + case "KEYS": + code = `Object.keys(${obj})`; + break; + case "VALUES": + code = `Object.values(${obj})`; + break; + case "ENTRIES": + code = `Object.entries(${obj})`; + break; + default: + code = "[]"; + } + + return [code, BlocklyJS.Order.FUNCTION_CALL]; +}; + +Blockly.Blocks["json_parse"] = { + init: function () { + const dropdown = new Blockly.FieldDropdown([ + ["object from text", "PARSE"], + ["text from object", "STRINGIFY"], + ]); + dropdown.setValidator((newMode) => { + this.updateType_(newMode); + }); + + this.setStyle("json_category"); + this.appendValueInput("INPUT") + .setCheck("String") + .appendField("make") + .appendField(dropdown, "MODE"); + this.setInputsInline(true); + this.setOutput(true, "Object"); + this.setTooltip(() => { + const mode = this.getFieldValue("MODE"); + if (mode === "PARSE") { + return "Convert a stringified object into an object."; + } else if (mode === "STRINGIFY") { + return "Convert an object into text representing an object."; + } + throw Error("Unknown mode: " + mode); + }); + }, + updateType_: function (newMode) { + const mode = this.getFieldValue("MODE"); + if (mode !== newMode) { + const inputConnection = this.getInput("INPUT")?.connection; + inputConnection?.setShadowDom(null); + const inputBlock = inputConnection?.targetBlock(); + + if (inputBlock) { + inputConnection.disconnect(); + if (inputBlock.isShadow()) { + inputBlock.dispose(false); + } else { + this.bumpNeighbours(); + } + } + } + if (newMode === "PARSE") { + this.outputConnection.setCheck("Object"); + this.getInput("INPUT").setCheck("String"); + } else { + this.outputConnection.setCheck("String"); + this.getInput("INPUT").setCheck("Object"); + } + }, + mutationToDom: function () { + const container = xmlUtils.createElement("mutation"); + container.setAttribute("mode", this.getFieldValue("MODE")); + return container; + }, + domToMutation: function (xmlElement) { + this.updateType_(xmlElement.getAttribute("mode")); + }, + saveExtraState: function () { + return { mode: this.getFieldValue("MODE") }; + }, + loadExtraState: function (state) { + this.updateType_(state["mode"]); + }, +}; + +BlocklyJS.javascriptGenerator.forBlock["json_parse"] = function (block) { + const input = + BlocklyJS.javascriptGenerator.valueToCode( + block, + "INPUT", + BlocklyJS.Order.ATOMIC + ) || "null"; + const mode = block.getFieldValue("MODE"); + + if (mode === "PARSE") { + return [`JSON.parse(${input})`, BlocklyJS.Order.NONE]; + } else if (mode === "STRINGIFY") { + return [`JSON.stringify(${input})`, BlocklyJS.Order.NONE]; + } +}; + +Blockly.Blocks["json_clone"] = { + init: function () { + this.appendValueInput("OBJECT") + .setCheck("Object") + .appendField("clone object"); + this.setOutput(true, "Object"); + this.setStyle("json_category"); + this.setTooltip("Creates a duplicate of a JSON object."); + }, +}; + +BlocklyJS.javascriptGenerator.forBlock["json_clone"] = function (block) { + const obj = + BlocklyJS.javascriptGenerator.valueToCode( + block, + "OBJECT", + BlocklyJS.Order.ATOMIC + ) || "{}"; + return [`JSON.parse(JSON.stringify(${obj}))`, BlocklyJS.Order.FUNCTION_CALL]; +}; diff --git a/src/blocks/list.js b/src/blocks/list.js new file mode 100644 index 0000000..dd1b98a --- /dev/null +++ b/src/blocks/list.js @@ -0,0 +1,454 @@ +import * as Blockly from "blockly"; +import * as BlocklyJS from "blockly/javascript"; + +Blockly.Blocks["lists_filter"] = { + init: function () { + this.appendValueInput("list").setCheck("Array").appendField("filter list"); + this.appendValueInput("method").setCheck("Boolean").appendField("by"); + this.setInputsInline(true); + this.setOutput(true, "Array"); + this.setStyle("list_blocks"); + this.setTooltip( + "Remove all items in a list which doesn't match the boolean" + ); + }, +}; + +BlocklyJS.javascriptGenerator.forBlock["lists_filter"] = function ( + block, + generator +) { + var val_list = generator.valueToCode(block, "list", BlocklyJS.Order.ATOMIC); + var val_method = generator.valueToCode( + block, + "method", + BlocklyJS.Order.ATOMIC + ); + var code = `${val_list}.filter(findOrFilterItem => ${val_method})`; + return [code, BlocklyJS.Order.NONE]; +}; + +Blockly.Blocks["lists_find"] = { + init: function () { + this.appendValueInput("list").setCheck("Array").appendField("in list"); + this.appendValueInput("method") + .setCheck("Boolean") + .appendField("find first that matches"); + this.setOutput(true, null); + this.setInputsInline(true); + this.setStyle("list_blocks"); + this.setTooltip( + "Returns the first item in a list that matches the boolean" + ); + }, +}; + +BlocklyJS.javascriptGenerator.forBlock["lists_find"] = function ( + block, + generator +) { + var val_list = generator.valueToCode(block, "list", BlocklyJS.Order.ATOMIC); + var val_method = generator.valueToCode( + block, + "method", + BlocklyJS.Order.ATOMIC + ); + var code = `${val_list}.find(findOrFilterItem => ${val_method})`; + return [code, BlocklyJS.Order.NONE]; +}; + +Blockly.Blocks["lists_filter_item"] = { + init: function () { + this.appendDummyInput("name").appendField("item in loop"); + this.setInputsInline(true); + this.setOutput(true, null); + this.setStyle("list_blocks"); + }, +}; + +BlocklyJS.javascriptGenerator.forBlock["lists_filter_item"] = () => [ + "findOrFilterItem", + BlocklyJS.Order.NONE, +]; + +Blockly.Blocks["lists_merge"] = { + init: function () { + this.appendValueInput("list").setCheck("Array").appendField("merge list"); + this.appendValueInput("list2").setCheck("Array").appendField("with"); + this.setInputsInline(true); + this.setOutput(true, "Array"); + this.setStyle("list_blocks"); + }, +}; + +BlocklyJS.javascriptGenerator.forBlock["lists_merge"] = function ( + block, + generator +) { + const val_list = generator.valueToCode(block, "list", BlocklyJS.Order.ATOMIC); + const val_list2 = generator.valueToCode( + block, + "list2", + BlocklyJS.Order.ATOMIC + ); + const code = `${val_list}.concat(${val_list2})`; + return [code, BlocklyJS.Order.NONE]; +}; + +Blockly.Blocks["lists_foreach"] = { + init: function () { + this.appendValueInput("LIST") + .setCheck("Array") + .appendField("for each item in list"); + this.appendStatementInput("DO").appendField("do"); + this.setInputsInline(false); + this.setPreviousStatement(true, null); + this.setNextStatement(true, null); + this.setStyle("list_blocks"); + this.setTooltip( + "Loops through every item in a list and runs the code inside for each one" + ); + }, +}; + +BlocklyJS.javascriptGenerator.forBlock["lists_foreach"] = function ( + block, + generator +) { + const list = + generator.valueToCode(block, "LIST", BlocklyJS.Order.NONE) || "[]"; + const branch = generator.statementToCode(block, "DO"); + const code = `${list}.forEach(findOrFilterItem => {\n${branch}});\n`; + return code; +}; + +Blockly.Blocks["lists_getIndex_modified"] = { + init: function () { + const MODE_OPTIONS = [ + [Blockly.Msg.LISTS_GET_INDEX_GET, "GET"], + [Blockly.Msg.LISTS_GET_INDEX_REMOVE, "REMOVE"], + ]; + this.WHERE_OPTIONS = [ + [Blockly.Msg.LISTS_GET_INDEX_FROM_START, "FROM_START"], + [Blockly.Msg.LISTS_GET_INDEX_FROM_END, "FROM_END"], + [Blockly.Msg.LISTS_GET_INDEX_FIRST, "FIRST"], + [Blockly.Msg.LISTS_GET_INDEX_LAST, "LAST"], + [Blockly.Msg.LISTS_GET_INDEX_RANDOM, "RANDOM"], + ]; + + this.setHelpUrl(Blockly.Msg.LISTS_GET_INDEX_HELPURL); + this.setStyle("list_blocks"); + + this.appendValueInput("VALUE") + .setCheck("Array") + .appendField(Blockly.Msg.LISTS_GET_INDEX_INPUT_IN_LIST); + + const modeField = new Blockly.FieldDropdown(MODE_OPTIONS, newMode => { + this.updateMode_(newMode); + return newMode; + }); + this.appendDummyInput() + .appendField(modeField, "MODE") + .appendField("", "SPACE"); + + const whereField = new Blockly.FieldDropdown( + this.WHERE_OPTIONS, + newWhere => { + const cur = this.getFieldValue("WHERE"); + const newNeedsAt = newWhere === "FROM_START" || newWhere === "FROM_END"; + const curNeedsAt = cur === "FROM_START" || cur === "FROM_END"; + if (newNeedsAt !== curNeedsAt) this.updateAt_(newNeedsAt); + } + ); + this.appendDummyInput().appendField(whereField, "WHERE"); + + this.appendDummyInput("AT"); + + this.setInputsInline(true); + + this.updateAt_(true); + this.updateMode_(this.getFieldValue("MODE") || "GET"); + + this.setTooltip(() => { + const mode = this.getFieldValue("MODE"); + const where = this.getFieldValue("WHERE"); + if (mode === "GET") { + switch (where) { + case "FROM_START": + case "FROM_END": + return Blockly.Msg.LISTS_GET_INDEX_TOOLTIP_GET_FROM; + case "FIRST": + return Blockly.Msg.LISTS_GET_INDEX_TOOLTIP_GET_FIRST; + case "LAST": + return Blockly.Msg.LISTS_GET_INDEX_TOOLTIP_GET_LAST; + case "RANDOM": + return Blockly.Msg.LISTS_GET_INDEX_TOOLTIP_GET_RANDOM; + } + } else { + switch (where) { + case "FROM_START": + case "FROM_END": + return Blockly.Msg.LISTS_GET_INDEX_TOOLTIP_REMOVE_FROM; + case "FIRST": + return Blockly.Msg.LISTS_GET_INDEX_TOOLTIP_REMOVE_FIRST; + case "LAST": + return Blockly.Msg.LISTS_GET_INDEX_TOOLTIP_REMOVE_LAST; + case "RANDOM": + return Blockly.Msg.LISTS_GET_INDEX_TOOLTIP_REMOVE_RANDOM; + } + } + return ""; + }); + }, + + mutationToDom: function () { + const container = document.createElement("mutation"); + const input = this.getInput("AT"); + const isValueInput = !!input && !!input.connection; + container.setAttribute("at", String(isValueInput)); + container.setAttribute("mode", this.getFieldValue("MODE") || "GET"); + return container; + }, + + domToMutation: function (xmlElement) { + const at = xmlElement.getAttribute("at") !== "false"; + const mode = xmlElement.getAttribute("mode") || "GET"; + this.updateAt_(at); + this.updateMode_(mode); + }, + + updateAt_: function (useValueInput) { + if (this.getInput("AT")) this.removeInput("AT", true); + if (this.getInput("ORDINAL")) this.removeInput("ORDINAL", true); + + if (useValueInput) { + this.appendValueInput("AT").setCheck("Number"); + if (Blockly.Msg.ORDINAL_NUMBER_SUFFIX) { + this.appendDummyInput("ORDINAL").appendField( + Blockly.Msg.ORDINAL_NUMBER_SUFFIX + ); + } + } else { + this.appendDummyInput("AT"); + } + }, + + updateMode_: function (mode) { + if (mode === "GET") { + if (!this.outputConnection) this.setOutput(true); + this.outputConnection.setCheck(null); + } else { + if (!this.outputConnection) this.setOutput(true); + this.outputConnection.setCheck("Array"); + } + }, +}; + +BlocklyJS.javascriptGenerator.forBlock["lists_getIndex_modified"] = function ( + block, + generator +) { + const workspaceOneBased = + block.workspace.options && block.workspace.options.oneBasedIndex; + const mode = block.getFieldValue("MODE"); + const where = block.getFieldValue("WHERE"); + + const listCode = + generator.valueToCode(block, "VALUE", BlocklyJS.Order.NONE) || "[]"; + const atCodeRaw = + generator.valueToCode(block, "AT", BlocklyJS.Order.NONE) || "0"; + + let i; + + switch (where) { + case "FIRST": + i = "0"; + break; + case "LAST": + i = "-1"; + break; + case "RANDOM": + i = `Math.floor(Math.random() * _.length)`; + break; + case "FROM_START": + i = workspaceOneBased ? `${atCodeRaw} - 1` : atCodeRaw; + break; + case "FROM_END": + i = workspaceOneBased ? `-${atCodeRaw}` : `-(${atCodeRaw} + 1)`; + break; + default: + i = "0"; + } + + return [ + mode === "REMOVE" + ? `${listCode}.toSpliced(${i}, 1)` + : `${listCode}.at(${i})`, + BlocklyJS.Order.FUNCTION_CALL, + ]; +}; + +Blockly.Blocks["lists_setIndex_modified"] = { + init: function () { + const MODE_OPTIONS = [ + [Blockly.Msg.LISTS_SET_INDEX_SET, "SET"], + [Blockly.Msg.LISTS_SET_INDEX_INSERT, "INSERT"], + ]; + this.WHERE_OPTIONS = [ + [Blockly.Msg.LISTS_GET_INDEX_FROM_START, "FROM_START"], + [Blockly.Msg.LISTS_GET_INDEX_FROM_END, "FROM_END"], + [Blockly.Msg.LISTS_GET_INDEX_FIRST, "FIRST"], + [Blockly.Msg.LISTS_GET_INDEX_LAST, "LAST"], + [Blockly.Msg.LISTS_GET_INDEX_RANDOM, "RANDOM"], + ]; + + this.setHelpUrl(Blockly.Msg.LISTS_SET_INDEX_HELPURL); + this.setStyle("list_blocks"); + + this.appendValueInput("LIST") + .setCheck("Array") + .appendField(Blockly.Msg.LISTS_SET_INDEX_INPUT_IN_LIST); + + const modeField = new Blockly.FieldDropdown(MODE_OPTIONS); + this.appendDummyInput() + .appendField(modeField, "MODE") + .appendField("", "SPACE"); + + const whereField = new Blockly.FieldDropdown( + this.WHERE_OPTIONS, + newWhere => { + this.updateAt_(newWhere === "FROM_START" || newWhere === "FROM_END"); + return newWhere; + } + ); + this.appendDummyInput().appendField(whereField, "WHERE"); + + this.appendDummyInput("AT"); + + this.appendValueInput("TO").appendField( + Blockly.Msg.LISTS_SET_INDEX_INPUT_TO + ); + + this.setInputsInline(true); + + this.setOutput(true, "Array"); + + this.updateAt_(true); + + this.setTooltip(() => { + const mode = this.getFieldValue("MODE"); + const where = this.getFieldValue("WHERE"); + if (mode === "SET") { + switch (where) { + case "FROM_START": + case "FROM_END": + return Blockly.Msg.LISTS_SET_INDEX_TOOLTIP_SET_FROM; + case "FIRST": + return Blockly.Msg.LISTS_SET_INDEX_TOOLTIP_SET_FIRST; + case "LAST": + return Blockly.Msg.LISTS_SET_INDEX_TOOLTIP_SET_LAST; + case "RANDOM": + return Blockly.Msg.LISTS_SET_INDEX_TOOLTIP_SET_RANDOM; + } + } else { + switch (where) { + case "FROM_START": + case "FROM_END": + return Blockly.Msg.LISTS_SET_INDEX_TOOLTIP_INSERT_FROM; + case "FIRST": + return Blockly.Msg.LISTS_SET_INDEX_TOOLTIP_INSERT_FIRST; + case "LAST": + return Blockly.Msg.LISTS_SET_INDEX_TOOLTIP_INSERT_LAST; + case "RANDOM": + return Blockly.Msg.LISTS_SET_INDEX_TOOLTIP_INSERT_RANDOM; + } + } + return Blockly.Msg.LISTS_SET_INDEX_TOOLTIP; + }); + }, + + mutationToDom: function () { + const container = document.createElement("mutation"); + const input = this.getInput("AT"); + const isValueInput = !!input && !!input.connection; + container.setAttribute("at", String(isValueInput)); + return container; + }, + + domToMutation: function (xmlElement) { + const at = xmlElement.getAttribute("at") !== "false"; + this.updateAt_(at); + }, + + updateAt_: function (useValueInput) { + if (this.getInput("AT")) this.removeInput("AT", true); + if (this.getInput("ORDINAL")) this.removeInput("ORDINAL", true); + + if (useValueInput) { + this.appendValueInput("AT").setCheck("Number"); + if (Blockly.Msg.ORDINAL_NUMBER_SUFFIX) { + this.appendDummyInput("ORDINAL").appendField( + Blockly.Msg.ORDINAL_NUMBER_SUFFIX + ); + } + } else { + this.appendDummyInput("AT"); + } + try { + this.moveInputBefore("AT", "TO"); + if (this.getInput("ORDINAL")) this.moveInputBefore("ORDINAL", "TO"); + } catch (e) {} + }, +}; + +BlocklyJS.javascriptGenerator.forBlock["lists_setIndex_modified"] = function ( + block, + generator +) { + const oneBased = block.workspace.options?.oneBasedIndex; + const mode = block.getFieldValue("MODE"); + const where = block.getFieldValue("WHERE"); + + const listCode = + generator.valueToCode(block, "LIST", BlocklyJS.Order.NONE) || "[]"; + const valueCode = + generator.valueToCode(block, "TO", BlocklyJS.Order.NONE) || "undefined"; + const atCode = + generator.valueToCode(block, "AT", BlocklyJS.Order.NONE) || "0"; + + let indexExpr; + switch (where) { + case "FIRST": + indexExpr = "0"; + break; + case "LAST": + indexExpr = "-1"; + break; + case "RANDOM": + indexExpr = "Math.floor(Math.random() * _.length)"; + break; + case "FROM_START": + indexExpr = oneBased ? `(${atCode} - 1)` : atCode; + break; + case "FROM_END": + indexExpr = oneBased ? `-${atCode}` : `-(${atCode} + 1)`; + break; + default: + indexExpr = "0"; + } + + const code = `((a) => { + const _ = [...a]; + let i = ${indexExpr}; + if (i < 0) i += _.length; + if (i < 0) i = 0; + if (i > _.length) i = _.length; + return ${ + mode === "INSERT" + ? `_.toSpliced(i, 0, ${valueCode})` + : `i < _.length ? _.toSpliced(i, 1, ${valueCode}) : _` + }; +})(${listCode})`; + + return [code, BlocklyJS.Order.FUNCTION_CALL]; +}; diff --git a/src/blocks/looks.js b/src/blocks/looks.js new file mode 100644 index 0000000..58183be --- /dev/null +++ b/src/blocks/looks.js @@ -0,0 +1,218 @@ +import * as Blockly from "blockly"; +import * as BlocklyJS from "blockly/javascript"; + +function getAvailableCostumes() { + if (window.projectCostumes && window.projectCostumes.length > 0) { + return window.projectCostumes.map(costume => [costume, costume]); + } + return [["default", "default"]]; +} + +Blockly.Blocks["say_message"] = { + init: function () { + this.appendValueInput("MESSAGE").appendField("say"); + this.setPreviousStatement(true, "default"); + this.setNextStatement(true, "default"); + this.setInputsInline(true); + this.setStyle("looks_blocks"); + }, +}; + +Blockly.Blocks["say_message_duration"] = { + init: function () { + this.appendValueInput("MESSAGE").appendField("say"); + this.appendValueInput("DURATION").setCheck("Number").appendField("for"); + this.appendDummyInput().appendField("seconds"); + this.setPreviousStatement(true, "default"); + this.setNextStatement(true, "default"); + this.setInputsInline(true); + this.setStyle("looks_blocks"); + }, +}; + +Blockly.Blocks["switch_costume"] = { + init: function () { + this.appendDummyInput() + .appendField("switch costume to") + .appendField(new Blockly.FieldDropdown( + function() { + return getAvailableCostumes(); + } + ), "COSTUME_NAME"); + this.setPreviousStatement(true); + this.setNextStatement(true); + this.setStyle("looks_blocks"); + }, +}; + +Blockly.Blocks["set_size"] = { + init: function () { + this.appendValueInput("AMOUNT") + .setCheck("Number") + .appendField("set size to"); + this.appendDummyInput().appendField("%"); + this.setPreviousStatement(true); + this.setNextStatement(true); + this.setStyle("looks_blocks"); + }, +}; + +Blockly.Blocks["change_size"] = { + init: function () { + this.appendValueInput("AMOUNT") + .setCheck("Number") + .appendField("change size by"); + this.appendDummyInput().appendField("%"); + this.setPreviousStatement(true); + this.setNextStatement(true); + this.setStyle("looks_blocks"); + }, +}; + +Blockly.Blocks["get_costume_size"] = { + init: function () { + this.appendDummyInput() + .appendField("costume") + .appendField( + new Blockly.FieldDropdown([ + ["width", "width"], + ["height", "height"], + ]), + "MENU" + ); + this.setOutput(true, "Number"); + this.setStyle("looks_blocks"); + }, +}; + +Blockly.Blocks["get_sprite_scale"] = { + init: function () { + this.appendDummyInput().appendField("size"); + this.setOutput(true, "Number"); + this.setStyle("looks_blocks"); + }, +}; + +Blockly.Blocks["looks_hide_sprite"] = { + init: function () { + this.appendDummyInput().appendField("hide sprite"); + this.setPreviousStatement(true, "default"); + this.setNextStatement(true, "default"); + this.setStyle("looks_blocks"); + }, +}; + +Blockly.Blocks["looks_show_sprite"] = { + init: function () { + this.appendDummyInput().appendField("show sprite"); + this.setPreviousStatement(true, "default"); + this.setNextStatement(true, "default"); + this.setStyle("looks_blocks"); + }, +}; + +Blockly.Blocks["looks_isVisible"] = { + init: function () { + this.appendDummyInput().appendField("is visible"); + this.setOutput(true, "Boolean"); + this.setStyle("looks_blocks"); + }, +}; + +BlocklyJS.javascriptGenerator.forBlock["say_message"] = function ( + block, + generator +) { + const message = + generator.valueToCode(block, "MESSAGE", BlocklyJS.Order.NONE) || ""; + + return `sayMessage(${message});\n`; +}; + +BlocklyJS.javascriptGenerator.forBlock["say_message_duration"] = function ( + block, + generator +) { + const message = + generator.valueToCode(block, "MESSAGE", BlocklyJS.Order.NONE) || ""; + const duration = + generator.valueToCode(block, "DURATION", BlocklyJS.Order.ATOMIC) || 2; + + return `sayMessage(${message}, ${duration});\n`; +}; + +BlocklyJS.javascriptGenerator.forBlock["switch_costume"] = function ( + block, + generator +) { + var costume = generator.valueToCode(block, "COSTUME", BlocklyJS.Order.ATOMIC); + return `switchCostume(${costume});\n`; +}; + +BlocklyJS.javascriptGenerator.forBlock["switch_costume"] = function (block, generator) { + var costume = "'" + block.getFieldValue("COSTUME_NAME") + "'"; + return `switchCostume(${costume});\n`; +}; + +BlocklyJS.javascriptGenerator.forBlock["set_size"] = function ( + block, + generator +) { + const amount = + generator.valueToCode(block, "AMOUNT", BlocklyJS.Order.ATOMIC) || 100; + return `setSize(${amount}, false);\n`; +}; + +BlocklyJS.javascriptGenerator.forBlock["change_size"] = function ( + block, + generator +) { + const amount = + generator.valueToCode(block, "AMOUNT", BlocklyJS.Order.ATOMIC) || 100; + return `setSize(${amount}, true);\n`; +}; + +BlocklyJS.javascriptGenerator.forBlock["get_costume_size"] = function (block) { + const menu = block.getFieldValue("MENU"); + return [`getCostumeSize("${menu}")`, BlocklyJS.Order.NONE]; +}; + +BlocklyJS.javascriptGenerator.forBlock["get_sprite_scale"] = function () { + return [`getSpriteScale()`, BlocklyJS.Order.NONE]; +}; + +BlocklyJS.javascriptGenerator.forBlock["looks_hide_sprite"] = function () { + return "toggleVisibility(false);\n"; +}; + +BlocklyJS.javascriptGenerator.forBlock["looks_show_sprite"] = function () { + return "toggleVisibility(true);\n"; +}; + +BlocklyJS.javascriptGenerator.forBlock["looks_isVisible"] = () => [ + "sprite.visible", + BlocklyJS.Order.NONE, +]; + +Blockly.Blocks["switch_backdrop"] = { + init: function () { + this.appendDummyInput() + .appendField("switch backdrop to") + .appendField(new Blockly.FieldDropdown( + function() { + if (window.projectBackdrops && window.projectBackdrops.length > 0) { + return window.projectBackdrops.map((backdrop) => [backdrop.name, backdrop.name]); + } + return [["no backdrops", ""]]; + } + ), "BACKDROP_NAME"); + this.setPreviousStatement(true); + this.setNextStatement(true); + this.setStyle("looks_blocks"); + }, +}; + +BlocklyJS.javascriptGenerator.forBlock["switch_backdrop"] = function (block) { + const name = block.getFieldValue("BACKDROP_NAME"); + return `setBackdropByName('${name}');\n`; +}; diff --git a/src/blocks/motion.js b/src/blocks/motion.js new file mode 100644 index 0000000..7ae9948 --- /dev/null +++ b/src/blocks/motion.js @@ -0,0 +1,223 @@ +import * as Blockly from "blockly"; +import * as BlocklyJS from "blockly/javascript"; + +Blockly.Blocks["move_steps"] = { + init: function () { + this.appendValueInput("STEPS").setCheck("Number").appendField("step"); + this.appendDummyInput().appendField("times"); + this.setPreviousStatement(true, "default"); + this.setNextStatement(true, "default"); + this.setStyle("motion_blocks"); + }, +}; + +Blockly.Blocks["change_position"] = { + init: function () { + this.appendValueInput("AMOUNT") + .setCheck("Number") + .appendField("change") + .appendField( + new Blockly.FieldDropdown([ + ["x", "x"], + ["y", "y"], + ]), + "MENU" + ) + .appendField("by"); + this.setPreviousStatement(true, "default"); + this.setNextStatement(true, "default"); + this.setStyle("motion_blocks"); + }, +}; + +Blockly.Blocks["set_position"] = { + init: function () { + this.appendValueInput("AMOUNT") + .setCheck("Number") + .appendField("set") + .appendField( + new Blockly.FieldDropdown([ + ["x", "x"], + ["y", "y"], + ]), + "MENU" + ) + .appendField("to"); + this.setPreviousStatement(true, "default"); + this.setNextStatement(true, "default"); + this.setStyle("motion_blocks"); + }, +}; + +Blockly.Blocks["goto_position"] = { + init: function () { + this.appendValueInput("x").setCheck("Number").appendField("go to x"); + this.appendValueInput("y").setCheck("Number").appendField("y"); + this.setInputsInline(true); + this.setPreviousStatement(true, "default"); + this.setNextStatement(true, "default"); + this.setStyle("motion_blocks"); + }, +}; + +Blockly.Blocks["get_position"] = { + init: function () { + this.appendDummyInput().appendField( + new Blockly.FieldDropdown([ + ["x", "x"], + ["y", "y"], + ]), + "MENU" + ); + this.setOutput(true, "Number"); + this.setStyle("motion_blocks"); + }, +}; + +Blockly.Blocks["angle_turn"] = { + init: function () { + this.appendValueInput("AMOUNT") + .setCheck("Number") + .appendField("turn") + .appendField( + new Blockly.FieldDropdown([ + [ + { + src: "icons/right.svg", + height: 30, + width: 30, + alt: "A circular arrow rotating to the right", + }, + "right", + ], + [ + { + src: "icons/left.svg", + height: 30, + width: 30, + alt: "A circular arrow rotating to the left", + }, + "left", + ], + ]), + "DIRECTION" + ); + this.appendDummyInput().appendField("degrees"); + this.setPreviousStatement(true, "default"); + this.setNextStatement(true, "default"); + this.setStyle("motion_blocks"); + }, +}; + +Blockly.Blocks["angle_set"] = { + init: function () { + this.appendValueInput("AMOUNT") + .setCheck("Number") + .appendField("set angle to"); + this.appendDummyInput().appendField("degrees"); + this.setInputsInline(true); + this.setPreviousStatement(true, "default"); + this.setNextStatement(true, "default"); + this.setStyle("motion_blocks"); + }, +}; + +Blockly.Blocks["point_towards"] = { + init: function () { + this.appendValueInput("x") + .setCheck("Number") + .appendField("point towards x"); + this.appendValueInput("y").setCheck("Number").appendField("y"); + this.setInputsInline(true); + this.setPreviousStatement(true, "default"); + this.setNextStatement(true, "default"); + this.setStyle("motion_blocks"); + }, +}; + +Blockly.Blocks["get_angle"] = { + init: function () { + this.appendDummyInput().appendField("angle"); + this.setOutput(true, "Number"); + this.setStyle("motion_blocks"); + }, +}; + +BlocklyJS.javascriptGenerator.forBlock["move_steps"] = function ( + block, + generator +) { + const steps = + generator.valueToCode(block, "STEPS", BlocklyJS.Order.ATOMIC) || 0; + return `moveSteps(${steps});\n`; +}; + +BlocklyJS.javascriptGenerator.forBlock["change_position"] = function ( + block, + generator +) { + const amount = + generator.valueToCode(block, "AMOUNT", BlocklyJS.Order.ATOMIC) || 0; + const menu = block.getFieldValue("MENU"); + if (menu === "y") return `sprite["${menu}"] -= ${amount};\n`; + else return `sprite["${menu}"] += ${amount};\n`; +}; + +BlocklyJS.javascriptGenerator.forBlock["set_position"] = function ( + block, + generator +) { + const amount = + generator.valueToCode(block, "AMOUNT", BlocklyJS.Order.ATOMIC) || 0; + const menu = block.getFieldValue("MENU"); + if (menu === "y") return `sprite["${menu}"] = -${amount};\n`; + else return `sprite["${menu}"] = ${amount};\n`; +}; + +BlocklyJS.javascriptGenerator.forBlock["goto_position"] = function ( + block, + generator +) { + const x = generator.valueToCode(block, "x", BlocklyJS.Order.ATOMIC) || 0; + const y = generator.valueToCode(block, "y", BlocklyJS.Order.ATOMIC) || 0; + return `sprite.x = ${x};\nsprite.y = -${y};\n`; +}; + +BlocklyJS.javascriptGenerator.forBlock["point_towards"] = function ( + block, + generator +) { + const x = generator.valueToCode(block, "x", BlocklyJS.Order.ATOMIC) || 0; + const y = generator.valueToCode(block, "y", BlocklyJS.Order.ATOMIC) || 0; + return `pointsTowards(${x}, ${y});\n`; +}; + +BlocklyJS.javascriptGenerator.forBlock["get_position"] = function (block) { + const menu = block.getFieldValue("MENU"); + return [`sprite["${menu}"]`, BlocklyJS.Order.NONE]; +}; + +BlocklyJS.javascriptGenerator.forBlock["angle_turn"] = function ( + block, + generator +) { + const direction = block.getFieldValue("DIRECTION"); + let amount = + generator.valueToCode(block, "AMOUNT", BlocklyJS.Order.ATOMIC) || 0; + if (direction === "left") amount = `-(${amount})`; + return `setAngle(${amount}, true);\n`; +}; + +BlocklyJS.javascriptGenerator.forBlock["angle_set"] = function ( + block, + generator +) { + const amount = + generator.valueToCode(block, "AMOUNT", BlocklyJS.Order.ATOMIC) || 0; + return `setAngle(${amount}, false);\n`; +}; + +BlocklyJS.javascriptGenerator.forBlock["get_angle"] = () => [ + "sprite.angle", + BlocklyJS.Order.NONE, +]; diff --git a/src/blocks/pen.js b/src/blocks/pen.js new file mode 100644 index 0000000..c185cb1 --- /dev/null +++ b/src/blocks/pen.js @@ -0,0 +1,115 @@ +import * as Blockly from "blockly"; +import * as BlocklyJS from "blockly/javascript"; + +Blockly.Blocks["pen_down"] = { + init: function () { + this.appendDummyInput().appendField("pen down"); + this.setPreviousStatement(true, "default"); + this.setNextStatement(true, "default"); + this.setColour("#0fbd8c"); + this.setTooltip("Put the pen down to draw"); + }, +}; +BlocklyJS.javascriptGenerator.forBlock["pen_down"] = function () { + return "setPenStatus(true);\n"; +}; + +Blockly.Blocks["pen_up"] = { + init: function () { + this.appendDummyInput().appendField("pen up"); + this.setPreviousStatement(true, "default"); + this.setNextStatement(true, "default"); + this.setColour("#0fbd8c"); + this.setTooltip("Lift the pen up"); + }, +}; +BlocklyJS.javascriptGenerator.forBlock["pen_up"] = function () { + return "setPenStatus(false);\n"; +}; + +Blockly.Blocks["set_pen_color"] = { + init: function () { + this.appendDummyInput().appendField("set pen color"); + this.appendValueInput("R").setCheck("Number").appendField("R"); + this.appendValueInput("G").setCheck("Number").appendField("G"); + this.appendValueInput("B").setCheck("Number").appendField("B"); + this.setInputsInline(true); + this.setPreviousStatement(true, "default"); + this.setNextStatement(true, "default"); + this.setColour("#0fbd8c"); + this.setTooltip("Set the pen color to a RGB value"); + }, +}; +BlocklyJS.javascriptGenerator.forBlock["set_pen_color"] = function ( + block, + generator +) { + const r = generator.valueToCode(block, "R", BlocklyJS.Order.ATOMIC) || 0; + const g = generator.valueToCode(block, "G", BlocklyJS.Order.ATOMIC) || 0; + const b = generator.valueToCode(block, "B", BlocklyJS.Order.ATOMIC) || 0; + return `setPenColor(${r}, ${g}, ${b});\n`; +}; + +Blockly.Blocks["set_pen_color_combined"] = { + init: function () { + this.appendDummyInput("MODE") + .appendField("set pen color to") + .appendField( + new Blockly.FieldDropdown([ + ["RGB", "RGB"], + ["HEX", "HEX"], + ]), + "MODE" + ); + this.appendValueInput("VALUE").setCheck(["String", "Number"]); + this.setInputsInline(true); + this.setPreviousStatement(true, "default"); + this.setNextStatement(true, "default"); + this.setColour("#0fbd8c"); + this.setTooltip("Set the pen color to a RGB or HEX value."); + }, +}; + +BlocklyJS.javascriptGenerator.forBlock["set_pen_color_combined"] = function ( + block, + generator +) { + const mode = block.getFieldValue("MODE"); + const value = generator.valueToCode(block, "VALUE", BlocklyJS.Order.ATOMIC); + if (mode === "HEX") return `setPenColorHex(${value});\n`; + else return `setPenColor(${value});\n`; +}; + +Blockly.Blocks["set_pen_size"] = { + init: function () { + this.appendValueInput("SIZE") + .setCheck("Number") + .appendField("set pen size to"); + this.appendDummyInput().appendField("px"); + this.setPreviousStatement(true, "default"); + this.setNextStatement(true, "default"); + this.setColour("#0fbd8c"); + this.setTooltip("Set the pen thickness to a specific value in pixels"); + }, +}; + +BlocklyJS.javascriptGenerator.forBlock["set_pen_size"] = function ( + block, + generator +) { + const size = + generator.valueToCode(block, "SIZE", BlocklyJS.Order.ATOMIC) || 1; + return `setPenSize("${size}");\n`; +}; + +Blockly.Blocks["clear_pen"] = { + init: function () { + this.appendDummyInput().appendField("clear pen"); + this.setPreviousStatement(true, "default"); + this.setNextStatement(true, "default"); + this.setColour("#0fbd8c"); + this.setTooltip("Clear all pen drawings"); + }, +}; + +BlocklyJS.javascriptGenerator.forBlock["clear_pen"] = () => "clearPen();\n"; diff --git a/src/blocks/set.js b/src/blocks/set.js new file mode 100644 index 0000000..dbcde43 --- /dev/null +++ b/src/blocks/set.js @@ -0,0 +1,353 @@ +import * as Blockly from "blockly"; +import * as BlocklyJS from "blockly/javascript"; + +Blockly.Blocks["sets_create_with"] = { + init: function () { + this.setStyle("set_blocks"); + this.setHelpUrl(""); + this.itemCount_ = 0; + this.updateShape_(); + this.setOutput(true, "Set"); + this.setMutator( + new Blockly.icons.MutatorIcon(["sets_create_with_item"], this) + ); + this.setTooltip("Create a set with any number of elements."); + }, + + mutationToDom: function () { + const container = Blockly.utils.xml.createElement("mutation"); + container.setAttribute("items", String(this.itemCount_)); + return container; + }, + + domToMutation: function (xmlElement) { + const items = xmlElement.getAttribute("items"); + this.itemCount_ = items ? parseInt(items, 10) : 0; + this.updateShape_(); + }, + + saveExtraState: function () { + return { itemCount: this.itemCount_ }; + }, + + loadExtraState: function (state) { + if (state && typeof state.itemCount === "number") { + this.itemCount_ = state.itemCount; + } else { + this.itemCount_ = 0; + } + this.updateShape_(); + }, + + decompose: function (workspace) { + const containerBlock = workspace.newBlock("sets_create_with_container"); + containerBlock.initSvg(); + let connection = containerBlock.getInput("STACK").connection; + for (let i = 0; i < this.itemCount_; i++) { + const itemBlock = workspace.newBlock("sets_create_with_item"); + itemBlock.initSvg(); + connection.connect(itemBlock.previousConnection); + connection = itemBlock.nextConnection; + } + return containerBlock; + }, + + compose: function (containerBlock) { + let itemBlock = containerBlock.getInputTargetBlock("STACK"); + const connections = []; + while (itemBlock) { + if (itemBlock.isInsertionMarker && itemBlock.isInsertionMarker()) { + itemBlock = itemBlock.getNextBlock(); + continue; + } + connections.push(itemBlock.valueConnection_ || null); + itemBlock = itemBlock.getNextBlock(); + } + + for (let i = 0; i < this.itemCount_; i++) { + const input = this.getInput("ADD" + i); + const targetConnection = + input && input.connection && input.connection.targetConnection; + if (targetConnection && !connections.includes(targetConnection)) { + targetConnection.disconnect(); + } + } + + this.itemCount_ = connections.length; + this.updateShape_(); + + for (let i = 0; i < this.itemCount_; i++) { + if (connections[i]) { + connections[i].reconnect(this, "ADD" + i); + } + } + }, + + saveConnections: function (containerBlock) { + let itemBlock = containerBlock.getInputTargetBlock("STACK"); + let i = 0; + while (itemBlock) { + if (itemBlock.isInsertionMarker && itemBlock.isInsertionMarker()) { + itemBlock = itemBlock.getNextBlock(); + continue; + } + const input = this.getInput("ADD" + i); + itemBlock.valueConnection_ = + input && input.connection && input.connection.targetConnection; + itemBlock = itemBlock.getNextBlock(); + i++; + } + }, + + updateShape_: function () { + if (this.itemCount_ === 0) { + if (!this.getInput("EMPTY")) { + this.appendDummyInput("EMPTY").appendField("create empty set"); + } + } else { + if (this.getInput("EMPTY")) { + this.removeInput("EMPTY"); + } + } + + for (let i = 0; i < this.itemCount_; i++) { + if (!this.getInput("ADD" + i)) { + const input = this.appendValueInput("ADD" + i).setAlign( + Blockly.inputs.Align.RIGHT + ); + if (i === 0) { + input.appendField("create set with"); + } + } + } + + let i = this.itemCount_; + while (this.getInput("ADD" + i)) { + this.removeInput("ADD" + i); + i++; + } + }, +}; + +Blockly.Blocks["sets_create_with_container"] = { + init: function () { + this.setStyle("set_blocks"); + this.appendDummyInput().appendField("set"); + this.appendStatementInput("STACK"); + this.contextMenu = false; + }, +}; + +Blockly.Blocks["sets_create_with_item"] = { + init: function () { + this.setStyle("set_blocks"); + this.appendDummyInput().appendField("element"); + this.setPreviousStatement(true); + this.setNextStatement(true); + this.contextMenu = false; + /** @type {Blockly.Connection?} */ + this.valueConnection_ = null; + }, +}; + +BlocklyJS.javascriptGenerator.forBlock["sets_create_with"] = function ( + block, + generator +) { + const elements = []; + for (let i = 0; i < block.itemCount_; i++) { + const code = + generator.valueToCode(block, "ADD" + i, BlocklyJS.Order.NONE) || "null"; + elements.push(code); + } + let code; + if (elements.length === 0) { + code = "new Set()"; + } else { + code = "new Set([" + elements.join(", ") + "])"; + } + return [code, BlocklyJS.Order.NONE]; +}; + +Blockly.Blocks["sets_convert"] = { + init: function () { + const dropdown = new Blockly.FieldDropdown([ + ["set from list", "SET"], + ["list from set", "LIST"], + ]); + dropdown.setValidator((newMode) => { + this.updateType_(newMode); + }); + + this.setStyle("set_blocks"); + this.appendValueInput("INPUT") + .setCheck("String") + .appendField("make") + .appendField(dropdown, "MODE"); + this.setInputsInline(true); + this.setOutput(true, "Set"); + this.setTooltip(() => { + const mode = this.getFieldValue("MODE"); + if (mode === "SET") { + return "Convert a list into a set (removes duplicates)."; + } else if (mode === "LIST") { + return "Convert a set into a list."; + } + throw Error("Unknown mode: " + mode); + }); + }, + updateType_: function (newMode) { + const mode = this.getFieldValue("MODE"); + if (mode !== newMode) { + const inputConnection = this.getInput("INPUT")?.connection; + inputConnection?.setShadowDom(null); + const inputBlock = inputConnection?.targetBlock(); + + if (inputBlock) { + inputConnection.disconnect(); + if (inputBlock.isShadow()) { + inputBlock.dispose(false); + } else { + this.bumpNeighbours(); + } + } + } + if (newMode === "SET") { + this.outputConnection.setCheck("Set"); + this.getInput("INPUT").setCheck("Array"); + } else { + this.outputConnection.setCheck("Array"); + this.getInput("INPUT").setCheck("Set"); + } + }, + mutationToDom: function () { + const container = Blockly.utils.xml.createElement("mutation"); + container.setAttribute("mode", this.getFieldValue("MODE")); + return container; + }, + domToMutation: function (xmlElement) { + this.updateType_(xmlElement.getAttribute("mode")); + }, + saveExtraState: function () { + return { mode: this.getFieldValue("MODE") }; + }, + loadExtraState: function (state) { + this.updateType_(state["mode"]); + }, +}; + +BlocklyJS.javascriptGenerator.forBlock["sets_convert"] = function (block) { + const input = + BlocklyJS.javascriptGenerator.valueToCode( + block, + "INPUT", + BlocklyJS.Order.ATOMIC + ) || "null"; + const mode = block.getFieldValue("MODE"); + + if (mode === "SET") { + return [`new Set(${input})`, BlocklyJS.Order.NONE]; + } else if (mode === "LIST") { + return [`[...${input}]`, BlocklyJS.Order.NONE]; + } +}; + +Blockly.Blocks["sets_add"] = { + init: function () { + this.appendValueInput("SET").setCheck("Set").appendField("in set"); + this.appendValueInput("VALUE").setCheck(null).appendField("add"); + this.setPreviousStatement(true, null); + this.setNextStatement(true, null); + this.setInputsInline(true); + this.setStyle("set_blocks"); + this.setTooltip("Adds a value to the set."); + }, +}; + +BlocklyJS.javascriptGenerator.forBlock["sets_add"] = function ( + block, + generator +) { + const set = generator.valueToCode(block, "SET", BlocklyJS.Order.ATOMIC); + const value = generator.valueToCode(block, "VALUE", BlocklyJS.Order.ATOMIC); + return `${set}.add(${value});\n`; +}; + +Blockly.Blocks["sets_delete"] = { + init: function () { + this.appendValueInput("SET").setCheck("Set").appendField("in set"); + this.appendValueInput("VALUE").setCheck(null).appendField("delete"); + this.setPreviousStatement(true, null); + this.setNextStatement(true, null); + this.setInputsInline(true); + this.setStyle("set_blocks"); + this.setTooltip("Deletes a value from the set."); + }, +}; + +BlocklyJS.javascriptGenerator.forBlock["sets_delete"] = function ( + block, + generator +) { + const set = generator.valueToCode(block, "SET", BlocklyJS.Order.ATOMIC); + const value = generator.valueToCode(block, "VALUE", BlocklyJS.Order.ATOMIC); + return `${set}.delete(${value});\n`; +}; + +Blockly.Blocks["sets_has"] = { + init: function () { + this.appendValueInput("SET").setCheck("Set").appendField("does set"); + this.appendValueInput("VALUE").setCheck(null).appendField("have"); + this.setOutput(true, "Boolean"); + this.setInputsInline(true); + this.setStyle("set_blocks"); + this.setTooltip("Returns true if the set contains the value."); + }, +}; + +BlocklyJS.javascriptGenerator.forBlock["sets_has"] = function ( + block, + generator +) { + const set = generator.valueToCode(block, "SET", BlocklyJS.Order.ATOMIC); + const value = generator.valueToCode(block, "VALUE", BlocklyJS.Order.ATOMIC); + return [`${set}.has(${value})`, BlocklyJS.Order.NONE]; +}; + +Blockly.Blocks["sets_size"] = { + init: function () { + this.appendValueInput("SET").setCheck("Set").appendField("size of set"); + this.setOutput(true, "Number"); + this.setInputsInline(true); + this.setStyle("set_blocks"); + this.setTooltip("Returns how many items are in the set."); + }, +}; + +BlocklyJS.javascriptGenerator.forBlock["sets_size"] = function ( + block, + generator +) { + const set = generator.valueToCode(block, "SET", BlocklyJS.Order.ATOMIC); + return [`${set}.size`, BlocklyJS.Order.NONE]; +}; + +Blockly.Blocks["sets_merge"] = { + init: function () { + this.appendValueInput("SET1").setCheck("Set").appendField("merge set"); + this.appendValueInput("SET2").setCheck("Set").appendField("with"); + this.setOutput(true, "Set"); + this.setInputsInline(true); + this.setStyle("set_blocks"); + this.setTooltip("Creates a new set combining all values from two sets"); + }, +}; + +BlocklyJS.javascriptGenerator.forBlock["sets_merge"] = function ( + block, + generator +) { + const set1 = generator.valueToCode(block, "SET1", BlocklyJS.Order.ATOMIC); + const set2 = generator.valueToCode(block, "SET2", BlocklyJS.Order.ATOMIC); + return [`new Set([...${set1}, ...${set2}])`, BlocklyJS.Order.NONE]; +}; diff --git a/src/blocks/sound.js b/src/blocks/sound.js new file mode 100644 index 0000000..bd449d4 --- /dev/null +++ b/src/blocks/sound.js @@ -0,0 +1,137 @@ +import * as Blockly from "blockly"; +import * as BlocklyJS from "blockly/javascript"; + +// Function to get available sounds - you'll need to implement this in your main editor +// This should return an array of sound names from your project +function getAvailableSounds() { + // This needs to be connected to your sound list + // For now, returning a default option + if (window.projectSounds && window.projectSounds.length > 0) { + return window.projectSounds.map(sound => [sound, sound]); + } + return [["no sounds", ""]]; +} + +Blockly.Blocks["play_sound"] = { + init: function () { + this.appendDummyInput() + .appendField("play sound") + .appendField(new Blockly.FieldDropdown( + function() { + return getAvailableSounds(); + } + ), "SOUND_NAME"); + this.appendDummyInput().appendField( + new Blockly.FieldDropdown([ + ["until finished", "true"], + ["without waiting", "false"], + ]), + "wait" + ); + this.setColour("#ff66ba"); + this.setPreviousStatement(true, "default"); + this.setNextStatement(true, "default"); + }, +}; + +BlocklyJS.javascriptGenerator.forBlock["play_sound"] = function (block, generator) { + var name = "'" + block.getFieldValue("SOUND_NAME") + "'"; + var wait = block.getFieldValue("wait"); + return `await playSound(${name}, ${wait});\n`; +}; + +Blockly.Blocks["stop_sound"] = { + init: function () { + this.appendDummyInput() + .appendField("stop sound") + .appendField(new Blockly.FieldDropdown( + function() { + return getAvailableSounds(); + } + ), "SOUND_NAME"); + this.setColour("#ff66ba"); + this.setPreviousStatement(true, "default"); + this.setNextStatement(true, "default"); + }, +}; + +BlocklyJS.javascriptGenerator.forBlock["stop_sound"] = function (block, generator) { + var name = "'" + block.getFieldValue("SOUND_NAME") + "'"; + return `stopSound(${name});\n`; +}; + +Blockly.Blocks["stop_all_sounds"] = { + init: function () { + this.appendDummyInput() + .appendField("stop") + .appendField( + new Blockly.FieldDropdown([ + ["all", "false"], + ["my", "true"], + ]), + "who" + ) + .appendField("sounds"); + this.setColour("#ff66ba"); + this.setPreviousStatement(true, "default"); + this.setNextStatement(true, "default"); + }, +}; + +BlocklyJS.javascriptGenerator.forBlock["stop_all_sounds"] = function (block) { + var who = block.getFieldValue("who"); + var code = `stopAllSounds(${who});\n`; + return code; +}; + +Blockly.Blocks["set_sound_property"] = { + init: function () { + this.appendValueInput("value") + .setCheck("Number") + .appendField("set") + .appendField( + new Blockly.FieldDropdown([ + ["volume", "volume"], + ["speed", "speed"], + ]), + "property" + ) + .appendField("to"); + this.appendDummyInput().appendField("%"); + this.setColour("#ff66ba"); + this.setPreviousStatement(true, "default"); + this.setNextStatement(true, "default"); + }, +}; + +BlocklyJS.javascriptGenerator.forBlock["set_sound_property"] = function ( + block, + generator +) { + var value = generator.valueToCode( + block, + "value", + BlocklyJS.Order.ATOMIC + ); + var property = block.getFieldValue("property"); + return `setSoundProperty("${property}", ${value});\n`; +}; + +Blockly.Blocks["get_sound_property"] = { + init: function () { + this.appendDummyInput().appendField( + new Blockly.FieldDropdown([ + ["volume", "volume"], + ["speed", "speed"], + ]), + "property" + ); + this.setColour("#ff66ba"); + this.setOutput(true, "Number"); + }, +}; + +BlocklyJS.javascriptGenerator.forBlock["get_sound_property"] = function (block) { + var property = block.getFieldValue("property"); + return [`soundProperties["${property}"]`, BlocklyJS.Order.NONE]; +}; diff --git a/src/blocks/system.js b/src/blocks/system.js new file mode 100644 index 0000000..675b142 --- /dev/null +++ b/src/blocks/system.js @@ -0,0 +1,137 @@ +import * as Blockly from "blockly"; +import * as BlocklyJS from "blockly/javascript"; + +const normalKeys = [ + ..."abcdefghijklmnopqrstuvwxyz", + ..."abcdefghijklmnopqrstuvwxyz0123456789".toUpperCase(), +]; + +Blockly.Blocks["key_pressed"] = { + init: function () { + this.appendDummyInput() + .appendField("is") + .appendField( + new Blockly.FieldDropdown([ + ["any", "any"], + ["space", " "], + ["enter", "Enter"], + ["escape", "Escape"], + ["up arrow", "ArrowUp"], + ["down arrow", "ArrowDown"], + ["left arrow", "ArrowLeft"], + ["right arrow", "ArrowRight"], + ...normalKeys.map((i) => [i, i]), + ]), + "KEY" + ) + .appendField("key down"); + this.setOutput(true, "Boolean"); + this.setColour("#5CB1D6"); + }, +}; + +Blockly.Blocks["get_mouse_position"] = { + init: function () { + this.appendDummyInput() + .appendField("mouse") + .appendField( + new Blockly.FieldDropdown([ + ["x", "x"], + ["y", "y"], + ]), + "MENU" + ); + this.setOutput(true, "Number"); + this.setColour("#5CB1D6"); + }, +}; + +Blockly.Blocks["mouse_button_pressed"] = { + init: function () { + this.appendDummyInput() + .appendField("is") + .appendField( + new Blockly.FieldDropdown([ + ["left", "0"], + ["middle", "1"], + ["right", "2"], + ["back", "3"], + ["forward", "4"], + ["any", "any"], + ]), + "BUTTON" + ) + .appendField("mouse button down"); + this.setOutput(true, "Boolean"); + this.setColour("#5CB1D6"); + }, +}; + +Blockly.Blocks["all_keys_pressed"] = { + init: function () { + this.appendDummyInput().appendField("keys currently down"); + this.setOutput(true, "Array"); + this.setColour("#5CB1D6"); + }, +}; + +Blockly.Blocks["mouse_over"] = { + init: function () { + this.appendDummyInput().appendField("is cursor over me"); + this.setOutput(true, "Boolean"); + this.setColour("#5CB1D6"); + }, +}; + +BlocklyJS.javascriptGenerator.forBlock["key_pressed"] = function (block, generator) { + const key = block.getFieldValue("KEY"); + const safeKey = generator.quote_(key); + return [`isKeyPressed(${safeKey})`, BlocklyJS.Order.NONE]; +}; + +BlocklyJS.javascriptGenerator.forBlock["get_mouse_position"] = function (block) { + const menu = block.getFieldValue("MENU"); + return [`getMousePosition("${menu}")`, BlocklyJS.Order.NONE]; +}; + +BlocklyJS.javascriptGenerator.forBlock["mouse_button_pressed"] = function ( + block, + generator +) { + const button = block.getFieldValue("BUTTON"); + const safeButton = generator.quote_(button); + return [`isMouseButtonPressed(${safeButton})`, BlocklyJS.Order.NONE]; +}; + +BlocklyJS.javascriptGenerator.forBlock["all_keys_pressed"] = () => [ + "Object.keys(keysPressed).filter(k => keysPressed[k])", + BlocklyJS.Order.NONE, +]; + +BlocklyJS.javascriptGenerator.forBlock["mouse_over"] = () => [ + "isMouseTouchingSprite()", + BlocklyJS.Order.NONE, +]; + +Blockly.Blocks["window_size"] = { + init: function () { + this.appendDummyInput() + .appendField("window") + .appendField( + new Blockly.FieldDropdown([ + ["width", "width"], + ["height", "height"], + ]), + "MENU" + ); + this.setOutput(true, "Number"); + this.setColour("#5CB1D6"); + }, +}; + +BlocklyJS.javascriptGenerator.forBlock["window_size"] = function (block) { + return [ + `window.inner${block.getFieldValue("MENU") === "width" ? "Width" : "Height"}`, + BlocklyJS.Order.NONE, + ]; +}; \ No newline at end of file diff --git a/src/blocks/tween.js b/src/blocks/tween.js new file mode 100644 index 0000000..5ac98ba --- /dev/null +++ b/src/blocks/tween.js @@ -0,0 +1,291 @@ +import * as Blockly from "blockly"; +import * as BlocklyJS from "blockly/javascript"; + +const TweenEasing = { + InLinear: (t) => t, + OutLinear: (t) => t, + InOutLinear: (t) => t, + InSine: (t) => 1 - Math.cos((t * Math.PI) / 2), + OutSine: (t) => Math.sin((t * Math.PI) / 2), + InOutSine: (t) => -(Math.cos(Math.PI * t) - 1) / 2, + InQuad: (t) => t * t, + OutQuad: (t) => 1 - (1 - t) * (1 - t), + InOutQuad: (t) => (t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2), + InCubic: (t) => t * t * t, + OutCubic: (t) => 1 - Math.pow(1 - t, 3), + InOutCubic: (t) => + t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2, + InQuart: (t) => t * t * t * t, + OutQuart: (t) => 1 - Math.pow(1 - t, 4), + InOutQuart: (t) => + t < 0.5 ? 8 * t * t * t * t : 1 - Math.pow(-2 * t + 2, 4) / 2, + InQuint: (t) => t * t * t * t * t, + OutQuint: (t) => 1 - Math.pow(1 - t, 5), + InOutQuint: (t) => + t < 0.5 ? 16 * t * t * t * t * t : 1 - Math.pow(-2 * t + 2, 5) / 2, + InExpo: (t) => (t === 0 ? 0 : Math.pow(2, 10 * t - 10)), + OutExpo: (t) => (t === 1 ? 1 : 1 - Math.pow(2, -10 * t)), + InOutExpo: (t) => { + if (t === 0) return 0; + if (t === 1) return 1; + return t < 0.5 + ? Math.pow(2, 20 * t - 10) / 2 + : (2 - Math.pow(2, -20 * t + 10)) / 2; + }, + InCirc: (t) => 1 - Math.sqrt(1 - Math.pow(t, 2)), + OutCirc: (t) => Math.sqrt(1 - Math.pow(t - 1, 2)), + InOutCirc: (t) => + t < 0.5 + ? (1 - Math.sqrt(1 - Math.pow(2 * t, 2))) / 2 + : (Math.sqrt(1 - Math.pow(-2 * t + 2, 2)) + 1) / 2, + InBack: (t) => { + const c1 = 1.70158, + c3 = c1 + 1; + return c3 * t * t * t - c1 * t * t; + }, + OutBack: (t) => { + const c1 = 1.70158, + c3 = c1 + 1; + return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2); + }, + InOutBack: (t) => { + const c1 = 1.70158, + c2 = c1 * 1.525; + return t < 0.5 + ? (Math.pow(2 * t, 2) * ((c2 + 1) * 2 * t - c2)) / 2 + : (Math.pow(2 * t - 2, 2) * ((c2 + 1) * (2 * t - 2) + c2) + 2) / 2; + }, + InElastic: (t) => { + const c4 = (2 * Math.PI) / 3; + if (t === 0) return 0; + if (t === 1) return 1; + return -Math.pow(2, 10 * t - 10) * Math.sin((t * 10 - 10.75) * c4); + }, + OutElastic: (t) => { + const c4 = (2 * Math.PI) / 3; + if (t === 0) return 0; + if (t === 1) return 1; + return Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c4) + 1; + }, + InOutElastic: (t) => { + const c5 = (2 * Math.PI) / 4.5; + if (t === 0) return 0; + if (t === 1) return 1; + return t < 0.5 + ? -(Math.pow(2, 20 * t - 10) * Math.sin((20 * t - 11.125) * c5)) / 2 + : (Math.pow(2, -20 * t + 10) * Math.sin((20 * t - 11.125) * c5)) / 2 + 1; + }, + InBounce: (t) => 1 - TweenEasing.OutBounce(1 - t), + OutBounce: (t) => { + const n1 = 7.5625, + d1 = 2.75; + if (t < 1 / d1) { + return n1 * t * t; + } else if (t < 2 / d1) { + return n1 * (t -= 1.5 / d1) * t + 0.75; + } else if (t < 2.5 / d1) { + return n1 * (t -= 2.25 / d1) * t + 0.9375; + } else { + return n1 * (t -= 2.625 / d1) * t + 0.984375; + } + }, + InOutBounce: (t) => + t < 0.5 + ? (1 - TweenEasing.OutBounce(1 - 2 * t)) / 2 + : (1 + TweenEasing.OutBounce(2 * t - 1)) / 2, +}; +Object.defineProperty(window, "TweenEasing", { + value: Object.freeze(TweenEasing), + configurable: false, + writable: false, + enumerable: true +}); + +Blockly.Blocks["tween_block"] = { + init: function () { + this.appendValueInput("FROM").setCheck("Number").appendField("tween from"); + this.appendValueInput("TO").setCheck("Number").appendField("to"); + this.appendDummyInput().appendField("in"); + this.appendValueInput("DURATION").setCheck("Number"); + this.appendDummyInput().appendField("seconds using"); + this.appendDummyInput() + .appendField( + new Blockly.FieldDropdown([ + ["linear", "Linear"], + ["sine", "Sine"], + ["quadratic", "Quad"], + ["cubic", "Cubic"], + ["quartic", "Quart"], + ["quintic", "Quint"], + ["expo", "Expo"], + ["circ", "Circ"], + ["back", "Back"], + ["elastic", "Elastic"], + ["bounce", "Bounce"], + ]), + "EASING_TYPE" + ) + .appendField( + new Blockly.FieldDropdown([ + ["in", "In"], + ["out", "Out"], + ["in-out", "InOut"], + ]), + "EASING_MODE" + ); + + this.appendStatementInput("DO").setCheck("default"); + this.appendDummyInput() + .setAlign(1) + .appendField( + new Blockly.FieldDropdown([ + ["wait", "WAIT"], + ["don't wait", "DONT_WAIT"], + ]), + "WAIT_MODE" + ) + .appendField("until finished"); + this.setInputsInline(true); + this.setPreviousStatement(true, "default"); + this.setNextStatement(true, "default"); + this.setColour("#32a2c0"); + this.setTooltip( + "Tween a value from one number to another over time using easing" + ); + }, +}; + +BlocklyJS.javascriptGenerator.forBlock["tween_block"] = function (block, generator) { + const easingType = block.getFieldValue("EASING_TYPE"); + const easingMode = block.getFieldValue("EASING_MODE"); + const from = + generator.valueToCode(block, "FROM", BlocklyJS.Order.ATOMIC) || + "0"; + const to = + generator.valueToCode(block, "TO", BlocklyJS.Order.ATOMIC) || "0"; + const duration = + generator.valueToCode(block, "DURATION", BlocklyJS.Order.ATOMIC) || + "1"; + const waitMode = block.getFieldValue("WAIT_MODE"); + + let branch = BlocklyJS.javascriptGenerator.statementToCode(block, "DO"); + branch = BlocklyJS.javascriptGenerator.addLoopTrap(branch, block); + + const code = `await startTween({ + from: ${from}, + to: ${to}, + duration: ${duration}, + easing: "${easingMode + easingType}", + wait: ${waitMode === "WAIT"}, + onUpdate: async (tweenValue) => { + ${branch} } +});\n`; + + return code; +}; + +Blockly.Blocks["tween_block_value"] = { + init: function () { + this.appendDummyInput("name").appendField("current tween value"); + this.setInputsInline(true); + this.setColour("#32a2c0"); + this.setOutput(true, "Number"); + }, +}; + +BlocklyJS.javascriptGenerator.forBlock["tween_block_value"] = () => [ + "tweenValue", + BlocklyJS.Order.NONE, +]; + +Blockly.Blocks["tween_sprite_property"] = { + init: function () { + this.appendValueInput("TO") + .setCheck("Number") + .appendField("tween") + .appendField( + new Blockly.FieldDropdown([ + ["x position", "x"], + ["y position", "y"], + ["angle", "angle"], + ["size", "size"], + ]), + "PROPERTY" + ) + .appendField("to"); + this.appendValueInput("DURATION").setCheck("Number").appendField("in"); + this.appendDummyInput() + .appendField("seconds using") + .appendField( + new Blockly.FieldDropdown([ + ["linear", "Linear"], + ["sine", "Sine"], + ["quadratic", "Quad"], + ["cubic", "Cubic"], + ["quartic", "Quart"], + ["quintic", "Quint"], + ["expo", "Expo"], + ["circ", "Circ"], + ["back", "Back"], + ["elastic", "Elastic"], + ["bounce", "Bounce"], + ]), + "EASING_TYPE" + ) + .appendField( + new Blockly.FieldDropdown([ + ["in", "In"], + ["out", "Out"], + ["in-out", "InOut"], + ]), + "EASING_MODE" + ); + this.setInputsInline(true); + this.setPreviousStatement(true, "default"); + this.setNextStatement(true, "default"); + this.setColour("#32a2c0"); + this.setTooltip( + "Tween a sprite property to a target value over time using easing" + ); + }, +}; + +BlocklyJS.javascriptGenerator.forBlock["tween_sprite_property"] = function ( + block, + generator +) { + const prop = block.getFieldValue("PROPERTY"); + const to = + generator.valueToCode(block, "TO", BlocklyJS.Order.ATOMIC) || "0"; + const duration = + generator.valueToCode(block, "DURATION", BlocklyJS.Order.ATOMIC) || + "1"; + const easingType = block.getFieldValue("EASING_TYPE"); + const easingMode = block.getFieldValue("EASING_MODE"); + + let fromGetter, setter; + if (prop === "size") { + fromGetter = "getSpriteScale()"; + setter = `setSize(tweenValue, false)`; + } else if (prop === "angle") { + fromGetter = `sprite.angle`; + setter = `setAngle(tweenValue, false)`; + } else { + fromGetter = `sprite["${prop}"]`; + setter = `sprite["${prop}"] = tweenValue`; + } + + setter = generator.addLoopTrap(setter, block); + + const code = `await startTween({ + from: ${fromGetter}, + to: ${to}, + duration: ${duration}, + easing: "${easingMode + easingType}", + onUpdate: async (tweenValue) => { + ${setter}; + } +});\n`; + + return code; +}; diff --git a/src/blocks/variable.js b/src/blocks/variable.js new file mode 100644 index 0000000..4d027a8 --- /dev/null +++ b/src/blocks/variable.js @@ -0,0 +1,85 @@ +import * as Blockly from "blockly"; +import * as BlocklyJS from "blockly/javascript"; +import { projectVariables } from "../scripts/editor"; + +function getVariables() { + if (Object.keys(projectVariables).length === 0) + return [["unknown", "unknown"]]; + else return Object.keys(projectVariables).map((name) => [name, name]); +} + +Blockly.Blocks["get_global_var"] = { + init: function () { + this.appendDummyInput().appendField( + new Blockly.FieldDropdown(() => getVariables()), + "VAR" + ); + this.setOutput(true); + this.setTooltip("Get a global variable"); + this.setStyle("variable_blocks"); + this.customContextMenu = function (options) { + const varName = this.getFieldValue("VAR"); + options.push({ + text: `Delete "${varName}" variable`, + enabled: true, + callback: () => { + delete projectVariables[varName]; + this.workspace.refreshToolboxSelection(); + }, + }); + }; + }, +}; + +Blockly.Blocks["set_global_var"] = { + init: function () { + this.appendValueInput("VALUE") + .setCheck(null) + .appendField("set") + .appendField(new Blockly.FieldDropdown(() => getVariables()), "VAR") + .appendField("to"); + this.setPreviousStatement(true, "default"); + this.setNextStatement(true, "default"); + this.setStyle("variable_blocks"); + }, +}; + +Blockly.Blocks["change_global_var"] = { + init: function () { + this.appendValueInput("VALUE") + .setCheck("Number") + .appendField("change") + .appendField(new Blockly.FieldDropdown(() => getVariables()), "VAR") + .appendField("by"); + this.setPreviousStatement(true, "default"); + this.setNextStatement(true, "default"); + this.setStyle("variable_blocks"); + }, +}; + +BlocklyJS.javascriptGenerator.forBlock["get_global_var"] = function (block) { + const name = block.getFieldValue("VAR"); + return [`projectVariables["${name}"]`, BlocklyJS.Order.ATOMIC]; +}; + +BlocklyJS.javascriptGenerator.forBlock["set_global_var"] = function (block) { + const name = block.getFieldValue("VAR"); + const value = + BlocklyJS.javascriptGenerator.valueToCode( + block, + "VALUE", + BlocklyJS.Order.ASSIGNMENT + ) || "0"; + return `projectVariables["${name}"] = ${value};\n`; +}; + +BlocklyJS.javascriptGenerator.forBlock["change_global_var"] = function (block) { + const name = block.getFieldValue("VAR"); + const value = + BlocklyJS.javascriptGenerator.valueToCode( + block, + "VALUE", + BlocklyJS.Order.ATOMIC + ) || "0"; + return `projectVariables["${name}"] += ${value};\n`; +}; diff --git a/src/cache.js b/src/cache.js new file mode 100644 index 0000000..c430134 --- /dev/null +++ b/src/cache.js @@ -0,0 +1,3 @@ +export const cache = { + user: null, +}; diff --git a/src/config.js b/src/config.js new file mode 100644 index 0000000..bb5eeb7 --- /dev/null +++ b/src/config.js @@ -0,0 +1,5 @@ +const localhost = window.location.hostname === "localhost"; + +export default { + apiUrl: localhost ? "http://localhost:3000" : "https://rarry-api-production.up.railway.app", +}; diff --git a/src/editor.css b/src/editor.css new file mode 100644 index 0000000..33f4b25 --- /dev/null +++ b/src/editor.css @@ -0,0 +1,415 @@ +#blocklyDiv { + height: 100%; + width: 100%; +} + +.main { + display: flex; + flex: 1; + overflow: hidden; +} + +html.stageLeft .main { + flex-direction: row-reverse; +} + +.left-panel { + width: 66.66%; + display: flex; + flex-direction: column; + border-right: 2px solid var(--color3); +} + +.tab-header { + display: flex; + border-bottom: 2px solid var(--color3); +} + +button.tab-button { + flex: 1; + padding: 0.5rem 0; + text-align: center; + font-weight: 600; + color: var(--primary); + background-color: var(--color2); + border-radius: 0; +} + +button.tab-button.inactive { + background-color: var(--color1); + color: var(--dark); +} + +.tab-content { + display: none; +} + +.tab-content.active { + display: flex; + flex: 1; + flex-direction: column; +} + +.tab-section { + padding: 1rem; + background-color: var(--color1); + overflow-y: auto; + flex: 1; +} + +.sound-container, +.costume-container { + padding: 8px; + gap: 0.5rem; + background-color: var(--color2); + border-radius: 0.375rem; + display: flex; + justify-content: flex-start; + align-items: center; + font-size: 1rem; + color: var(--dark); +} + +#stage-wrapper { + position: relative; + width: 100%; + padding-top: 56.25%; +} + +#stage-div.fullscreen { + display: flex; + flex-direction: column; + justify-content: center; + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + margin: 0; + padding: 0; + z-index: 9999; + background: var(--color1); +} + +#stage canvas { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +.right-panel { + width: 33.33%; + display: flex; + flex-direction: column; +} + +#stage-controls { + background-color: var(--color1); + padding: 0.5rem; + border-bottom: 2px solid var(--color3); + display: flex; + justify-content: center; + gap: 1rem; +} + +#stage-controls button { + padding: 0.5rem; + width: 2.5rem; + height: 2.5rem; + border-radius: 25%; + background-color: var(--color2); +} + +#stage-controls button:hover { + background-color: var(--color3); +} + +#stage-controls button.active { + outline: 2px solid #2acc65a6; + outline-offset: -2px; +} + +#stage-controls button img { + width: 100%; + height: 100%; + user-select: none; + pointer-events: none; +} + +html.dark #fullscreen-button img { + filter: invert(1); +} + +#sprite-info { + padding: 0.5rem; + display: flex; + flex-direction: row; + gap: 5px; +} + +#sprite-info p { + color: var(--dark); +} + +#sprite-info p:not(:last-child) { + padding-right: 5px; + border-right: 2px solid var(--color2); +} + +#sprites-section { + display: flex; + flex-direction: column; + gap: 0.5rem; + background-color: var(--color1); + padding: 0.5rem; + border-top: 2px solid var(--color3); + overflow-y: auto; +} + +#sprites-section div#sprite-buttons { + display: flex; + flex-direction: row; + gap: 0.5rem; +} + +#sprites-list { + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 1rem; + max-height: 9rem; + overflow-y: scroll; +} + +#sprites-list div { + width: 4rem; + height: 4rem; + background-color: var(--color2); + border-radius: 0.375rem; + display: flex; + justify-content: center; + align-items: center; + font-size: 0.875rem; + cursor: pointer; +} + +#sprites-list div.active { + background-color: var(--primary); + color: var(--color1); +} + +#sprites-section-buttons { + display: flex; + flex-direction: row; + gap: 0.5rem; +} + +#costumes-list, +#sounds-list { + display: flex; + flex-direction: column; + gap: 0.5rem; + margin-top: 0.5rem; + margin-bottom: 0.5rem; +} + +#extensionButton { + padding: 3px; + margin: 0 5px; + margin-top: auto; + background: var(--primary); + order: 9009; + cursor: pointer; +} + +.extensions-popup { + display: flex; + flex-direction: column; + gap: 5px; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 9999; + background-color: var(--color1); +} + +.extensions-popup .extensions-list { + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 1rem; + padding: 0.7rem; +} + +.extensions-list div { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0.5rem; + border-radius: 0.5rem; + background-color: var(--color2); +} + +.extensions-list div img { + border-radius: 0.5rem; +} + +.extensions-list div h2 { + text-align: center; +} + +.extensions-popup header { + padding: 0.7rem; +} + +.blocklyMainBackground { + stroke-width: 0; + stroke: none; +} + +.blocklyToolboxCategoryGroup { + height: 100%; +} + +html.dark .blocklyToolboxCategoryIcon { + filter: invert(1); +} + +.blocklyToolboxCategory { + height: initial; + padding: 3px 0; + transition: background-color 0.2s ease; + border-left: none !important; +} + +.blocklyToolboxCategory.blocklyToolboxSelected { + background-color: rgba(0, 0, 0, 0.3) !important; +} + +html.dark .blocklyToolboxCategory.blocklyToolboxSelected { + background-color: rgba(255, 255, 255, 0.3) !important; +} + +.draggingBlocklyToolboxCategory .blocklyToolboxCategory { + background: none !important; +} + +.blocklyToolboxCategoryLabel { + text-align: center; + transition: color 0.2s ease; +} + +.blocklyTreeSeparator { + border-bottom-color: var(--dark); +} + +.blocklyTreeRowContentContainer { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + justify-content: flex-start; + align-items: center; + padding: 3px; +} + +.blocklyWidgetDiv .blocklyMenu { + background-color: var(--color1); + border-color: var(--color2); +} + +.blocklyMenuItem { + color: var(--dark); +} + +.blocklyMenuItemDisabled { + color: var(--color4); +} + +.categoryBubble { + margin: 0; + padding: 0; + border-radius: 50%; + border: 1px solid; + width: 1.25rem; + height: 1.25rem; +} + +#room-users { + display: flex; + flex-direction: column; + gap: 0.3rem; +} + +#room-users div { + display: inline-flex; + align-items: center; + gap: 0.3rem; +} + +#room-users div img { + width: 2rem; + height: 2rem; + border-radius: 20%; +} + +.smallLabel { + margin-left: auto; + font-size: 0.8em; + color: var(--dark-light); +} +#backdrops-section { + padding: 12px; + border-top: 1px solid var(--border-color); +} + +#backdrops-section h3 { + margin: 0 0 8px 0; + font-size: 14px; + font-weight: 600; +} + +#backdrops-list { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 8px; + min-height: 60px; +} + +#backdrops-list > div { + width: 60px; + height: 45px; + border: 2px solid transparent; + border-radius: 4px; + cursor: pointer; + overflow: hidden; + transition: border-color 0.2s; +} + +#backdrops-list > div:hover { + border-color: var(--primary-color); +} + +#backdrops-list > div.active { + border-color: var(--primary-color); + box-shadow: 0 0 0 1px var(--primary-color); +} + +#backdrops-list > div img { + width: 100%; + height: 100%; + object-fit: cover; +} + +#backdrops-section-buttons { + display: flex; + gap: 8px; +} + +#backdrops-section-buttons button { + flex: 1; +} diff --git a/src/functions/extensionManager.js b/src/functions/extensionManager.js new file mode 100644 index 0000000..93f16a7 --- /dev/null +++ b/src/functions/extensionManager.js @@ -0,0 +1,248 @@ +import * as Blockly from "blockly"; +import * as BlocklyJS from "blockly/javascript"; +import { activeExtensions } from "../scripts/editor"; +import { Thread } from "./threads"; + +Thread.resetAll(); + +function textToBlock(block, text, fields) { + const regex = /\[([^\]]+)\]/g; + let lastIndex = 0; + let match; + + while ((match = regex.exec(text))) { + const before = text.slice(lastIndex, match.index); + if (before) { + block.appendDummyInput().appendField(before.trim()); + } + + const inputName = match[1].trim(); + const spec = fields?.[inputName]; + + if (spec?.kind === "statement") { + block + .appendStatementInput(inputName) + .setCheck(spec?.accepts || "default"); + } else if (spec?.kind === "value") { + block.appendValueInput(inputName).setCheck(spec?.type); + } else if (spec?.kind === "menu") { + const menuItems = spec.items.map((item) => + typeof item === "string" ? [item, item] : [item.text, item.value] + ); + const field = new Blockly.FieldDropdown(menuItems); + block.appendDummyInput().appendField(field, inputName); + } else { + block.appendDummyInput().appendField("[" + inputName + "]"); + } + + lastIndex = regex.lastIndex; + } + + const after = text.slice(lastIndex); + if (after) { + block.appendDummyInput().appendField(after.trim()); + } +} + +export function setupExtensions() { + if (!window.extensions) { + const backing = {}; + + const proxy = new Proxy(backing, { + defineProperty(target, prop, descriptor) { + if (prop in target) + throw new Error(`Extension "${prop}" already defined`); + return Reflect.defineProperty(target, prop, { + ...descriptor, + writable: false, + configurable: false, + }); + }, + set(target, prop, value) { + if (prop in target) + throw new Error(`Extension "${prop}" is already defined and locked`); + return Reflect.defineProperty(target, prop, { + value, + writable: false, + configurable: false, + enumerable: true, + }); + }, + deleteProperty() { + throw new Error("Extensions cannot be removed"); + }, + get(target, prop, receiver) { + return Reflect.get(target, prop, receiver); + }, + ownKeys(target) { + return Reflect.ownKeys(target); + }, + }); + + Object.defineProperty(window, "extensions", { + value: proxy, + writable: false, + configurable: false, + enumerable: true, + }); + } +} + +export async function registerExtension(extClass) { + const ext = new extClass(); + const id = ext.id || ext.constructor.name; + + if (activeExtensions.some((i) => (i?.id || i) === id)) { + console.warn(`Extension ${id} already registered`); + return; + } + + const coreDom = document.getElementById("toolbox"); + + const category = ext.registerCategory?.(); + let categoryEl = null; + if (category) { + categoryEl = document.createElement("category"); + categoryEl.setAttribute("name", category.name || "Extension"); + if (!category.color) category.color = "#888"; + categoryEl.setAttribute("colour", category.color); + if (category.iconURI) categoryEl.setAttribute("iconURI", category.iconURI); + } + + const blocks = ext.registerBlocks?.() || []; + const blockDefs = {}; + blocks.forEach((blockDef) => { + if (!blockDef.id) { + console.warn("Skipped registration of block with no ID"); + return; + } + + const blockType = `${id}_${blockDef.id}`; + blockDefs[blockType] = blockDef; + Blockly.Blocks[blockType] = { + init: function () { + textToBlock(this, blockDef.text, blockDef.fields); + + if (blockDef.type === "statement") { + this.setPreviousStatement(true, blockDef.statementType || "default"); + this.setNextStatement(true, blockDef.statementType || "default"); + } else if (blockDef.type === "cap") { + this.setPreviousStatement(true, blockDef.statementType || "default"); + } else if (blockDef.type === "output") { + this.setOutput(true, blockDef.outputType); + if (blockDef.outputShape) this.setOutputShape(blockDef.outputShape); + } + if (blockDef.tooltip) this.setTooltip(blockDef.tooltip); + this.setInputsInline(true); + this.setColour(blockDef?.color || category.color); + }, + }; + + if (categoryEl) { + const blockEl = document.createElement("block"); + blockEl.setAttribute("type", blockType); + + for (const [name, spec] of Object.entries(blockDef.fields || {})) { + if (spec?.kind === "menu") continue; + + if (spec.default !== undefined && spec?.kind !== "statement") { + const valueEl = document.createElement("value"); + valueEl.setAttribute("name", name.trim()); + + let shadowEl = null; + + if (spec.type === "Number") { + shadowEl = document.createElement("shadow"); + shadowEl.setAttribute("type", "math_number"); + + const fieldEl = document.createElement("field"); + fieldEl.setAttribute("name", "NUM"); + fieldEl.textContent = spec.default; + shadowEl.appendChild(fieldEl); + } else if (spec.type === "String") { + shadowEl = document.createElement("shadow"); + shadowEl.setAttribute("type", "text"); + + const fieldEl = document.createElement("field"); + fieldEl.setAttribute("name", "TEXT"); + fieldEl.textContent = spec.default; + shadowEl.appendChild(fieldEl); + } else if (spec.type === "Boolean") { + shadowEl = document.createElement("shadow"); + shadowEl.setAttribute("type", "logic_boolean"); + + const fieldEl = document.createElement("field"); + fieldEl.setAttribute("name", "BOOL"); + fieldEl.textContent = spec.default ? "TRUE" : "FALSE"; + shadowEl.appendChild(fieldEl); + } + + if (shadowEl) { + valueEl.appendChild(shadowEl); + } + + blockEl.appendChild(valueEl); + } + } + + categoryEl.appendChild(blockEl); + } + }); + + if (categoryEl) { + coreDom.appendChild(categoryEl); + Blockly.getMainWorkspace().updateToolbox(coreDom); + } + + const codeGen = ext.registerCode?.() || {}; + Object.entries(codeGen).forEach(([blockType, fn]) => { + const fullType = `${id}_${blockType}`; + + window.extensions[fullType] = fn; + const def = blockDefs[fullType] || {}; + BlocklyJS.javascriptGenerator.forBlock[fullType] = function (block) { + const inputs = {}; + + for (const input of block.inputList) { + const name = input.name; + let codeExpr; + + if (input.type === 1 || input.type === 2) { + codeExpr = + BlocklyJS.javascriptGenerator.valueToCode( + block, + name, + BlocklyJS.Order.ATOMIC + ) || undefined; + if (codeExpr !== undefined) inputs[name] = codeExpr; + } else if (input.type === 3) { + codeExpr = + BlocklyJS.javascriptGenerator.statementToCode(block, name) || + undefined; + if (codeExpr !== undefined) + inputs[name] = `async () => { ${codeExpr} }`; + } + } + + for (const [name, spec] of Object.entries(def.fields || {})) { + if (spec.kind === "menu") { + const fieldVal = block.getFieldValue(name); + if (fieldVal !== undefined) inputs[name] = JSON.stringify(fieldVal); + } + } + + const argsParts = Object.entries(inputs).map( + ([k, v]) => `${JSON.stringify(k)}:${v}` + ); + const args = `{${argsParts.join(",")}}`; + const callCode = `extensions["${fullType}"](${args},Thread)`; + + const finalCode = def.promise ? `await ${callCode}` : callCode; + + if (block.outputConnection) return [finalCode, BlocklyJS.Order.NONE]; + else return finalCode + ";\n"; + }; + }); + + activeExtensions.push({ id, code: extClass.toString() }); +} diff --git a/src/functions/patches.js b/src/functions/patches.js new file mode 100644 index 0000000..c4ea6e6 --- /dev/null +++ b/src/functions/patches.js @@ -0,0 +1,334 @@ +import * as Blockly from "blockly"; +import * as BlocklyJS from "blockly/javascript"; +import * as PIXI from "pixi.js-legacy"; + +BlocklyJS.javascriptGenerator.INFINITE_LOOP_TRAP = + 'if (stopped()) throw new Error("shouldStop");\nif (!fastExecution) await new Promise(r => setTimeout(r, 16));\n'; + +Blockly.VerticalFlyout.prototype.getFlyoutScale = () => 0.8; + +[ + "controls_if", + "controls_if_if", + "controls_if_elseif", + "controls_if_else", +].forEach((type) => { + Blockly.Blocks[type].init = (function (original) { + return function () { + original.call(this); + this.setColour("#FFAB19"); + }; + })(Blockly.Blocks[type].init); +}); + +Blockly.Blocks["controls_forEach"].init = (function (original) { + return function () { + original.call(this); + this.setColour("#e35340"); + }; +})(Blockly.Blocks["controls_forEach"].init); + +Blockly.Blocks["text"] = { + init: function () { + this.appendDummyInput().appendField(new Blockly.FieldTextInput(""), "TEXT"); + this.setOutput(true, "String"); + this.setStyle("text_blocks"); + this.setTooltip(Blockly.Msg["TEXT_TEXT_TOOLTIP"]); + this.setHelpUrl(Blockly.Msg["TEXT_TEXT_HELPURL"]); + + Blockly.Extensions.apply("parent_tooltip_when_inline", this, false); + setTimeout(() => { + if (!this.isShadow()) { + Blockly.Extensions.apply("text_quotes", this, false); + } + }); + }, +}; + +Object.keys(Blockly.Blocks).forEach((type) => { + const block = Blockly.Blocks[type]; + if (!block || typeof block.init !== "function") return; + + const originalInit = block.init; + block.init = function () { + originalInit.call(this); + if (this.previousConnection && this.previousConnection.check_ === null) + this.setPreviousStatement(true, "default"); + if (this.nextConnection && this.nextConnection.check_ === null) + this.setNextStatement(true, "default"); + }; +}); + +BlocklyJS.javascriptGenerator.forBlock["procedures_defnoreturn"] = function ( + block, + generator +) { + const procedureName = generator.getProcedureName(block.getFieldValue("NAME")); + + let injectedCode = ""; + if (generator.STATEMENT_PREFIX) { + injectedCode += generator.injectId(generator.STATEMENT_PREFIX, block); + } + if (generator.STATEMENT_SUFFIX) { + injectedCode += generator.injectId(generator.STATEMENT_SUFFIX, block); + } + if (injectedCode) { + injectedCode = generator.prefixLines(injectedCode, generator.INDENT); + } + + let loopTrap = ""; + if (generator.INFINITE_LOOP_TRAP) { + loopTrap = generator.prefixLines( + generator.injectId(generator.INFINITE_LOOP_TRAP, block), + generator.INDENT + ); + } + + let bodyCode = ""; + if (block.getInput("STACK")) { + bodyCode = generator.statementToCode(block, "STACK"); + } + + let returnCode = ""; + if (block.getInput("RETURN")) { + returnCode = + generator.valueToCode(block, "RETURN", BlocklyJS.Order.NONE) || ""; + } + + let returnWrapper = ""; + if (bodyCode && returnCode) { + returnWrapper = injectedCode; + } + + if (returnCode) { + returnCode = generator.INDENT + "return " + returnCode + ";\n"; + } + + const args = []; + const vars = block.getVars(); + for (let i = 0; i < vars.length; i++) { + args[i] = generator.getVariableName(vars[i]); + } + + let code = + "async function " + + procedureName + + "(" + + args.join(", ") + + ") {\n" + + injectedCode + + loopTrap + + bodyCode + + returnWrapper + + returnCode + + "}"; + + code = generator.scrub_(block, code); + generator.definitions_["%" + procedureName] = code; + + return null; +}; + +BlocklyJS.javascriptGenerator.forBlock["procedures_defreturn"] = function ( + block, + generator +) { + const procedureName = generator.getProcedureName(block.getFieldValue("NAME")); + + let statementWrapper = ""; + if (generator.STATEMENT_PREFIX) { + statementWrapper += generator.injectId(generator.STATEMENT_PREFIX, block); + } + if (generator.STATEMENT_SUFFIX) { + statementWrapper += generator.injectId(generator.STATEMENT_SUFFIX, block); + } + if (statementWrapper) { + statementWrapper = generator.prefixLines( + statementWrapper, + generator.INDENT + ); + } + + let loopTrapCode = ""; + if (generator.INFINITE_LOOP_TRAP) { + loopTrapCode = generator.prefixLines( + generator.injectId(generator.INFINITE_LOOP_TRAP, block), + generator.INDENT + ); + } + + let bodyCode = ""; + if (block.getInput("STACK")) { + bodyCode = generator.statementToCode(block, "STACK"); + } + + let returnCode = ""; + if (block.getInput("RETURN")) { + returnCode = + generator.valueToCode(block, "RETURN", BlocklyJS.Order.NONE) || ""; + } + + let returnWrapper = ""; + if (bodyCode && returnCode) { + returnWrapper = statementWrapper; + } + + if (returnCode) { + returnCode = generator.INDENT + "return " + returnCode + ";\n"; + } + + const args = []; + const vars = block.getVars(); + for (let i = 0; i < vars.length; i++) { + args[i] = generator.getVariableName(vars[i]); + } + + let code = + "async function " + + procedureName + + "(" + + args.join(", ") + + ") {\n" + + statementWrapper + + loopTrapCode + + bodyCode + + returnWrapper + + returnCode + + "}"; + + code = generator.scrub_(block, code); + generator.definitions_["%" + procedureName] = code; + + return null; +}; + +BlocklyJS.javascriptGenerator.forBlock["procedures_callreturn"] = function ( + block, + generator +) { + const procedureName = generator.getProcedureName(block.getFieldValue("NAME")); + + const args = []; + const vars = block.getVars(); + for (let i = 0; i < vars.length; i++) { + args[i] = + generator.valueToCode(block, "ARG" + i, BlocklyJS.Order.NONE) || "null"; + } + + return [ + "await " + procedureName + "(" + args.join(", ") + ")", + BlocklyJS.Order.FUNCTION_CALL, + ]; +}; + +BlocklyJS.javascriptGenerator.forBlock["procedures_callnoreturn"] = function ( + block, + generator +) { + const code = generator.forBlock.procedures_callreturn(block, generator)[0]; + return code + ";\n"; +}; + +export const SpriteChangeEvents = new PIXI.utils.EventEmitter(); + +const originalX = Object.getOwnPropertyDescriptor( + PIXI.DisplayObject.prototype, + "x" +); +const originalY = Object.getOwnPropertyDescriptor( + PIXI.DisplayObject.prototype, + "y" +); +const originalAngle = Object.getOwnPropertyDescriptor( + PIXI.DisplayObject.prototype, + "angle" +); +const originalTexture = Object.getOwnPropertyDescriptor( + PIXI.Sprite.prototype, + "texture" +); + +Object.defineProperty(PIXI.Sprite.prototype, "x", { + get() { + return originalX.get.call(this); + }, + set(value) { + if (this.x !== value) { + originalX.set.call(this, value); + SpriteChangeEvents.emit("positionChanged", this); + } + }, +}); + +Object.defineProperty(PIXI.Sprite.prototype, "y", { + get() { + return originalY.get.call(this); + }, + set(value) { + if (this.y !== value) { + originalY.set.call(this, value); + SpriteChangeEvents.emit("positionChanged", this); + } + }, +}); + +PIXI.Sprite.prototype.setPosition = function (x = null, y = null, add = false) { + const newX = x !== null ? (add ? this.x + x : x) : this.x; + const newY = y !== null ? (add ? this.y + y : y) : this.y; + if (this.x === newX && this.y === newY) return; + originalX.set.call(this, newX); + originalY.set.call(this, newY); + SpriteChangeEvents.emit("positionChanged", this); +}; + +Object.defineProperty(PIXI.Sprite.prototype, "angle", { + get() { + return originalAngle.get.call(this); + }, + set(value) { + if (this.angle !== value) { + originalAngle.set.call(this, value); + SpriteChangeEvents.emit("positionChanged", this); + } + }, +}); + +Object.defineProperty(PIXI.Sprite.prototype, "texture", { + get() { + return originalTexture.get.call(this); + }, + set(value) { + if (this.constructor === PIXI.Sprite && this.texture !== value) { + originalTexture.set.call(this, value); + SpriteChangeEvents.emit("textureChanged", this); + } else { + originalTexture.set.call(this, value); + } + }, +}); + +const originalObsPointSet = PIXI.ObservablePoint.prototype.set; + +PIXI.ObservablePoint.prototype.set = function (x, y) { + const result = originalObsPointSet.call(this, x, y); + if (this._parentScaleEvent) { + SpriteChangeEvents.emit("scaleChanged", this._parentScaleEvent); + } + return result; +}; + +class ToolboxBubbleCategory extends Blockly.ToolboxCategory { + createIconDom_() { + const element = document.createElement("div"); + element.classList.add("categoryBubble"); + element.style.backgroundColor = this.colour_; + return element; + } +} + +Blockly.registry.register( + Blockly.registry.Type.TOOLBOX_ITEM, + Blockly.ToolboxCategory.registrationName, + ToolboxBubbleCategory, + true +); diff --git a/src/functions/render.js b/src/functions/render.js new file mode 100644 index 0000000..b57b8d9 --- /dev/null +++ b/src/functions/render.js @@ -0,0 +1,217 @@ +import * as Blockly from "blockly"; + +class CustomConstantProvider extends Blockly.zelos.ConstantProvider { + init() { + super.init(); + this.BOWL = this.makeBowl(); + this.PILLOW = this.makePillow(); + this.SPIKEY = this.makeSpikey(); + } + + makeBowl() { + const maxW = this.MAX_DYNAMIC_CONNECTION_SHAPE_WIDTH; + const maxH = maxW * 2; + const roundedCopy = this.ROUNDED; + + function makeMainPath(blockHeight, up, right) { + const extra = blockHeight > maxH ? blockHeight - maxH : 0; + const h_ = Math.min(blockHeight, maxH); + const h = h_ + extra; + const radius = h / 2; + const radiusH = Math.min(h_ / 2, maxH); + const dirR = right ? 1 : -1; + const dirU = up ? -1 : 1; + + return ` + h ${radiusH * dirR} + q ${(h_ / 4) * -dirR} ${radius * dirU} 0 ${h * dirU} + h ${radiusH * -dirR} + `; + } + + return { + type: this.SHAPES.ROUND, + isDynamic: true, + width(h) { + const half = h / 2; + return half > maxW ? maxW : half; + }, + height(h) { + return h; + }, + connectionOffsetY(h) { + return h / 2; + }, + connectionOffsetX(w) { + return -w; + }, + pathDown(h) { + return makeMainPath(h, false, false); + }, + pathUp(h) { + return makeMainPath(h, true, false); + }, + pathRightDown(h) { + return roundedCopy.pathRightDown(h); + }, + pathRightUp(h) { + return roundedCopy.pathRightUp(h); + }, + }; + } + + makePillow() { + const maxWidth = this.MAX_DYNAMIC_CONNECTION_SHAPE_WIDTH; + const maxHeight = maxWidth * 2; + + function makeMainPath(blockHeight, up, right) { + const remainingHeight = + blockHeight > maxHeight ? blockHeight - maxHeight : 0; + const height = blockHeight > maxHeight ? maxHeight : blockHeight; + const radius = height / 8; + + const dirRight = right ? 1 : -1; + const dirUp = up ? -1 : 1; + + const radiusW = radius * dirRight; + const radiusH = radius * dirUp; + + return ` + h ${radiusW} + q ${radiusW} 0 ${radiusW} ${radiusH} + q 0 ${radiusH} ${radiusW} ${radiusH} + q ${radiusW} 0 ${radiusW} ${radiusH} + v ${(remainingHeight + height - radius * 6) * dirUp} + q 0 ${radiusH} ${-radiusW} ${radiusH} + q ${-radiusW} 0 ${-radiusW} ${radiusH} + q 0 ${radiusH} ${-radiusW} ${radiusH} + h ${-radiusW} + `; + } + + return { + type: this.SHAPES.HEXAGONAL, + isDynamic: true, + width(height) { + const halfHeight = height / 2; + return halfHeight > maxWidth ? maxWidth : halfHeight; + }, + height(height) { + return height; + }, + connectionOffsetY(connectionHeight) { + return connectionHeight / 2; + }, + connectionOffsetX(connectionWidth) { + return -connectionWidth; + }, + pathDown(height) { + return makeMainPath(height, false, false); + }, + pathUp(height) { + return makeMainPath(height, true, false); + }, + pathRightDown(height) { + return makeMainPath(height, false, true); + }, + pathRightUp(height) { + return makeMainPath(height, false, true); + }, + }; + } + + makeSpikey() { + const maxW = this.MAX_DYNAMIC_CONNECTION_SHAPE_WIDTH; + const maxH = maxW * 2; + + function makeMainPath(blockHeight, up, right) { + const extra = blockHeight > maxH ? blockHeight - maxH : 0; + const h_ = Math.min(blockHeight, maxH); + const h = h_ + extra; + const radius = h / 4; + const radiusH = Math.min(h_ / 4, maxH); + const dirR = right ? 1 : -1; + const dirU = up ? -1 : 1; + + return ` + h ${2 * radiusH * dirR} + l ${radiusH * -dirR} ${radius * dirU} + l ${radiusH * dirR} ${radius * dirU} + l ${radiusH * -dirR} ${radius * dirU} + l ${radiusH * dirR} ${radius * dirU} + h ${2 * radiusH * -dirR} + `; + } + + return { + type: this.SHAPES.HEXAGONAL, + isDynamic: true, + width(h) { + const half = h / 2; + return half > maxW ? maxW : half; + }, + height(h) { + return h; + }, + connectionOffsetY(h) { + return h / 2; + }, + connectionOffsetX(w) { + return -w; + }, + pathDown(h) { + return makeMainPath(h, false, false); + }, + pathUp(h) { + return makeMainPath(h, true, false); + }, + pathRightDown(h) { + return makeMainPath(h, false, true); + }, + pathRightUp(h) { + return makeMainPath(h, true, true); + }, + }; + } + + /** + * @param {Blockly.RenderedConnection} connection + */ + shapeFor(connection) { + let checks = connection.getCheck() ?? []; + if (!checks && connection.targetConnection) + checks = connection.targetConnection.getCheck() ?? []; + let outputShape = connection.sourceBlock_.getOutputShape(); + + if (connection.type === 1 || connection.type === 2) { + if ( + (checks.includes("Array") || outputShape === 4) && + !["text_length", "text_isEmpty"].includes(connection.sourceBlock_.type) + ) { + return this.BOWL; + } else if (checks.includes("Object") || outputShape === 5) { + return this.PILLOW; + } else if (checks.includes("Set") || outputShape === 6) { + return this.SPIKEY; + } /*else if ( + checks.includes("String") && + connection?.sourceBlock_?.isShadow() && + connection?.targetConnection?.shadowState?.type === "text" + ) { + return this.SQUARED; + }*/ + } + + return super.shapeFor(connection); + } +} + +export default class CustomRenderer extends Blockly.zelos.Renderer { + constructor() { + super(); + } + + makeConstants_() { + return new CustomConstantProvider(); + } +} diff --git a/src/functions/runCode.js b/src/functions/runCode.js new file mode 100644 index 0000000..d1af593 --- /dev/null +++ b/src/functions/runCode.js @@ -0,0 +1,441 @@ +import * as PIXI from "pixi.js-legacy"; +import { calculateBubblePosition, projectVariables } from "../scripts/editor"; +import { Thread } from "./threads"; +import { promiseWithAbort } from "./utils"; + +const BUBBLE_PADDING = 10; +const BUBBLE_TAIL_HEIGHT = 15; +const BUBBLE_TAIL_WIDTH = 15; +const BUBBLE_COLOR = 0xffffff; +const BUBBLE_TEXTSTYLE = new PIXI.TextStyle({ fill: 0x000000, fontSize: 24 }); +const LINE_COLOR = 0xbdc1c7; + +export function runCodeWithFunctions({ + code, + projectStartedTime, + spriteData, + app, + eventRegistry, + mouseButtonsPressed, + keysPressed, + playingSounds, + runningScripts, + signal, + penGraphics, + activeEventThreads, +}) { + Thread.resetAll(); + let fastExecution = false; + + const sprite = spriteData.pixiSprite; + const renderer = app.renderer; + const stage = app.stage; + const costumeMap = new Map( + (spriteData.costumes || []).map((c) => [c.name, c]) + ); + const soundMap = new Map((spriteData.sounds || []).map((s) => [s.name, s])); + const extensions = window.extensions; + const MyFunctions = {}; + + function stopped() { + return signal.aborted === true; + } + + function registerEvent(type, key, callback) { + if (stopped()) return; + + const entry = { + type, + cb: async () => { + if (stopped()) return; + + const threadId = Thread.create(); + Thread.enter(threadId); + activeEventThreads.count++; + + try { + const result = await promiseWithAbort( + () => callback(Thread.getCurrentContext()), + signal + ); + if (result === "shouldStop" || stopped()) return; + } catch (err) { + if (err.message !== "shouldStop") console.error(err); + } finally { + Thread.exit(); + activeEventThreads.count--; + } + }, + }; + + switch (type) { + case "flag": + eventRegistry.flag.push(entry); + break; + case "key": + if (!eventRegistry.key.has(key)) eventRegistry.key.set(key, []); + eventRegistry.key.get(key).push(entry); + break; + case "stageClick": + eventRegistry.stageClick.push(entry); + break; + case "timer": + eventRegistry.timer.push({ ...entry, value: key }); + break; + case "interval": + eventRegistry.interval.push({ ...entry, seconds: key }); + break; + case "custom": + if (!eventRegistry.custom.has(key)) eventRegistry.custom.set(key, []); + eventRegistry.custom.get(key).push(entry); + break; + } + } + + function triggerCustomEvent(eventName) { + const entries = eventRegistry.custom.get(eventName); + if (!entries) return; + for (const entry of entries) { + entry.cb(); + } + } + + function moveSteps(steps = 0) { + const { rotation: a } = sprite; + sprite.x += Math.cos(a) * steps; + sprite.y += Math.sin(a) * steps; + } + + function getMousePosition(menu) { + const mouse = renderer.events.pointer.global; + if (menu === "x") + return Math.round((mouse.x - renderer.width / 2) / stage.scale.x); + else if (menu === "y") + return -Math.round((mouse.y - renderer.height / 2) / stage.scale.y); + } + + function sayMessage(message, seconds) { + if (stopped()) return; + + message = String(message ?? ""); + if (!message) return; + + if (!spriteData.currentBubble) { + const bubble = new PIXI.Graphics(); + const text = new PIXI.Text("", BUBBLE_TEXTSTYLE); + text.x = BUBBLE_PADDING; + text.y = BUBBLE_PADDING; + + const container = new PIXI.Container(); + container.addChild(bubble); + container.addChild(text); + container.bubble = bubble; + container.text = text; + + spriteData.currentBubble = container; + stage.addChild(container); + } + + const container = spriteData.currentBubble; + const { bubble, text } = container; + + if (spriteData.sayTimeout !== null) { + clearTimeout(spriteData.sayTimeout); + spriteData.sayTimeout = null; + } + + if (text.text !== message) { + text.text = message; + + const bubbleWidth = text.width + BUBBLE_PADDING * 2; + const bubbleHeight = text.height + BUBBLE_PADDING * 2; + + bubble.clear(); + bubble.beginFill(BUBBLE_COLOR); + bubble.lineStyle(2, LINE_COLOR); + bubble.drawRoundedRect(0, 0, bubbleWidth, bubbleHeight, 10); + + bubble.moveTo(bubbleWidth / 2 - BUBBLE_TAIL_WIDTH / 2, bubbleHeight); + bubble.lineTo(bubbleWidth / 2, bubbleHeight + BUBBLE_TAIL_HEIGHT); + bubble.lineTo(bubbleWidth / 2 + BUBBLE_TAIL_WIDTH / 2, bubbleHeight); + bubble.closePath(); + bubble.endFill(); + } + + const pos = calculateBubblePosition( + sprite, + bubble.width, + bubble.height, + BUBBLE_TAIL_HEIGHT + ); + container.x = pos.x; + container.y = pos.y; + + container.visible = true; + + if (typeof seconds === "number" && seconds > 0) { + spriteData.sayTimeout = setTimeout(() => { + container.visible = false; + spriteData.sayTimeout = null; + }, Math.min(seconds * 1000, 2147483647)); + } + } + + function waitOneFrame() { + return new Promise((res, rej) => { + if (stopped()) return rej("stopped"); + + const id = requestAnimationFrame(() => { + if (stopped()) return rej("stopped"); + runningScripts.splice( + runningScripts.findIndex((t) => t.id === id), + 1 + ); + res(); + }); + runningScripts.push({ type: "raf", id }); + }); + } + + function wait(ms) { + return new Promise((res, rej) => { + if (stopped()) return rej("stopped"); + + const id = setTimeout(() => { + if (stopped()) return rej("stopped"); + runningScripts.splice( + runningScripts.findIndex((t) => t.id === id), + 1 + ); + res(); + }, ms); + runningScripts.push({ type: "timeout", id }); + }); + } + + function switchCostume(name) { + const found = costumeMap.get(name); + if (found) { + sprite.texture = found.texture; + } + } + + function setSize(amount = 0, additive) { + let amountN = amount / 100; + if (additive) + sprite.scale.set(sprite.scale.x + amountN, sprite.scale.y + amountN); + else sprite.scale.set(amountN, amountN); + } + + function setAngle(amount, additive) { + let angle = additive ? sprite.angle + amount : amount + angle = ((angle % 360) + 360) % 360; + sprite.angle = angle; + } + + function pointsTowards(x, y) { + const { width, height } = renderer + const targetX = width / 2 + x * stage.scale.x; + const targetY = height / 2 - y * stage.scale.y; + const spriteX = width / 2 + sprite.x * stage.scale.x; + const spriteY = height / 2 - sprite.y * stage.scale.y; + + let angle = Math.atan2(targetX - spriteX, targetY - spriteY) * (180 / Math.PI); + angle = ((angle % 360) + 360) % 360; + sprite.angle = angle; + } + + function projectTime() { + return (Date.now() - projectStartedTime) / 1000; + } + + function isKeyPressed(key) { + if (key === "any") { + return Object.values(keysPressed).some((pressed) => pressed); + } + + return !!keysPressed[key]; + } + + function isMouseButtonPressed(button) { + if (button === "any") { + return Object.values(mouseButtonsPressed).some((pressed) => pressed); + } + + return !!mouseButtonsPressed[button]; + } + + function getCostumeSize(type) { + const frame = sprite?.texture?.frame; + if (!frame) return 0; + + if (type === "width") return frame.width; + else if (type === "height") return frame.height; + else return 0; + } + + function getSpriteScale() { + const scaleX = sprite.scale.x; + const scaleY = sprite.scale.y; + return ((scaleX + scaleY) / 2) * 100; + } + + function startTween({ from, to, duration, easing, onUpdate, wait = true }) { + if (stopped()) return; + + const tweenPromise = new Promise((resolve) => { + const start = performance.now(); + const change = to - from; + const easeFn = window.TweenEasing[easing] || window.TweenEasing.linear; + + function tick(now) { + if (stopped()) return resolve("shouldStop"); + + const t = Math.min((now - start) / (duration * 1000), 1); + const value = from + change * easeFn(t); + + if (onUpdate) { + const result = onUpdate(value); + if (result === "shouldStop") return resolve("shouldStop"); + } + + if (t < 1) { + requestAnimationFrame(tick); + } else { + resolve(); + } + } + + const id = requestAnimationFrame(tick); + runningScripts.push({ type: "raf", id }); + }); + + return wait ? tweenPromise : undefined; + } + + let soundProperties = { + volume: 100, + speed: 100, + }; + + function setSoundProperty(property, value) { + if (!soundProperties[property]) return; + if (property === "speed") value = Math.min(1600, Math.max(7, value)); + if (property === "volume") value = Math.min(100, Math.max(0, value)); + soundProperties[property] = value; + } + + async function playSound(name, wait = false) { + const sound = soundMap.get(name); + if (!sound) return; + + if (!playingSounds.has(spriteData.id)) + playingSounds.set(spriteData.id, new Map()); + + const spriteSounds = playingSounds.get(spriteData.id); + + const oldAudio = spriteSounds.get(name); + if (oldAudio) { + oldAudio.pause(); + oldAudio.currentTime = 0; + } + + const audio = new Audio(sound.dataURL); + spriteSounds.set(name, audio); + + audio.volume = soundProperties.volume / 100; + audio.playbackRate = soundProperties.speed / 100; + audio.play(); + + const cleanup = () => { + if (spriteSounds.get(name) === audio) { + spriteSounds.delete(name); + } + }; + + audio.addEventListener("ended", cleanup); + audio.addEventListener("pause", cleanup); + + if (wait) { + return new Promise((res) => { + audio.addEventListener("ended", () => res()); + }); + } + } + + function stopSound(name) { + const spriteSounds = playingSounds.get(spriteData.id); + if (!spriteSounds || !spriteSounds.has(name)) return; + + const audio = spriteSounds.get(name); + audio.pause(); + audio.currentTime = 0; + spriteSounds.delete(name); + } + + function stopAllSounds(thisSprite = false) { + if (thisSprite) { + const spriteSounds = playingSounds.get(spriteData.id); + if (!spriteSounds) return; + + for (const audio of spriteSounds.values()) { + audio.pause(); + audio.currentTime = 0; + } + + playingSounds.delete(spriteData.id); + } else { + for (const spriteSounds of playingSounds.values()) { + for (const audio of spriteSounds.values()) { + audio.pause(); + audio.currentTime = 0; + } + } + playingSounds.clear(); + } + } + + function isMouseTouchingSprite() { + const mouse = renderer.events.pointer.global; + const bounds = sprite.getBounds(); + return bounds.contains(mouse.x, mouse.y); + } + + function setPenStatus(active) { + spriteData.penDown = !!active; + } + + function setPenColor(r, g, b) { + if (typeof r === "string") { + const [r_, g_, b_] = r.split(","); + r = +r_; + g = +g_; + b = +b_; + } + spriteData.penColor = (r << 16) | (g << 8) | b; + } + + function setPenColorHex(value) { + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(value); + spriteData.penColor = result + ? (parseInt(result[1], 16) << 16) | + (parseInt(result[2], 16) << 8) | + parseInt(result[3], 16) + : 0x000000; + } + + function setPenSize(size = 0) { + spriteData.penSize = Math.max(1, size); + } + + function clearPen() { + penGraphics.clear(); + } + + function toggleVisibility(bool = true) { + sprite.visible = bool; + if (spriteData.currentBubble) spriteData.currentBubble.visible = bool; + } + + eval(code); +} diff --git a/src/functions/theme.js b/src/functions/theme.js new file mode 100644 index 0000000..22f96d2 --- /dev/null +++ b/src/functions/theme.js @@ -0,0 +1,282 @@ +import "@fortawesome/fontawesome-free/css/all.min.css"; +import * as Blockly from "blockly"; +import config from "../config"; +import { cache } from "../cache"; +import { showPopup } from "./utils"; + +const root = document.documentElement; +const theme = localStorage.getItem("theme") === "dark" ?? false; +const icons = localStorage.getItem("removeIcons") === "true" ?? false; +const rarryToolbar = + localStorage.getItem("removeRarryToolbar") === "true" ?? false; +const toolboxPosition = + localStorage.getItem("toolboxPosition") || "space-between"; +const headerColor = localStorage.getItem("headerColor") || ""; +const stageLeft = localStorage.getItem("stageLeft") === "true" ?? false; + +const blockStyles = { + logic_blocks: { + colourPrimary: "#59BA57", + }, + math_blocks: { + colourPrimary: "#59BA57", + }, + text_blocks: { + colourPrimary: "#59BA57", + }, + loop_blocks: { + colourPrimary: "#FFAB19", + }, + variable_blocks: { + colourPrimary: "#FF8C1A", + }, + list_blocks: { + colourPrimary: "#E35340", + }, + procedure_blocks: { + colourPrimary: "#FF6680", + }, + motion_blocks: { + colourPrimary: "#4C97FF", + }, + looks_blocks: { + colourPrimary: "#9966FF", + }, + events_blocks: { + colourPrimary: "#e9c600", + }, + control_blocks: { + colourPrimary: "#FFAB19", + }, + json_category: { + colourPrimary: "#FF8349", + }, + set_blocks: { + colourPrimary: "#2CC2A9", + }, +}; + +const lightTheme = Blockly.Theme.defineTheme("customLightTheme", { + base: Blockly.Themes.Classic, + blockStyles: blockStyles, +}); + +const darkTheme = Blockly.Theme.defineTheme("customDarkTheme", { + base: Blockly.Themes.Classic, + blockStyles: blockStyles, + componentStyles: { + workspaceBackgroundColour: "#1a1e25", + toolboxBackgroundColour: "#303236", + toolboxForegroundColour: "#fff", + flyoutBackgroundColour: "#212327", + flyoutForegroundColour: "#ccc", + flyoutOpacity: 1, + scrollbarColour: "#797979", + insertionMarkerColour: "#fff", + insertionMarkerOpacity: 0.3, + scrollbarOpacity: 0.4, + cursorColour: "#d0d0d0", + }, +}); + +export function toggleTheme(dark, workspace) { + localStorage.setItem("theme", dark ? "dark" : "light"); + + if (dark) root.classList.add("dark"); + else root.classList.remove("dark"); + + if (workspace) workspace.setTheme(dark ? darkTheme : lightTheme); +} + +export function toggleIcons(removeIcons) { + localStorage.setItem("removeIcons", String(removeIcons)); + + if (removeIcons) root.classList.add("removeIcons"); + else root.classList.remove("removeIcons"); +} + +export function toggleRarryToolbar(removeIcon) { + localStorage.setItem("removeRarryToolbar", String(removeIcon)); + + if (removeIcon) root.classList.add("removeRarryToolbar"); + else root.classList.remove("removeRarryToolbar"); +} + +export function setToolboxPosition(pos) { + localStorage.setItem("toolboxPosition", pos); + + const header = document.querySelector("header"); + if (!header) return; + + root.classList.remove("toolbox-left", "toolbox-center", "toolbox-right"); + + if (pos === "default") return; + + root.classList.add(`toolbox-${pos}`); +} + +export function setHeaderColor(color) { + localStorage.setItem("headerColor", color); + + if (!color) root.style.removeProperty("--header-color"); + else root.style.setProperty("--header-color", color); +} + +export function toggleStageLeft(left) { + localStorage.setItem("stageLeft", String(left)); + + if (left) root.classList.add("stageLeft"); + else root.classList.remove("stageLeft"); +} + +export function setupThemeButton(workspace) { + toggleTheme(theme, workspace); + toggleIcons(icons); + toggleRarryToolbar(rarryToolbar); + toggleStageLeft(stageLeft); + setToolboxPosition(toolboxPosition); + setHeaderColor(headerColor); + + const themeButton = document.getElementById("theme-button"); + if (themeButton) + themeButton.addEventListener("click", () => + showPopup({ + title: "Appearance", + rows: [ + [ + "Theme:", + { + type: "button", + label: ' Light', + onClick: () => toggleTheme(false, workspace), + }, + { + type: "button", + label: ' Dark', + onClick: () => toggleTheme(true, workspace), + }, + ], + [ + "Show icon on buttons:", + { + type: "checkbox", + checked: + !document.documentElement.classList.contains("removeIcons"), + onChange: checked => { + toggleIcons(!checked); + }, + }, + ], + [ + "Show Rarry logo on toolbar:", + { + type: "checkbox", + checked: + !document.documentElement.classList.contains( + "removeRarryToolbar" + ), + onChange: checked => { + toggleRarryToolbar(!checked); + }, + }, + ], + [ + "Toolbar color:", + { + type: "color", + value: localStorage.getItem("headerColor") || "", + onChange: value => setHeaderColor(value), + }, + { + type: "button", + label: "Reset", + onClick: () => setHeaderColor(""), + }, + ], + [ + "Toolbar position:", + { + type: "menu", + value: localStorage.getItem("toolboxPosition") || "default", + options: [ + { label: "Space Between (default)", value: "default" }, + { label: "Left", value: "left" }, + { label: "Center", value: "center" }, + { label: "Right", value: "right" }, + ], + onChange: value => setToolboxPosition(value), + }, + ], + ...(workspace + ? [ + [ + "Renderer (applies after refresh):", + { + type: "menu", + value: localStorage.getItem("renderer"), + options: [ + { label: "Zelos (default)", value: "custom_zelos" }, + { label: "Thrasos", value: "thrasos" }, + { label: "Geras", value: "geras" }, + ], + onChange: value => localStorage.setItem("renderer", value), + }, + ], + [ + "Stage on left:", + { + type: "checkbox", + checked: + document.documentElement.classList.contains("stageLeft"), + onChange: checked => { + toggleStageLeft(checked); + }, + }, + ], + ] + : []), + ], + }) + ); +} + +export function setupUserTag() { + function setUserTag(user) { + if (user === null) { + if (cache.user === null) return; + user = cache.user; + } + + login.parentElement.innerHTML = ` +
+ + ${user.username} +
+ `; + } + + const login = document.getElementById("login-button"); + if (login && localStorage.getItem("tooken") !== null) { + if (cache.user) { + setUserTag(cache.user); + } else { + fetch(`${config.apiUrl}/users/me`, { + headers: { + Authorization: localStorage.getItem("tooken"), + }, + }) + .then(response => { + if (!response.ok) + throw new Error( + "Failed to fetch user data: " + response.statusText + ); + return response.json(); + }) + .then(data => { + cache.user = data; + setUserTag(data); + }) + .catch(console.error); + } + } +} diff --git a/src/functions/threads.js b/src/functions/threads.js new file mode 100644 index 0000000..6551e5c --- /dev/null +++ b/src/functions/threads.js @@ -0,0 +1,84 @@ +export function createThreadSystem() { + const threads = new Map(); + let current = null; + let nextId = 1; + + function ensure(threadId) { + if (!threads.has(threadId)) threads.set(threadId, {}); + } + + return { + resetAll() { + threads.clear(); + current = null; + nextId = 1; + }, + + create(initialVars = {}) { + const id = `t${nextId++}`; + threads.set(id, { ...initialVars }); + return id; + }, + + enter(threadId) { + ensure(threadId); + current = threadId; + return this.getCurrentContext(); + }, + + exit() { + current = null; + }, + + set(threadId, key, value) { + ensure(threadId); + threads.get(threadId)[key] = value; + }, + get(threadId, key) { + return threads.get(threadId) ? threads.get(threadId)[key] : undefined; + }, + has(threadId, key) { + return threads.get(threadId) + ? threads.get(threadId)[key] !== undefined + : false; + }, + + getCurrentContext() { + const threadId = current; + return { + id: threadId, + vars: threadId ? threads.get(threadId) : null, + set: (k, v) => { + if (!threadId) return; + threads.get(threadId)[k] = v; + }, + get: (k) => { + if (!threadId) return undefined; + return threads.get(threadId)[k]; + }, + has: (k) => { + if (!threadId) return false; + return threads.get(threadId)[k] !== undefined; + }, + spawn: (fn, initialVars = {}) => { + const newId = `t${nextId++}`; + threads.set(newId, { ...initialVars }); + + const prev = current; + current = newId; + try { + return fn({ + id: newId, + get: (k) => threads.get(newId)[k], + set: (k, v) => (threads.get(newId)[k] = v), + }); + } finally { + current = prev; + } + }, + }; + }, + }; +}; + +export const Thread = createThreadSystem(); \ No newline at end of file diff --git a/src/functions/utils.js b/src/functions/utils.js new file mode 100644 index 0000000..ca745e3 --- /dev/null +++ b/src/functions/utils.js @@ -0,0 +1,260 @@ +let currentPopup; + +export function showNotification({ + message = "", + duration = 5000, + closable = true, +}) { + const notification = document.createElement("div"); + notification.className = "notification"; + notification.innerHTML = ` + ${message} + ${ + closable + ? '' + : "" + } + `; + + let container = document.querySelector(".notification-container"); + if (!container) { + container = document.createElement("div"); + container.className = "notification-container"; + document.body.appendChild(container); + } + + container.appendChild(notification); + + function hide() { + notification.classList.add("hide"); + setTimeout(() => notification.remove(), 300); + } + + if (closable) { + notification + .querySelector(".notification-close") + ?.addEventListener("click", hide); + } + + setTimeout(hide, duration); + + return notification; +} + +export function showPopup({ innerHTML = "", title = "", rows = [] }) { + const popup = document.createElement("div"); + popup.className = "popup"; + + if (currentPopup) currentPopup.remove(); + currentPopup = popup; + + const rowsHTML = rows + .map((row, rowIndex) => { + const rowHTML = row + .map((item, colIndex) => { + if (typeof item === "string") { + if (item === "") return; + return `${item}`; + } + + switch (item.type) { + case "custom": + return item.html || ""; + case "button": + return ``; + case "input": + return ``; + case "checkbox": + return ``; + case "textarea": + return ``; + case "label": + return `${item.text}`; + case "menu": + return ``; + case "color": + return ``; + default: + return ""; + } + }) + .join(""); + return ``; + }) + .join(""); + + popup.innerHTML = ` + `; + + document.body.appendChild(popup); + + popup.querySelector(".popup-close").addEventListener("click", () => { + currentPopup = null; + popup.remove(); + }); + + rows.forEach((row, rowIndex) => { + row.forEach((item, colIndex) => { + const el = popup.querySelector( + `[data-row="${rowIndex}"][data-col="${colIndex}"]` + ); + if (!el) return; + + if (item.type === "button" && item.onClick) { + el.addEventListener("click", () => item.onClick(popup)); + } + if (item.type === "input" && item.onInput) { + el.addEventListener("input", e => item.onInput(e.target.value, popup)); + } + if (item.type === "checkbox" && item.onChange) { + el.addEventListener("change", e => + item.onChange(e.target.checked, popup) + ); + } + if (item.type === "textarea" && item.onInput) { + el.addEventListener("input", e => item.onInput(e.target.value, popup)); + } + if (item.type === "menu" && item.onChange) { + el.addEventListener("change", e => + item.onChange(e.target.value, popup) + ); + } + if (item.type === "color" && item.onChange) { + el.addEventListener("input", e => item.onChange(e.target.value, popup)); + } + }); + }); + + return popup; +} + +export function promiseWithAbort(promiseOrFn, signal) { + try { + const p = typeof promiseOrFn === "function" ? promiseOrFn() : promiseOrFn; + if (!(p instanceof Promise)) return Promise.resolve(p); + + if (signal.aborted) return Promise.reject(new Error("shouldStop")); + + return Promise.race([ + p, + new Promise((_, rej) => { + signal.addEventListener("abort", () => rej(new Error("shouldStop")), { + once: true, + }); + }), + ]); + } catch (err) { + return Promise.reject(err); + } +} + +async function encodeOggWithMediaRecorder(dataURL) { + const audioCtx = new (window.AudioContext || window.webkitAudioContext)(); + + const base64 = dataURL.split(",")[1]; + const raw = Uint8Array.from(atob(base64), c => c.charCodeAt(0)); + const buffer = await audioCtx.decodeAudioData(raw.buffer); + + const src = audioCtx.createBufferSource(); + src.buffer = buffer; + + const dest = audioCtx.createMediaStreamDestination(); + src.connect(dest); + + const recorder = new MediaRecorder(dest.stream, { + mimeType: "audio/ogg", + }); + + const chunks = []; + recorder.ondataavailable = e => chunks.push(e.data); + + return new Promise(resolve => { + recorder.onstop = () => { + const blob = new Blob(chunks, { type: "audio/ogg" }); + const fr = new FileReader(); + fr.onloadend = () => resolve(fr.result); + fr.readAsDataURL(blob); + }; + + recorder.start(); + src.start(); + src.onended = () => recorder.stop(); + }); +} + +export async function compressAudio(dataURL) { + if (window.MediaRecorder && MediaRecorder.isTypeSupported("audio/ogg")) { + try { + return await encodeOggWithMediaRecorder(dataURL); + } catch (e) { + console.warn("OGG recording failed, falling back:", e); + } + } + + return dataURL; +} + +export async function compressImage(dataURL) { + if (!dataURL || typeof dataURL !== "string") return null; + if (dataURL.startsWith("data:image/webp")) return dataURL; + + return new Promise(resolve => { + const img = new Image(); + img.onload = () => { + const canvas = document.createElement("canvas"); + canvas.width = img.width; + canvas.height = img.height; + const ctx = canvas.getContext("2d"); + ctx.drawImage(img, 0, 0); + resolve(canvas.toDataURL("image/webp", 0.9)); + }; + img.src = dataURL; + }); +} diff --git a/src/home.css b/src/home.css new file mode 100644 index 0000000..7c7fb8f --- /dev/null +++ b/src/home.css @@ -0,0 +1,70 @@ +.about { + display: flex; + flex-direction: column; + gap: 1.5rem; + padding: 3rem; + background-image: url("/icons/blur.png"); + background-size: cover; + background-position: center; + background-repeat: no-repeat; + text-align: center; + color: #f3f4f6; + font-size: larger; + text-shadow: 0 2px 3px black; +} + +.about * { + color: #f3f4f6; +} + +.feature-cards { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 2rem; + justify-items: center; +} + +.feature-card { + background-color: var(--color1); + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 0.5rem; + padding: 2rem; + border-radius: 1rem; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + max-width: 15rem; + text-shadow: none; + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.feature-card:hover { + transform: translateY(-5px); +} + +.feature-card h3 { + font-size: 1.25rem; + color: var(--dark); +} + +.feature-card p { + color: var(--dark-light); + font-size: 0.95rem; +} + +.cta-section { + padding: 2rem; + text-align: center; + background-color: var(--primary); + color: white; +} + +.cta-section h2 { + font-size: 2rem; + margin-bottom: 1.5rem; +} + +button.large { + font-size: 1.5rem; +} diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..d11c826 --- /dev/null +++ b/src/index.css @@ -0,0 +1,489 @@ +@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"); + +html { + --font: "Inter", sans-serif; + --dark: #2b323b; + --dark-light: #424c5a; + --primary: #833bf6; + --primary-dark: #8930dc; + --danger: #f63b3b; + --danger-dark: #dd3434; + --color1: #f3f4f6; + --color2: #e4e5e7; + --color3: #cbcdcf; + --color4: #b9bbbd; + --header-color: var(--primary); +} + +html.dark { + --dark: #e2e8f0; + --dark-light: #c8cdd4; + --primary: #833bf6; + --primary-dark: #8930dc; + --danger: #f63b3b; + --danger-dark: #dd3434; + --color1: #262d36; + --color2: #2f3741; + --color3: #3d4552; + --color4: #464f5e; + --header-color: var(--color4); +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +a { + color: var(--primary-dark); + text-decoration: none; +} + +h1, +h2, +h3, +input { + color: var(--dark); +} + +body, +div#app { + font-family: var(--font); + background-color: var(--color1); + overscroll-behavior: none; + height: 100vh; + width: 100vw; + display: flex; + flex-direction: column; +} + +div#app, +div#stage { + overflow: hidden; +} + +header { + background-color: var(--header-color); + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.3rem 0.5rem; + z-index: 10; + gap: 1rem; +} + +html.toolbox-left header { + justify-content: flex-start; +} + +html.toolbox-center header { + justify-content: center; +} + +html.toolbox-right header { + justify-content: flex-end; +} + +header h2, +header button:not(.white) { + color: #f3f4f6; +} + +header h1 { + font-size: 1.25rem; + font-weight: 700; + color: var(--color1); +} + +header div { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + align-items: center; + overflow: hidden; + gap: 0.5rem; +} + +.hidden, +html.removeRarryToolbar header img.logo { + display: none !important; +} + +button, +input[type="file"] { + font-family: var(--font); + font-size: medium; + font-weight: 700; + padding: 0.5rem 0.9rem; + border-radius: 0.5rem; + border: none; + color: var(--color1); + cursor: pointer; + will-change: background-color, scale; + transition: background-color 0.1s, scale 0.1s; +} + +input[type="text"], +input[type="password"], +select, +textarea:not(.blocklyTextarea) { + font-family: var(--font); + font-size: medium; + padding: 0.5rem; + border-radius: 0.5rem; + background-color: var(--color2); + border: 1px solid var(--color4); + color: var(--dark); +} + +button { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + justify-content: center; + align-items: center; + gap: 0.3rem; + background-color: var(--dark); +} + +button:not(.tab-button):active { + scale: 0.95; +} + +input[type="file"] { + background-color: var(--primary); +} + +input[type="file"]::file-selector-button { + font-family: var(--font); + background-color: white; + color: #2b323b; + border: none; + border-radius: 0.2rem; +} + +img.button { + width: 2rem; + cursor: pointer; + will-change: scale; + transition: scale 0.1s; +} + +img.button:active { + scale: 0.95; +} + +button:hover { + background-color: var(--dark-light); +} + +button.primary { + background-color: var(--primary); +} + +button.primary:hover { + background-color: var(--primary-dark); +} + +button.danger { + background-color: var(--danger); +} + +button.danger:hover { + background-color: var(--danger-dark); +} + +button.orange { + background-color: #f18f3f; +} + +button.orange:hover { + background-color: #e28940; +} + +button.green { + background-color: #2acc66; +} + +button.green:hover { + background-color: #28b85c; +} + +button.large { + font-size: 1.5rem; +} + +html.dark header button { + color: var(--color1); +} + +html:not(.removeIcons) button:has(i:not(.stay)) { + padding-left: 0.7rem; +} + +html.removeIcons button i:not(.stay) { + display: none; +} + +@keyframes scaleIn { + from { + opacity: 0; + scale: 0; + } + + to { + opacity: 1; + scale: 1; + } +} + +@keyframes scaleOut { + from { + opacity: 1; + scale: 1; + } + to { + opacity: 0; + scale: 0; + } +} + +.popup { + position: fixed; + inset: 0; + display: flex; + justify-content: center; + align-items: center; + background: rgba(0, 0, 0, 0.5); + z-index: 9999; +} + +.popup-content { + position: relative; + border-radius: 0.5rem; + width: max(300px, 50%); + height: max(300px, 50%); + background: var(--color2); + color: var(--dark); + animation: scaleIn 0.3s ease; + overflow: auto; +} + +.popup-content header { + padding: 0.5rem; + padding-left: 0.7rem; + border-top-left-radius: 0.5rem; + border-top-right-radius: 0.5rem; + width: 100%; + height: fit-content; +} + +.popup-body textarea.extension-code-input { + resize: vertical; + width: 100%; +} + +.popup-body, +.popup-row { + display: flex; + gap: 0.5rem; +} + +.popup-body { + padding: 0.5rem; + flex-direction: column; +} + +.popup-row { + flex-direction: row; + align-items: center; +} + +.popup-close { + margin-left: auto; + font-size: 1.2rem; + padding: 0.5rem; +} + +.notification-container { + position: fixed; + top: 1rem; + right: 1rem; + display: flex; + flex-direction: column; + gap: 0.5rem; + z-index: 10000; +} + +.notification { + background: var(--color2); + color: var(--dark); + border-radius: 0.5rem; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.25); + padding: 0.4rem; + padding-left: 0.8rem; + animation: scaleIn 0.3s ease forwards; +} + +.notification.hide { + animation: scaleOut 0.3s ease forwards; +} + +.notification-content { + display: flex; + align-items: center; + justify-content: space-between; +} + +.notification-close { + background: none; + color: var(--dark); + padding: 0.5rem; + font-size: 1rem; +} + +.notification-close:hover { + background: var(--danger); +} + +input[type="checkbox"] { + appearance: none; + -webkit-appearance: none; + -moz-appearance: none; + background-color: var(--color3); + cursor: pointer; + width: fit-content; + height: fit-content; + margin: 0; + padding: 0.7rem; + border-radius: 0.4rem; +} + +input[type="checkbox"]:checked { + background-color: var(--primary); + position: relative; +} + +input[type="checkbox"]:checked::after { + content: ""; + position: absolute; + top: 50%; + left: 50%; + width: 4px; + height: 9px; + border: solid #f3f4f6; + border-width: 0 2px 2px 0; + transform: translate(-50%, -50%) rotate(45deg); +} + +input[type="color"] { + background: none; + appearance: none; + -webkit-appearance: none; + border: none; + padding: 0; + width: 32px; + height: 32px; + cursor: pointer; +} + +.userTag { + padding: 0.5rem; + border-radius: 0.5rem; + background-color: var(--primary-dark); + color: #e2e8f0; +} + +.userTag a { + color: #e2e8f0; +} + +html.dark .userTag { + background-color: var(--color3); +} + +.userTag img { + width: 2rem; + height: 2rem; + border-radius: 20%; +} + +footer { + padding: 1.5rem; + text-align: center; + background-color: var(--color2); + color: var(--dark-light); + font-size: 0.9rem; + margin-top: auto; +} + +.dropdown-content button:hover { + background-color: var(--dark-light); +} + +::-webkit-scrollbar { + width: 8px; +} + +::-webkit-scrollbar-track { + background: var(--color2); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb { + background: var(--color3); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--color4); +} + +:disabled { + opacity: 0.5; + pointer-events: none; +} + +.project-name-input { + padding: 8px 16px; + border: 2px solid var(--border-color); + border-radius: 8px; + background: var(--background-primary); + color: var(--text-primary); + font-size: 15px; + font-weight: 600; + min-width: 220px; + text-align: center; + transition: all 0.2s ease; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.project-name-input:hover { + border-color: var(--primary-color); + box-shadow: 0 2px 8px rgba(59, 130, 246, 0.2); +} + +.project-name-input:focus { + outline: none; + border-color: var(--primary-color); + background: var(--background-secondary); + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); +} + +.project-name-input::placeholder { + color: var(--text-secondary); + opacity: 0.5; + font-weight: 500; +} + +header > div { + display: flex; + align-items: center; + gap: 12px; +} + +.project-name-input::before { + content: "📝"; + margin-right: 8px; +} \ No newline at end of file diff --git a/src/login.css b/src/login.css new file mode 100644 index 0000000..d7f2264 --- /dev/null +++ b/src/login.css @@ -0,0 +1,24 @@ +body { + background-image: url("/icons/blur.png"); + background-size: cover; + background-position: center; + background-repeat: no-repeat; +} + +body, +.info { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; +} + +.info { + border-radius: 1rem; + gap: 1.5rem; + padding: 2rem; + font-size: large; + background-color: var(--color1); + color: var(--dark); +} \ No newline at end of file diff --git a/src/scripts/editor.js b/src/scripts/editor.js new file mode 100644 index 0000000..6901146 --- /dev/null +++ b/src/scripts/editor.js @@ -0,0 +1,2743 @@ +import "@fortawesome/fontawesome-free/css/all.min.css"; + +import * as Blockly from "blockly"; +import * as BlocklyJS from "blockly/javascript"; +import * as PIXI from "pixi.js-legacy"; +import pako from "pako"; +import JSZip from "jszip"; +import { io } from "socket.io-client"; + +import CustomRenderer from "../functions/render.js"; +import { setupThemeButton } from "../functions/theme.js"; +import { + compressAudio, + compressImage, + promiseWithAbort, + showNotification, + showPopup, +} from "../functions/utils.js"; + +import { SpriteChangeEvents } from "../functions/patches.js"; +import { + registerExtension, + setupExtensions, +} from "../functions/extensionManager.js"; +import { Thread } from "../functions/threads.js"; +import { runCodeWithFunctions } from "../functions/runCode.js"; + +import config from "../config"; + +BlocklyJS.javascriptGenerator.addReservedWords( + "whenFlagClicked,moveSteps,getAngle,getMousePosition,sayMessage,waitOneFrame,wait,switchCostume,setSize,setAngle,projectTime,isKeyPressed,isMouseButtonPressed,getCostumeSize,getSpriteScale,_startTween,startTween,soundProperties,setSoundProperty,playSound,stopSound,stopAllSounds,isMouseTouchingSprite,setPenStatus,setPenColor,setPenColorHex,setPenSize,clearPen,Thread,fastExecution,BUBBLE_TEXTSTYLE,sprite,renderer,stage,costumeMap,soundMap,stopped,code,penGraphics,runningScripts,findOrFilterItem,registerEvent,triggerCustomEvent,hideSprite,showSprite,MyFunctions" +); + +import.meta.glob("../blocks/**/*.js", { eager: true }); + +Thread.resetAll(); + +let currentSocket = null; +let currentRoom = null; +let amHost = false; +let invitesEnabled = true; +let connectedUsers = []; + +const wrapper = document.getElementById("stage-wrapper"); +const stageContainer = document.getElementById("stage"); +const costumesList = document.getElementById("costumes-list"); +const loadInput = document.getElementById("load-input"); +const loadButton = document.getElementById("load-button"); +const deleteSpriteButton = document.getElementById("delete-sprite-button"); +const runButton = document.getElementById("run-button"); +const tabButtons = document.querySelectorAll(".tab-button"); +const tabContents = document.querySelectorAll(".tab-content"); +const fullscreenButton = document.getElementById("fullscreen-button"); + +export const BASE_WIDTH = 480; +export const BASE_HEIGHT = 360; +const MAX_HTTP_BUFFER = 20 * 1024 * 1024; + +const app = new PIXI.Application({ + width: BASE_WIDTH, + height: BASE_HEIGHT, + backgroundColor: 0xffffff, + powerPreference: "high-performance", +}); +app.stageWidth = BASE_WIDTH; +app.stageHeight = BASE_HEIGHT; + +export function resizeCanvas() { + if (!wrapper) return; + + const w = wrapper.clientWidth; + const h = wrapper.clientHeight; + + app.renderer.resize(w, h); + + const scale = Math.min(w / BASE_WIDTH, h / BASE_HEIGHT); + + app.stage.scale.set(scale); + + app.stage.x = w / 2; + app.stage.y = h / 2; +} +resizeCanvas(); + +stageContainer.appendChild(app.view); + +let penGraphics; +function createPenGraphics() { + if (penGraphics && !penGraphics._destroyed) return; + penGraphics = new PIXI.Graphics(); + penGraphics.clear(); + app.stage.addChildAt(penGraphics, 0); + window.penGraphics = penGraphics; +} +createPenGraphics(); + +export let projectVariables = {}; +export let sprites = []; +export let activeSprite = null; +window.projectSounds = []; +window.projectCostumes = ["default"]; +window.projectBackdrops = []; +let currentBackdrop = null; +let projectName = "Untitled Project"; + +Blockly.blockRendering.register("custom_zelos", CustomRenderer); + +let renderer = localStorage.getItem("renderer"); +if (!renderer) { + localStorage.setItem("renderer", "custom_zelos"); + renderer = "custom_zelos"; +} + +const blocklyDiv = document.getElementById("blocklyDiv"); +const toolbox = document.getElementById("toolbox"); +window.setBackdrop = setBackdrop; +export const workspace = Blockly.inject(blocklyDiv, { + toolbox: toolbox, + scrollbars: true, + trashcan: true, + renderer, + + grid: { + spacing: 20, + length: 1, + colour: "#ccc", + snap: false + }, + + zoom: { + controls: true, + wheel: true, + startScale: 0.9, + maxScale: 3, + minScale: 0.3, + scaleSpeed: 1.2, + }, + + plugins: { + connectionChecker: "CustomChecker", + }, +}); + +const observer = new ResizeObserver(() => { + Blockly.svgResize(workspace); +}); + +observer.observe(blocklyDiv); + +setupThemeButton(workspace); + +workspace.registerToolboxCategoryCallback("GLOBAL_VARIABLES", function (_) { + const xmlList = []; + + const button = Blockly.utils.xml.createElement("button"); + button.setAttribute("text", "Create variable"); + button.setAttribute("callbackKey", "ADD_GLOBAL_VARIABLE"); + xmlList.push(button); + + if (Object.keys(projectVariables).length === 0) return xmlList; + + const valueShadow = Blockly.utils.xml.createElement("value"); + valueShadow.setAttribute("name", "VALUE"); + const shadow = Blockly.utils.xml.createElement("shadow"); + shadow.setAttribute("type", "math_number"); + const field = Blockly.utils.xml.createElement("field"); + field.setAttribute("name", "NUM"); + field.textContent = "0"; + shadow.appendChild(field); + valueShadow.appendChild(shadow); + + const set = Blockly.utils.xml.createElement("block"); + set.setAttribute("type", "set_global_var"); + set.appendChild(valueShadow.cloneNode(true)); + xmlList.push(set); + + const change = Blockly.utils.xml.createElement("block"); + change.setAttribute("type", "change_global_var"); + change.appendChild(valueShadow); + xmlList.push(change); + + for (const name in projectVariables) { + const get = Blockly.utils.xml.createElement("block"); + get.setAttribute("type", "get_global_var"); + const varField = Blockly.utils.xml.createElement("field"); + varField.setAttribute("name", "VAR"); + varField.textContent = name; + get.appendChild(varField); + xmlList.push(get); + } + + return xmlList; +}); + +function addGlobalVariable(name, emit = false) { + if (!name) name = prompt("New variable name:"); + if (name) { + let newName = name, + count = 0; + while (newName in projectVariables) { + count++; + newName = name + count; + } + + projectVariables[newName] = 0; + + if (emit && currentSocket && currentRoom) + currentSocket.emit("projectUpdate", { + roomId: currentRoom, + type: "addVariable", + data: newName, + }); + } +} + +workspace.registerButtonCallback("ADD_GLOBAL_VARIABLE", () => + addGlobalVariable(null, true) +); + +function dynamicFunctionsCategory(workspace) { + const xmlList = []; + + const block = document.createElement("block"); + block.setAttribute("type", "functions_definition"); + xmlList.push(block); + + const blockReturnValue = document.createElement("value"); + blockReturnValue.setAttribute("name", "VALUE"); + blockReturnValue.innerHTML = + 'name'; + + const blockReturn = document.createElement("block"); + blockReturn.setAttribute("type", "functions_return"); + blockReturn.appendChild(blockReturnValue); + xmlList.push(blockReturn); + + const sep = document.createElement("sep"); + sep.setAttribute("gap", "50"); + xmlList.push(sep); + + const defs = workspace + .getTopBlocks(false) + .filter(b => b.type === "functions_definition"); + + defs.forEach(defBlock => { + const block = document.createElement("block"); + block.setAttribute("type", "functions_call"); + + const mutation = document.createElement("mutation"); + mutation.setAttribute("functionId", defBlock.functionId_); + mutation.setAttribute("shape", defBlock.blockShape_); + mutation.setAttribute("items", defBlock.argTypes_.length); + mutation.setAttribute( + "returntypes", + JSON.stringify(defBlock.returnTypes_ || []) + ); + + for (let i = 0; i < defBlock.argTypes_.length; i++) { + const item = document.createElement("item"); + item.setAttribute("type", defBlock.argTypes_[i]); + item.setAttribute("name", defBlock.argNames_[i]); + mutation.appendChild(item); + } + + block.appendChild(mutation); + xmlList.push(block); + }); + + return xmlList; +} + +workspace.registerToolboxCategoryCallback( + "FUNCTIONS_CATEGORY", + dynamicFunctionsCategory +); + +function addSprite(id, emit = false) { + const texture = PIXI.Texture.from("./icons/ddededodediamante.png", { + crossorigin: true, + }); + const sprite = new PIXI.Sprite(texture); + sprite.anchor.set(0.5); + sprite.x = 0; + sprite.y = 0; + sprite.scale._parentScaleEvent = sprite; + app.stage.addChild(sprite); + + if (!id) id = "sprite-" + Date.now(); + + const spriteData = { + id, + pixiSprite: sprite, + code: "", + costumes: [{ name: "default", texture: texture }], + sounds: [], + }; + sprites.push(spriteData); + + if (emit && currentSocket && currentRoom) + currentSocket.emit("projectUpdate", { + roomId: currentRoom, + type: "addSprite", + data: id, + }); + + return spriteData; +} + +function setActiveSprite(spriteData) { + activeSprite = spriteData; + renderSpritesList(true); + + const workspaceContainer = workspace.getParentSvg().parentNode; + + if (!spriteData) { + deleteSpriteButton.disabled = true; + workspaceContainer.style.display = "none"; + return; + } else { + deleteSpriteButton.disabled = false; + workspaceContainer.style.display = ""; + } + + Blockly.Events.disable(); + + const xmlText = + activeSprite.code || + ''; + const xmlDom = Blockly.utils.xml.textToDom(xmlText); + Blockly.Xml.clearWorkspaceAndLoadFromXml(xmlDom, workspace); + + Blockly.Events.enable(); +} + +function deleteSprite(id, emit = false) { + const sprite = sprites.find(s => s.id === id); + if (!sprite) return; + + if (sprite.currentBubble) { + app.stage.removeChild(sprite.currentBubble); + sprite.currentBubble = null; + } + + app.stage.removeChild(sprite.pixiSprite); + + const index = sprites.indexOf(sprite); + + if (emit && currentSocket && currentRoom) + currentSocket.emit("projectUpdate", { + roomId: currentRoom, + type: "removeSprite", + data: id, + }); + + // ADD THIS CODE to remove costumes and sounds from global arrays: + sprite.costumes.forEach(costume => { + // Check if this costume exists in any other sprite + const existsElsewhere = sprites.some(s => + s.id !== sprite.id && s.costumes.some(c => c.name === costume.name) + ); + if (!existsElsewhere) { + window.projectCostumes = window.projectCostumes.filter(c => c !== costume.name); + } + }); + + sprite.sounds.forEach(sound => { + // Check if this sound exists in any other sprite + const existsElsewhere = sprites.some(s => + s.id !== sprite.id && s.sounds.some(snd => snd.name === sound.name) + ); + if (!existsElsewhere) { + window.projectSounds = window.projectSounds.filter(s => s !== sound.name); + } + }); + + sprites = sprites.filter(s => s.id !== sprite.id); + + workspace.clear(); + + if (sprites.length > 0) { + setActiveSprite(sprites[Math.min(index, sprites.length - 1)]); + } else { + setActiveSprite(null); + } + + // ADD THIS LINE to refresh toolbox: + workspace.updateToolbox(document.getElementById('toolbox')); +} + +function renderSpritesList(renderOthers = false) { + const listEl = document.getElementById("sprites-list"); + listEl.innerHTML = ""; + if (sprites.length === 0) listEl.style.display = "none"; + else listEl.style.display = ""; + + sprites.forEach(spriteData => { + const spriteIconContainer = document.createElement("div"); + if (activeSprite && activeSprite.id === spriteData.id) + spriteIconContainer.className = "active"; + + const img = new Image(50, 50); + img.style.objectFit = "contain"; + const costumeTexture = spriteData.pixiSprite.texture; + const baseTex = costumeTexture.baseTexture; + + if (baseTex.valid) { + img.src = baseTex.resource?.url || ""; + } else { + baseTex.on("loaded", () => { + img.src = baseTex.resource?.url || ""; + }); + } + + spriteIconContainer.appendChild(img); + spriteIconContainer.onclick = () => setActiveSprite(spriteData); + listEl.appendChild(spriteIconContainer); + }); + + if (renderOthers === true) { + renderSpriteInfo(); + renderCostumesList(); + renderSoundsList(); + } +} + +function renderSpriteInfo() { + const infoEl = document.getElementById("sprite-info"); + + if (!activeSprite) { + infoEl.innerHTML = "

Select a sprite to see its info.

"; + } else { + const sprite = activeSprite.pixiSprite; + + infoEl.innerHTML = ` +

${Math.round(sprite.x)}, ${Math.round(-sprite.y)}

+

${Math.round(sprite.angle)}º

+

size: ${Math.round(((sprite.scale.x + sprite.scale.y) / 2) * 100)}

+

+ `; + } +} + +function createRenameableLabel(initialName, onRename) { + const container = document.createElement("div"); + container.style.display = "flex"; + container.style.alignItems = "center"; + container.style.gap = "8px"; + + const nameLabel = document.createElement("p"); + nameLabel.textContent = initialName; + nameLabel.style.margin = "0"; + nameLabel.style.cursor = "pointer"; + + function startRename() { + let willRename = true; + + const input = document.createElement("input"); + input.type = "text"; + input.value = nameLabel.textContent; + input.style.flexGrow = "1"; + + container.replaceChild(input, nameLabel); + input.focus(); + input.select(); + + function commit() { + if (willRename) { + const newName = input.value.trim(); + if (newName && newName !== nameLabel.textContent) { + onRename(newName); + nameLabel.textContent = newName; + } + } + container.replaceChild(nameLabel, input); + } + + input.addEventListener("blur", commit); + input.addEventListener("keydown", e => { + if (e.key === "Enter") input.blur(); + else if (e.key === "Escape") { + willRename = false; + input.blur(); + } + }); + } + + nameLabel.addEventListener("click", startRename); + container.appendChild(nameLabel); + + return container; +} + +function createDeleteButton(onDelete) { + const img = document.createElement("img"); + img.src = "icons/trash.svg"; + img.className = "button"; + img.draggable = false; + img.onclick = onDelete; + return img; +} + +function renderCostumesList() { + costumesList.innerHTML = ""; + + if (!activeSprite || !activeSprite.costumes) return; + + activeSprite.costumes.forEach((costume, index) => { + const costumeContainer = document.createElement("div"); + costumeContainer.className = "costume-container"; + + const img = new Image(60, 60); + img.style.objectFit = "contain"; + img.src = costume.texture.baseTexture.resource.url; + + const renameableLabel = createRenameableLabel(costume.name, newName => { + const oldName = costume.name; + costume.name = newName; + + // ADD THIS CODE: + const oldIndex = window.projectCostumes.indexOf(oldName); + if (oldIndex !== -1 && !window.projectCostumes.includes(newName)) { + window.projectCostumes[oldIndex] = newName; + workspace.updateToolbox(document.getElementById('toolbox')); + } + + if (currentSocket && currentRoom) { + currentSocket.emit("projectUpdate", { + roomId: currentRoom, + type: "renameCostume", + data: { + spriteId: activeSprite.id, + oldName, + newName, + }, + }); + } + }); + + const _texture = costume.texture.baseTexture || costume.texture; + const sizeLabel = document.createElement("span"); + sizeLabel.className = "smallLabel"; + sizeLabel.textContent = "Loading..."; + if (_texture.valid) { + sizeLabel.textContent = `${_texture.width}x${_texture.height}`; + } else { + _texture.once("update", () => { + sizeLabel.textContent = `${_texture.width}x${_texture.height}`; + }); + } + + const deleteBtn = createDeleteButton(() => { + const deleted = activeSprite.costumes[index]; + activeSprite.costumes.splice(index, 1); + + // ADD THIS CODE to remove from global array if not used elsewhere: + if (deleted) { + const existsElsewhere = sprites.some(s => + s.id !== activeSprite.id && s.costumes.some(c => c.name === deleted.name) + ); + if (!existsElsewhere) { + window.projectCostumes = window.projectCostumes.filter(c => c !== deleted.name); + } + } + + if (activeSprite.costumes.length > 0) { + activeSprite.pixiSprite.texture = activeSprite.costumes[0].texture; + } else { + activeSprite.pixiSprite.texture = PIXI.Texture.EMPTY; + } + renderCostumesList(); + + // ADD THIS LINE to refresh toolbox: + workspace.updateToolbox(document.getElementById('toolbox')); + + if (currentSocket && currentRoom && deleted) { + currentSocket.emit("projectUpdate", { + roomId: currentRoom, + type: "deleteCostume", + data: { + spriteId: activeSprite.id, + name: deleted.name, + }, + }); + } + }); + + costumeContainer.appendChild(img); + costumeContainer.appendChild(renameableLabel); + costumeContainer.appendChild(deleteBtn); + costumeContainer.appendChild(sizeLabel); + + costumesList.appendChild(costumeContainer); + }); +} + +function renderSoundsList() { + const soundsList = document.getElementById("sounds-list"); + soundsList.innerHTML = ""; + + if (!activeSprite || !activeSprite.sounds) return; + + activeSprite.sounds.forEach((sound, index) => { + const container = document.createElement("div"); + container.className = "sound-container"; + + let sizeBytes = 0; + if (sound.dataURL) { + const base64Length = + sound.dataURL.length - (sound.dataURL.indexOf(",") + 1); + sizeBytes = Math.floor((base64Length * 3) / 4); + } + + const renameableLabel = createRenameableLabel(sound.name, newName => { + const oldName = sound.name; + sound.name = newName; + + // ADD THIS CODE: + const oldIndex = window.projectSounds.indexOf(oldName); + if (oldIndex !== -1 && !window.projectSounds.includes(newName)) { + window.projectSounds[oldIndex] = newName; + workspace.updateToolbox(document.getElementById('toolbox')); + } + + if (currentSocket && currentRoom) { + currentSocket.emit("projectUpdate", { + roomId: currentRoom, + type: "renameSound", + data: { + spriteId: activeSprite.id, + oldName, + newName, + }, + }); + } + }); + + let sizeLabel; + if (typeof sizeBytes === "number" && sizeBytes > 0) { + sizeLabel = document.createElement("span"); + sizeLabel.className = "smallLabel"; + + const sizeKB = sizeBytes / 1024; + if (sizeKB < 1024) { + sizeLabel.textContent = `${sizeKB.toFixed(2)} KB`; + } else { + sizeLabel.textContent = `${(sizeKB / 1024).toFixed(2)} MB`; + } + } + + const playButton = document.createElement("img"); + playButton.src = "icons/play.svg"; + playButton.className = "button"; + playButton.draggable = false; + playButton.onclick = () => { + if (playButton.audio) { + playButton.audio.pause(); + playButton.audio.currentTime = 0; + playButton.src = "icons/play.svg"; + playButton.audio = null; + } else { + const audio = new Audio(sound.dataURL); + playButton.audio = audio; + playButton.src = "icons/stopAudio.svg"; + + audio.addEventListener("ended", () => { + if (playButton.audio === audio) { + playButton.src = "icons/play.svg"; + playButton.audio = null; + } + }); + + audio.play(); + } + }; + + const deleteBtn = createDeleteButton(() => { + const deleted = activeSprite.sounds[index]; + activeSprite.sounds.splice(index, 1); + + if (deleted) { + const existsElsewhere = sprites.some(s => + s.id !== activeSprite.id && s.sounds.some(snd => snd.name === deleted.name) + ); + if (!existsElsewhere) { + window.projectSounds = window.projectSounds.filter(s => s !== deleted.name); + } + } + + if (playButton.audio) { + playButton.audio.pause(); + playButton.audio.currentTime = 0; + playButton.audio = null; + } + renderSoundsList(); + + workspace.updateToolbox(document.getElementById('toolbox')); + + if (currentSocket && currentRoom && deleted) { + currentSocket.emit("projectUpdate", { + roomId: currentRoom, + type: "deleteSound", + data: { + spriteId: activeSprite.id, + name: deleted.name, + }, + }); + } + }); + + container.appendChild(renameableLabel); + container.appendChild(playButton); + container.appendChild(deleteBtn); + if (sizeLabel) container.appendChild(sizeLabel); + soundsList.appendChild(container); + }); +} + +function renderBackdropsList() { + const listEl = document.getElementById("backdrops-list"); + const deleteBtn = document.getElementById("delete-backdrop-button"); + + if (!listEl) return; + + listEl.innerHTML = ""; + + if (window.projectBackdrops.length === 0) { + listEl.style.display = "none"; + if (deleteBtn) deleteBtn.disabled = true; + } else { + listEl.style.display = ""; + if (deleteBtn) deleteBtn.disabled = false; + } + + window.projectBackdrops.forEach((backdrop, index) => { + const backdropContainer = document.createElement("div"); + if (currentBackdrop === index) { + backdropContainer.className = "active"; + } + + const img = new Image(); + img.style.objectFit = "cover"; + + const baseTex = backdrop.texture.baseTexture; + if (baseTex.valid) { + img.src = baseTex.resource?.url || ""; + } else { + baseTex.on("loaded", () => { + img.src = baseTex.resource?.url || ""; + }); + } + + backdropContainer.appendChild(img); + backdropContainer.onclick = () => setBackdrop(index); + backdropContainer.title = backdrop.name; + listEl.appendChild(backdropContainer); + }); +} + +function setBackdrop(index) { + if (!window.projectBackdrops || window.projectBackdrops.length === 0) { + app.renderer.backgroundColor = 0xffffff; + currentBackdrop = null; + return; + } + + if (index < 0 || index >= window.projectBackdrops.length) { + // Clear backdrop + app.renderer.backgroundColor = 0xffffff; + currentBackdrop = null; + + // Remove any existing backdrop sprite + const oldBackdrop = app.stage.children.find(child => child.isBackdrop); + if (oldBackdrop) { + app.stage.removeChild(oldBackdrop); + } + + renderBackdropsList(); + return; + } + + currentBackdrop = index; + const backdrop = window.projectBackdrops[index]; + + if (backdrop && backdrop.texture) { + // Remove old backdrop sprite if exists + const oldBackdrop = app.stage.children.find(child => child.isBackdrop); + if (oldBackdrop) { + app.stage.removeChild(oldBackdrop); + } + + // Create new backdrop sprite + const backdropSprite = new PIXI.Sprite(backdrop.texture); + backdropSprite.isBackdrop = true; + backdropSprite.anchor.set(0.5); + backdropSprite.x = 0; + backdropSprite.y = 0; + + // Scale to cover the stage + const scaleX = BASE_WIDTH / backdrop.texture.width; + const scaleY = BASE_HEIGHT / backdrop.texture.height; + const scale = Math.max(scaleX, scaleY); + backdropSprite.scale.set(scale); + + // Add at index 0 or right after penGraphics + const penIndex = app.stage.getChildIndex(penGraphics); + app.stage.addChildAt(backdropSprite, penIndex); + + backdrop.sprite = backdropSprite; + } + + renderBackdropsList(); + + if (currentSocket && currentRoom) { + currentSocket.emit("projectUpdate", { + roomId: currentRoom, + type: "setBackdrop", + data: backdrop.name, // CHANGED: Send name instead of index + }); + } +} + +function setBackdropByName(name) { + if (!name || !window.projectBackdrops || window.projectBackdrops.length === 0) { + setBackdrop(-1); + return; + } + + const index = window.projectBackdrops.findIndex(b => b.name === name); + if (index === -1) { + console.warn(`Backdrop "${name}" not found`); + return; + } + + setBackdrop(index); +} + +// Make it globally available +window.setBackdropByName = setBackdropByName; + +function addBackdrop(name, textureData, emit = false) { + const texture = PIXI.Texture.from(textureData); + + let uniqueName = name; + let counter = 1; + while (window.projectBackdrops.some(b => b.name === uniqueName)) { + counter++; + uniqueName = `${name}_${counter}`; + } + + const backdropSprite = new PIXI.Sprite(texture); + backdropSprite.isBackdrop = true; + backdropSprite.anchor.set(0.5); + backdropSprite.x = 0; + backdropSprite.y = 0; + + const scaleX = BASE_WIDTH / texture.width; + const scaleY = BASE_HEIGHT / texture.height; + const scale = Math.max(scaleX, scaleY); + backdropSprite.scale.set(scale); + + window.projectBackdrops.push({ + name: uniqueName, + texture, + sprite: backdropSprite, + data: textureData, + }); + + renderBackdropsList(); + workspace.updateToolbox(document.getElementById('toolbox')); + + if (emit && currentSocket && currentRoom) { + currentSocket.emit("projectUpdate", { + roomId: currentRoom, + type: "addBackdrop", + data: { + name: uniqueName, + texture: textureData, + }, + }); + } + + return window.projectBackdrops.length - 1; +} + +function deleteBackdrop(index, emit = false) { + if (index < 0 || index >= window.projectBackdrops.length) return; + + const backdrop = window.projectBackdrops[index]; + + // Remove sprite from stage if it's current + if (currentBackdrop === index && backdrop.sprite) { + app.stage.removeChild(backdrop.sprite); + currentBackdrop = null; + app.renderer.backgroundColor = 0xffffff; + } + + window.projectBackdrops.splice(index, 1); + + // Adjust currentBackdrop index if needed + if (currentBackdrop !== null && currentBackdrop > index) { + currentBackdrop--; + } else if (currentBackdrop === index) { + currentBackdrop = null; + } + + renderBackdropsList(); + workspace.updateToolbox(document.getElementById('toolbox')); + + if (emit && currentSocket && currentRoom) { + currentSocket.emit("projectUpdate", { + roomId: currentRoom, + type: "deleteBackdrop", + data: index, + }); + } +} + +export function calculateBubblePosition( + sprite, + bubbleWidth, + bubbleHeight, + tailHeight = 15 +) { + let bubbleX = sprite.x - bubbleWidth / 2; + let bubbleY = sprite.y - sprite.height / 2 - bubbleHeight - tailHeight; + + bubbleX = Math.max( + Math.min(bubbleX, app.stageWidth / 2), + -app.stageWidth / 2 - bubbleWidth + ); + bubbleY = Math.max( + Math.min(bubbleY, app.stageHeight / 2 - bubbleHeight), + -app.stageHeight / 2 + ); + + return { x: bubbleX, y: bubbleY }; +} + +const keysPressed = {}; +const mouseButtonsPressed = {}; +const playingSounds = new Map(); + +let currentRunController = null; + +let eventRegistry = { + flag: [], + key: new Map(), + stageClick: [], + timer: [], + interval: [], + custom: new Map(), +}; + +let _activeEventThreadsCount = 0; +const activeEventThreads = {}; + +Object.defineProperty(activeEventThreads, "count", { + get() { + return _activeEventThreadsCount; + }, + set(value) { + _activeEventThreadsCount = Math.max(0, value); + updateRunButtonState(); + }, +}); + +function updateRunButtonState() { + if (runningScripts.length > 0 || activeEventThreads.count > 0) { + runButton.classList.add("active"); + } else { + runButton.classList.remove("active"); + } +} + +const runningScripts = []; + +function stopAllScripts() { + if (currentRunController) { + try { + currentRunController.abort(); + } catch (e) {} + currentRunController = null; + } + + for (const i of runningScripts) { + if (i.type === "timeout") clearTimeout(i.id); + else if (i.type === "interval") clearInterval(i.id); + else if (i.type === "raf") cancelAnimationFrame(i.id); + } + runningScripts.length = 0; + + for (const spriteSounds of playingSounds.values()) { + for (const audio of spriteSounds.values()) { + try { + audio.pause(); + audio.currentTime = 0; + } catch (e) {} + } + } + playingSounds.clear(); + + for (const k in keysPressed) delete keysPressed[k]; + for (const k in mouseButtonsPressed) delete mouseButtonsPressed[k]; + + for (const type in eventRegistry) { + if (Array.isArray(eventRegistry[type])) { + eventRegistry[type].length = 0; + } else if (eventRegistry[type] instanceof Map) { + eventRegistry[type].clear(); + } + } + + Thread.resetAll(); + activeEventThreads.count = 0; + + for (const spriteData of sprites) { + const bubble = spriteData.currentBubble; + if (bubble) { + if (bubble.destroy) bubble.destroy({ children: true }); + spriteData.currentBubble = null; + } + + if (spriteData.sayTimeout) { + clearTimeout(spriteData.sayTimeout); + spriteData.sayTimeout = null; + } + } +} + +async function runCode() { + stopAllScripts(); + + await new Promise(r => requestAnimationFrame(r)); + + runButton.classList.add("active"); + + const controller = new AbortController(); + const signal = controller.signal; + currentRunController = controller; + + let projectStartedTime = Date.now(); + + try { + for (const spriteData of sprites) { + const tempWorkspace = new Blockly.Workspace({ + readOnly: true, + plugins: { + connectionChecker: "CustomChecker", + }, + }); + + const xmlText = spriteData.code || ""; + const xmlDom = Blockly.utils.xml.textToDom(xmlText); + Blockly.Xml.domToWorkspace(xmlDom, tempWorkspace); + + const code = BlocklyJS.javascriptGenerator.workspaceToCode(tempWorkspace); + tempWorkspace.dispose(); + + try { + runCodeWithFunctions({ + code, + projectStartedTime, + spriteData, + app, + eventRegistry, + mouseButtonsPressed, + keysPressed, + playingSounds, + runningScripts, + signal, + penGraphics, + activeEventThreads, + }); + } catch (e) { + console.error(`Error processing code for sprite ${spriteData.id}:`, e); + } + } + + const results = await Promise.allSettled( + eventRegistry.flag.map(entry => promiseWithAbort(entry.cb, signal)) + ); + + results.forEach(res => { + if (res.status === "rejected" && res.reason?.message !== "shouldStop") { + console.error("Error running flag event:", res.reason); + } + }); + + for (const entry of eventRegistry.timer) { + const id = setTimeout(() => entry.cb(), entry.value * 1000); + runningScripts.push({ type: "timeout", id }); + } + + for (const entry of eventRegistry.interval) { + const id = setInterval(() => entry.cb(), entry.seconds * 1000); + runningScripts.push({ type: "interval", id }); + } + } catch (err) { + console.error("Error running project:", err); + stopAllScripts(); + } finally { + updateRunButtonState(); + } +} + +app.view.addEventListener("click", () => { + for (const entry of eventRegistry.stageClick) { + entry.cb(); + } +}); + +document.getElementById("add-sprite-button").addEventListener("click", () => { + let spriteData = addSprite(null, true); + setActiveSprite(spriteData); +}); + +deleteSpriteButton.addEventListener("click", () => + deleteSprite(activeSprite.id, true) +); + +runButton.addEventListener("click", runCode); +document + .getElementById("stop-button") + .addEventListener("click", stopAllScripts); + +tabButtons.forEach(button => { + button.addEventListener("click", () => { + const tab = button.dataset.tab; + if (tab !== "sounds") { + document.querySelectorAll("#sounds-list .button").forEach(i => { + if (i.audio) { + i.audio.pause(); + i.audio.currentTime = 0; + i.audio = null; + i.src = "icons/play.svg"; + } + }); + } + + tabButtons.forEach(i => { + i.classList.add("inactive"); + }); + + button.classList.remove("inactive"); + + tabContents.forEach(content => { + content.classList.toggle("active", content.id === `${tab}-tab`); + }); + + if (tab === "code") { + setTimeout(() => Blockly.svgResize(workspace), 0); + } else if (tab === "costumes") { + renderCostumesList(); + } else if (tab === "sounds") { + renderSoundsList(); + } + }); +}); + +export async function getProject() { + const spritesData = await Promise.all( + sprites.map(async sprite => { + const costumesData = await Promise.all( + sprite.costumes.map(async c => { + let dataURL; + const url = c?.texture?.baseTexture?.resource?.url; + if (typeof url === "string" && url.startsWith("data:")) { + dataURL = url; + } else { + dataURL = await app.renderer.extract.base64( + new PIXI.Sprite(c.texture) + ); + } + return { + name: c.name, + data: dataURL, + }; + }) + ); + + return { + id: sprite.id, + code: sprite.code, + costumes: costumesData, + sounds: sprite.sounds.map(s => ({ name: s.name, data: s.dataURL })), + data: { + x: sprite.pixiSprite.x, + y: sprite.pixiSprite.y, + scale: { + x: sprite.pixiSprite.scale.x ?? 1, + y: sprite.pixiSprite.scale.y ?? 1, + }, + angle: sprite.pixiSprite.angle, + currentCostume: sprite.costumes.findIndex( + c => c.texture === sprite.pixiSprite.texture + ), + }, + }; + }) + ); + + const backdropsData = window.projectBackdrops.map(backdrop => ({ + name: backdrop.name, + data: backdrop.data, + })); + + return { + sprites: spritesData, + extensions: activeExtensions, + variables: projectVariables ?? {}, + backdrops: backdropsData, + currentBackdrop: currentBackdrop, + projectName: projectName, + }; +} + +async function saveProject() { + const zip = new JSZip(); + const json = { + sprites: [], + extensions: activeExtensions, + variables: projectVariables ?? {}, + backdrops: [], // ADD THIS + currentBackdrop: currentBackdrop, // ADD THIS + projectName: projectName, + }; + const toUint8Array = base64 => + Uint8Array.from(atob(base64), c => c.charCodeAt(0)); + + await Promise.all( + sprites.map(async sprite => { + const spriteId = sprite.id; + + const costumeEntries = ( + await Promise.all( + sprite.costumes.map(async c => { + let dataURL; + const url = c?.texture?.baseTexture?.resource?.url; + if (typeof url === "string" && url.startsWith("data:")) { + dataURL = url; + } else { + dataURL = await app.renderer.extract.base64( + new PIXI.Sprite(c.texture) + ); + } + + const processed = await compressImage(dataURL); + if (!processed) return null; + + const base64 = processed.split(",")[1]; + const binary = toUint8Array(base64); + const fileName = `${spriteId}.c.${c.name}.webp`; + zip.file(fileName, binary, { binary: true }); + return { name: c.name, path: fileName }; + }) + ) + ).filter(Boolean); + + const soundEntries = ( + await Promise.all( + sprite.sounds.map(async s => { + const processed = await compressAudio(s.dataURL); + if (!processed) return null; + + const base64 = processed.split(",")[1]; + const binary = toUint8Array(base64); + const fileName = `${spriteId}.s.${s.name}.ogg`; + zip.file(fileName, binary, { binary: true }); + return { name: s.name, path: fileName }; + }) + ) + ).filter(Boolean); + + json.sprites.push({ + id: spriteId, + code: sprite.code, + costumes: costumeEntries, + sounds: soundEntries, + data: { + x: sprite.pixiSprite.x, + y: sprite.pixiSprite.y, + scale: { + x: sprite.pixiSprite.scale.x ?? 1, + y: sprite.pixiSprite.scale.y ?? 1, + }, + angle: sprite.pixiSprite.angle, + currentCostume: sprite.costumes.findIndex( + c => c.texture === sprite.pixiSprite.texture + ), + }, + }); + }) + ); + + // ADD THIS SECTION to save backdrops: + if (window.projectBackdrops && window.projectBackdrops.length > 0) { + const backdropEntries = await Promise.all( + window.projectBackdrops.map(async (backdrop, index) => { + let dataURL = backdrop.data; + + // If we don't have the data URL saved, extract it + if (!dataURL) { + dataURL = await app.renderer.extract.base64( + new PIXI.Sprite(backdrop.texture) + ); + } + + const processed = await compressImage(dataURL); + if (!processed) return null; + + const base64 = processed.split(",")[1]; + const binary = toUint8Array(base64); + const fileName = `backdrop.${index}.${backdrop.name}.webp`; + zip.file(fileName, binary, { binary: true }); + return { name: backdrop.name, path: fileName }; + }) + ); + + json.backdrops = backdropEntries.filter(Boolean); + } + + zip.file("project.json", JSON.stringify(json)); + const blob = await zip.generateAsync({ + type: "blob", + compression: "DEFLATE", + compressionOptions: { level: 9 }, + }); + + const sanitizedName = projectName.replace(/[^a-z0-9]/gi, '_').toLowerCase() || "untitled_project"; + const a = document.createElement("a"); + a.href = URL.createObjectURL(blob); + a.download = `${sanitizedName}.neo`; + a.click(); + URL.revokeObjectURL(a.href); +} + +async function loadProject(ev) { + const [file] = ev.target.files ?? []; + if (!file) return; + + // If it's an old format file, use the old loader + if (file.name.endsWith(".NeoIDE") || file.name.endsWith(".NeoIDEz")) { + return oldLoadProject(ev); + } + + // Otherwise use the new ZIP-based format + try { + const zip = await JSZip.loadAsync(await file.arrayBuffer()); + const json = JSON.parse(await zip.file("project.json").async("string")); + const sprites = []; + + for (const entry of json.sprites) { + const sprite = { ...entry, costumes: [], sounds: [] }; + + await Promise.all([ + ...(entry.costumes || []).map(async c => { + const base64 = await zip.file(c.path).async("base64"); + sprite.costumes.push({ + name: c.name, + data: `data:image/webp;base64,${base64}`, + }); + }), + ...(entry.sounds || []).map(async s => { + const base64 = await zip.file(s.path).async("base64"); + sprite.sounds.push({ + name: s.name, + data: `data:audio/ogg;base64,${base64}`, + }); + }), + ]); + + sprites.push(sprite); + } + + // Load backdrops + const backdrops = []; + if (Array.isArray(json.backdrops)) { + await Promise.all( + json.backdrops.map(async backdrop => { + const base64 = await zip.file(backdrop.path).async("base64"); + backdrops.push({ + name: backdrop.name, + data: `data:image/webp;base64,${base64}`, + }); + }) + ); + } + + handleProjectData({ + sprites, + extensions: json.extensions, + variables: json.variables, + backdrops, + currentBackdrop: json.currentBackdrop, + projectName: json.projectName, // ADD THIS LINE + }); + } catch (err) { + console.error("Failed to load project file:", err); + window.alert("Failed to load project. The file may be corrupted."); + } +} + +async function oldLoadProject(input) { + if (typeof input === "object" && !input.target) { + return await handleProjectData(input); + } + if (typeof input === "string") { + try { + const data = JSON.parse(input); + return await handleProjectData(data); + } catch (err) { + console.error("Invalid JSON string passed to loadProject:", err); + return window.alert("Invalid JSON string provided."); + } + } + + const file = input?.target?.files?.[0]; + if (!file) return; + + stopAllScripts(); + + const reader = new FileReader(); + reader.onload = async () => { + input.target.value = ""; + + const buffer = reader.result; + + let data; + try { + const text = new TextDecoder().decode(buffer); + data = JSON.parse(text); + } catch { + try { + // ADD THIS CHECK to verify it's actually compressed data + const uint8Array = new Uint8Array(buffer); + + // Check if it looks like gzip/deflate header + if (uint8Array[0] === 0x1f && uint8Array[1] === 0x8b) { + // It's gzip + const inflated = pako.inflate(uint8Array); + const json = new TextDecoder().decode(inflated); + data = JSON.parse(json); + } else if (uint8Array[0] === 0x78) { + // It's zlib/deflate + const inflated = pako.inflate(uint8Array); + const json = new TextDecoder().decode(inflated); + data = JSON.parse(json); + } else { + // Try inflating anyway as last resort + try { + const inflated = pako.inflate(uint8Array); + const json = new TextDecoder().decode(inflated); + data = JSON.parse(json); + } catch (inflateErr) { + console.error("Failed to parse file", inflateErr); + return window.alert("Invalid or corrupted project file. Please make sure you're loading a valid .neo or .NeoIDE file."); + } + } + } catch (err) { + console.error("Failed to parse file", err); + return window.alert("Invalid or corrupted project file. Please make sure you're loading a valid .neo or .NeoIDE file."); + } + } + + await handleProjectData(data); + }; + reader.readAsArrayBuffer(file); +} + +async function handleProjectData(data) { + if (!data || typeof data !== "object") { + console.error("Invalid project data:", data); + window.alert("Invalid project data."); + return; + } + + if (!data.sprites && !data.extensions) { + data = { sprites: data, extensions: [] }; + } + + try { + console.log("Loading project data:", data); + console.log("Project name from file:", data.projectName); + + if (data.projectName) { + console.log("Setting project name to:", data.projectName); + updateProjectNameInput(data.projectName); + } else { + console.log("No project name found, using default"); + updateProjectNameInput("Untitled Project"); + } + + // Verify it was set + console.log("Current projectName variable:", projectName); + console.log("Input element value:", document.getElementById("project-name-input")?.value); + + if (data?.extensions) { + const extensionsToLoad = data.extensions.filter( + i => !activeExtensions.some(z => (z?.id || z) === (i?.id || i)) + ); + + for (const ext of extensionsToLoad) { + try { + if (typeof ext === "string") { + addExtension(ext); + } else if (ext?.id) { + const ExtensionClass = await eval("(" + ext.code + ")"); + if (ExtensionClass) await registerExtension(ExtensionClass); + } + } catch (err) { + console.error("Failed to load extension", ext?.id || ext, err); + } + } + } + + for (const child of app.stage.removeChildren()) { + if (child.destroy) child.destroy({ children: true }); + } + sprites = []; + + if (!Array.isArray(data.sprites)) { + window.alert("No valid sprites found in file."); + return; + } + + if (data.variables) projectVariables = data.variables; + + // Reset arrays + window.projectCostumes = ["default"]; + window.projectSounds = []; + window.projectBackdrops = []; + currentBackdrop = null; + + createPenGraphics(); + + // MOVE BACKDROP LOADING HERE - BEFORE sprites are created + if (Array.isArray(data.backdrops)) { + window.projectBackdrops = []; + + // Use Promise.all to wait for all textures to load + await Promise.all( + data.backdrops.map(async (backdrop) => { + if (!backdrop?.data || !backdrop.name) return; + + return new Promise((resolve, reject) => { + try { + const texture = PIXI.Texture.from(backdrop.data); + + // Wait for texture to be ready + const onLoad = () => { + const backdropSprite = new PIXI.Sprite(texture); + backdropSprite.isBackdrop = true; + backdropSprite.anchor.set(0.5); + backdropSprite.x = 0; + backdropSprite.y = 0; + + const scaleX = BASE_WIDTH / texture.width; + const scaleY = BASE_HEIGHT / texture.height; + const scale = Math.max(scaleX, scaleY); + backdropSprite.scale.set(scale); + + window.projectBackdrops.push({ + name: backdrop.name, + texture, + sprite: backdropSprite, + data: backdrop.data, + }); + + resolve(); + }; + + if (texture.baseTexture.valid) { + onLoad(); + } else { + texture.baseTexture.once('loaded', onLoad); + texture.baseTexture.once('error', () => { + console.warn(`Failed to load backdrop: ${backdrop.name}`); + resolve(); // Resolve anyway to not block loading + }); + } + } catch (err) { + console.warn(`Failed to load backdrop: ${backdrop.name}`, err); + resolve(); // Resolve anyway to not block loading + } + }); + }) + ); + + console.log('Backdrops loaded:', window.projectBackdrops.map(b => b.name)); + } + + data?.sprites?.forEach((entry, i) => { + if (!entry || typeof entry !== "object") return; + + const spriteData = { + id: entry.id || `sprite-${i}`, + code: entry.code || "", + costumes: [], + sounds: [], + data: { + x: entry?.data?.x ?? 0, + y: entry?.data?.y ?? 0, + scale: { + x: entry?.data?.scale?.x ?? 1, + y: entry?.data?.scale?.y ?? 1, + }, + angle: entry?.data?.angle ?? 0, + rotation: entry?.data?.rotation ?? 0, + currentCostume: entry?.data?.currentCostume, + }, + }; + + if (Array.isArray(entry.costumes)) { + entry.costumes.forEach(c => { + if (!c?.data || !c.name) return; + try { + const texture = PIXI.Texture.from(c.data); + spriteData.costumes.push({ name: c.name, texture }); + + if (!window.projectCostumes.includes(c.name)) { + window.projectCostumes.push(c.name); + } + } catch (err) { + console.warn(`Failed to load costume: ${c.name}`, err); + const texture = PIXI.Texture.WHITE; + spriteData.costumes.push({ name: c.name, texture }); + } + }); + } + + if (Array.isArray(entry.sounds)) { + entry.sounds.forEach(s => { + if (!s?.name || !s?.data) return; + spriteData.sounds.push({ name: s.name, dataURL: s.data }); + + if (!window.projectSounds.includes(s.name)) { + window.projectSounds.push(s.name); + } + }); + } + + const sprite = + spriteData.costumes.length > 0 + ? new PIXI.Sprite(spriteData.costumes[0].texture) + : new PIXI.Sprite(); + + sprite.anchor.set(0.5); + sprite.x = spriteData.data.x; + sprite.y = spriteData.data.y; + sprite.scale.x = spriteData.data.scale.x; + sprite.scale.y = spriteData.data.scale.y; + + if (entry?.data?.angle !== null) sprite.angle = spriteData.data.angle; + else sprite.rotation = spriteData.data.rotation; + + const cc = spriteData.data.currentCostume; + if (typeof cc === "number" && spriteData.costumes[cc]) { + sprite.texture = spriteData.costumes[cc].texture; + } + + spriteData.pixiSprite = sprite; + spriteData.pixiSprite.scale._parentScaleEvent = sprite; + + app.stage.addChild(sprite); + sprites.push(spriteData); + }); + + // Set the active sprite (this loads the workspace) + setActiveSprite(sprites[0] || null); + + // NOW set the backdrop after sprites are loaded + if (typeof data.currentBackdrop === "number" && data.currentBackdrop >= 0) { + setBackdrop(data.currentBackdrop); + } + + renderBackdropsList(); + + // IMPORTANT: Update toolbox AFTER everything is loaded + workspace.updateToolbox(document.getElementById('toolbox')); + + // Force refresh all blocks with dropdowns to show correct values + setTimeout(() => { + workspace.getAllBlocks(false).forEach(block => { + if (block.type === 'switch_backdrop') { + // Trigger the dropdown to refresh + const field = block.getField('BACKDROP_NAME'); + if (field) { + field.forceRerender(); + } + } + }); + }, 100); + + } catch (err) { + console.error("Failed to load project:", err); + window.alert("Something went wrong while loading the project."); + } +} + +document.getElementById("save-button").addEventListener("click", saveProject); + +loadButton.addEventListener("click", () => { + loadInput.click(); +}); +loadInput.addEventListener("change", loadProject); + +document.getElementById("costume-upload").addEventListener("change", e => { + const file = e.target.files[0]; + if (!file || !activeSprite) return; + + const reader = new FileReader(); + reader.onload = () => { + const texture = PIXI.Texture.from(reader.result); + + let baseName = file.name.split(".")[0]; + let uniqueName = baseName; + let counter = 1; + + const nameExists = name => activeSprite.costumes.some(c => c.name === name); + + while (nameExists(uniqueName)) { + counter++; + uniqueName = `${baseName}_${counter}`; + } + + activeSprite.costumes.push({ name: uniqueName, texture }); + + // ADD THESE TWO LINES: + if (!window.projectCostumes.includes(uniqueName)) { + window.projectCostumes.push(uniqueName); + } + workspace.updateToolbox(document.getElementById('toolbox')); + + if (currentSocket && currentRoom) { + currentSocket.emit("projectUpdate", { + roomId: currentRoom, + type: "addCostume", + data: { + spriteId: activeSprite.id, + name: uniqueName, + texture: reader.result, + }, + }); + } + + if (document.getElementById("costumes-tab").classList.contains("active")) { + tabButtons.forEach(button => { + if (button.dataset.tab === "costumes") button.click(); + }); + } + }; + reader.readAsDataURL(file); + e.target.value = ""; +}); + +document.getElementById("sound-upload").addEventListener("change", async e => { + const file = e.target.files[0]; + if (!file || !activeSprite) return; + + const reader = new FileReader(); + reader.onload = async () => { + let dataURL = reader.result; + dataURL = await compressAudio(dataURL); + + if (currentSocket && currentRoom) { + const base64 = dataURL.substring(dataURL.indexOf(",") + 1); + const estimatedBytes = base64.length * 0.75; + if (estimatedBytes >= MAX_HTTP_BUFFER) { + showNotification({ + message: + "❌ This audio file may be too large to upload. Try compressing it to avoid this.", + }); + e.target.value = ""; + return; + } + } + + let baseName = file.name.split(".")[0]; + let uniqueName = baseName; + let counter = 1; + + const nameExists = name => activeSprite.sounds.some(s => s.name === name); + + while (nameExists(uniqueName)) { + counter++; + uniqueName = `${baseName}_${counter}`; + } + + activeSprite.sounds.push({ + name: uniqueName, + dataURL, + }); + + // ADD THESE TWO LINES: + if (!window.projectSounds.includes(uniqueName)) { + window.projectSounds.push(uniqueName); + } + workspace.updateToolbox(document.getElementById('toolbox')); + + if (currentSocket && currentRoom) { + currentSocket.emit("projectUpdate", { + roomId: currentRoom, + type: "addSound", + data: { + spriteId: activeSprite.id, + name: uniqueName, + dataURL, + }, + }); + } + + if (document.getElementById("sounds-tab").classList.contains("active")) { + renderSoundsList(); + } + }; + + reader.readAsDataURL(file); + e.target.value = ""; +}); + +document.getElementById("add-backdrop-button").addEventListener("click", () => { + document.getElementById("backdrop-upload").click(); +}); + +document.getElementById("delete-backdrop-button").addEventListener("click", () => { + if (currentBackdrop !== null) { + deleteBackdrop(currentBackdrop, true); + } +}); + +document.getElementById("backdrop-upload").addEventListener("change", e => { + const file = e.target.files[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = () => { + const baseName = file.name.split(".")[0]; + const index = addBackdrop(baseName, reader.result, true); + setBackdrop(index); + }; + reader.readAsDataURL(file); + e.target.value = ""; +}); + +// Replace the project name event listener with this: +const projectNameInput = document.getElementById("project-name-input"); +if (projectNameInput) { + projectNameInput.addEventListener("input", (e) => { + projectName = e.target.value.trim() || "Untitled Project"; + + // Sync with live share (optional) + if (currentSocket && currentRoom) { + currentSocket.emit("projectUpdate", { + roomId: currentRoom, + type: "setProjectName", + data: projectName, + }); + } + }); + + // Set initial value + projectNameInput.value = projectName; +} + +// ADD THIS HELPER FUNCTION to update the input +function updateProjectNameInput(name) { + projectName = name; + const nameInput = document.getElementById("project-name-input"); + if (nameInput) { + nameInput.value = name; + } +} +window.updateProjectNameInput = updateProjectNameInput; + +window.addEventListener("resize", () => { + resizeCanvas(); +}); + +function isXmlEmpty(input = "") { + input = input.trim(); + return ( + input === '' || + input === "" + ); +} + +window.addEventListener("beforeunload", e => { + if (sprites.some(sprite => !isXmlEmpty(sprite.code))) { + e.preventDefault(); + e.returnValue = ""; + if (currentSocket) currentSocket?.disconnect?.(); + } +}); + +const allowedKeys = new Set([ + "ArrowUp", + "ArrowDown", + "ArrowLeft", + "ArrowRight", + " ", + "Enter", + "Escape", + ..."abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", +]); +window.addEventListener("keydown", e => { + const key = e.key; + if (!allowedKeys.has(key)) return; + + keysPressed[key] = true; + + const specificHandlers = eventRegistry.key.get(key); + if (specificHandlers) { + for (const entry of specificHandlers) { + entry.cb(); + } + } + + const anyHandlers = eventRegistry.key.get("any"); + if (anyHandlers) { + for (const entry of anyHandlers) { + entry.cb(key); + } + } +}); + +window.addEventListener("keyup", e => { + const key = e.key; + if (allowedKeys.has(key)) { + keysPressed[key] = false; + } +}); + +window.addEventListener("blur", () => { + for (const key in keysPressed) { + keysPressed[key] = false; + } +}); + +window.addEventListener("mousedown", e => { + mouseButtonsPressed[e.button] = true; +}); +window.addEventListener("mouseup", e => { + mouseButtonsPressed[e.button] = false; +}); + +SpriteChangeEvents.on("scaleChanged", sprite => { + if (activeSprite?.pixiSprite === sprite) renderSpriteInfo(); +}); + +SpriteChangeEvents.on("positionChanged", sprite => { + if (activeSprite?.pixiSprite === sprite) renderSpriteInfo(); + + const spriteData = sprites.find(s => s?.pixiSprite === sprite); + if (!spriteData) return; + + if (spriteData.currentBubble) { + const { width, height } = spriteData.currentBubble; + const pos = calculateBubblePosition(sprite, width, height); + Object.assign(spriteData.currentBubble, pos); + } + + const { x, y } = sprite; + const [x0, y0] = spriteData.lastPos || [x, y]; + + if (spriteData.penDown) { + penGraphics.lineStyle(spriteData.penSize || 1, spriteData.penColor); + penGraphics.moveTo(x0, y0); + penGraphics.lineTo(x, y); + } + + spriteData.lastPos = [x, y]; +}); + +SpriteChangeEvents.on("textureChanged", event => { + renderSpritesList(false); +}); + +/* setup extensions stuff */ + +export const activeExtensions = []; + +const extensions = [ + { + id: "tween", + name: "Tween", + xml: ` + + + + 100 + + + + + 3 + + + + + + + 0 + + + + + 100 + + + + + 3 + + + + + `, + }, + { + id: "pen", + name: "Pen", + xml: ` + + + + + + 255,100,100 + + + + + 1 + + + `, + }, + { + id: "sets", + name: "Sets", + xml: ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + `, + }, +]; + +const extensionsPopup = document.querySelector(".extensions-popup"); +const extensionsList = document.querySelector(".extensions-list"); + +function addExtensionButton() { + const toolboxDiv = document.querySelector( + "div.blocklyToolbox div.blocklyToolboxCategoryGroup" + ); + if (!toolboxDiv || !extensionsPopup) return; + + const button = document.createElement("button"); + button.innerHTML = ''; + button.id = "extensionButton"; + + ["pointerdown", "mousedown", "mouseup", "click"].forEach(evt => + button.addEventListener(evt, e => { + e.stopPropagation(); + e.preventDefault(); + }) + ); + + button.addEventListener("click", () => { + extensionsPopup.classList.remove("hidden"); + }); + + toolboxDiv.appendChild(button); +} + +function addExtension(id, emit = false) { + if (activeExtensions.includes(id)) return; + + const extension = extensions.find(e => e?.id === id); + if (!extension || !extension.xml) return; + + const parser = new DOMParser(); + const extDoc = parser.parseFromString(extension.xml, "text/xml"); + const coreDom = document.getElementById("toolbox"); + + const category = extDoc.querySelector("category"); + coreDom.appendChild(category.cloneNode(true)); + + workspace.updateToolbox(coreDom); + + activeExtensions.push(id); + document.querySelector(`button[data-extension-id="${id}"]`).disabled = true; + + setTimeout(() => { + extensionsPopup.classList.add("hidden"); + }); + + if (emit && currentSocket && currentRoom); + currentSocket.emit("projectUpdate", { + roomId: currentRoom, + type: "addExtension", + data: id, + }); +} + +setupExtensions(); +addExtensionButton(); + +extensions.forEach(e => { + if (!e || !e.id) return; + + const extension = document.createElement("div"); + const addButton = document.createElement("button"); + addButton.onclick = () => addExtension(e.id, true); + addButton.dataset.extensionId = e.id; + addButton.innerText = "Add"; + extension.innerHTML = `

${e?.name ?? "Extension Name"}

+ `; + extension.appendChild(addButton); + extensionsList.appendChild(extension); +}); + +const stageDiv = document.getElementById("stage-div"); + +fullscreenButton.addEventListener("click", () => { + const isFull = stageDiv.classList.toggle("fullscreen"); + fullscreenButton.innerHTML = ``; + resizeCanvas(); +}); + +document + .getElementById("extensions-custom-button") + .addEventListener("click", () => { + const isSharing = currentSocket && currentRoom; + showPopup({ + title: "Custom Extensions", + rows: [ + [ + "⚠ Warning: Only use custom extensions from people you trust! Do not run custom extensions you don't know about.", + ], + [ + "Insert extension code:", + { + type: "textarea", + placeholder: "class Extension { ... }", + className: "extension-code-input", + }, + ], + [ + { + type: "button", + label: ' Add', + className: "primary", + disabled: isSharing, + onClick: popup => { + const input = popup.querySelector('[data-row="1"][data-col="1"]'); + const userCode = input ? input.value : ""; + + const iframe = document.createElement("iframe"); + iframe.style.display = "none"; + iframe.sandbox = "allow-scripts"; + iframe.srcdoc = ` + + `; + document.body.appendChild(iframe); + + const handleMessage = event => { + if (!event.data) return; + + switch (event.data.type) { + case "registerExtension": + try { + const extensionCode = "(" + event.data.code + ")"; + const ExtensionClass = eval(extensionCode); + registerExtension(ExtensionClass); + + console.log("extension registered:", ExtensionClass); + } catch (error) { + console.error("Error in extension:", error); + window.alert("Error in extension: " + error); + } + + iframe.remove(); + window.removeEventListener("message", handleMessage); + break; + case "error": + console.error("Error in extension:", event.data.error); + window.alert("Error in extension: " + event.data.error); + break; + case "iframeReady": + iframe.contentWindow.postMessage( + { type: "runCode", code: userCode }, + "*" + ); + break; + } + }; + + window.addEventListener("message", handleMessage); + + popup.remove(); + document + .getElementById("extensions-popup") + ?.classList.add("hidden"); + }, + }, + isSharing + ? "You can't add custom extensions while live sharing the project." + : "", + ], + ], + }); + }); + +function getToken() { + return localStorage.getItem("tooken"); +} + +function serializeWorkspace(workspace) { + const xmlDom = Blockly.Xml.workspaceToDom(workspace, true); + return Blockly.Xml.domToText(xmlDom); +} + +function createSession() { + if (currentSocket && currentSocket.connected) return currentSocket; + + currentSocket = io(`${config.apiUrl}/live`); + + currentSocket.on("connect", () => { + console.log("connected to liveshare"); + }); + + currentSocket.on("disconnect", () => { + console.log("disconnected from liveshare"); + + currentSocket = null; + currentRoom = null; + amHost = false; + connectedUsers = []; + + updateUsersList(); + }); + + currentSocket.on("userList", users => { + connectedUsers = users; + updateUsersList(); + }); + + currentSocket.on("userJoined", async ({ username, socketId }) => { + console.log(`${username} joined to room`); + if (amHost) { + currentSocket.emit("sendProjectData", { + to: socketId, + data: await getProject(), + }); + } + updateUsersList(); + }); + + currentSocket.on("projectData", async data => { + console.log("received project data from host"); + await handleProjectData(data); + }); + + currentSocket.on("projectUpdate", ({ type, data }) => { + switch (type) { + case "addVariable": { + projectVariables[data] = 0; + break; + } + case "addSprite": { + addSprite(data, false); + renderSpritesList(true); + break; + } + case "removeSprite": { + deleteSprite(data, false); + renderSpritesList(true); + break; + } + case "addExtension": { + addExtension(data, false); + break; + } + case "addCostume": { + const target = sprites.find(s => s.id === data.spriteId); + if (!target) return; + const texture = PIXI.Texture.from(data.texture); + target.costumes.push({ name: data.name, texture }); + if (activeSprite?.id === target.id) renderCostumesList(); + break; + } + case "addSound": { + const target = sprites.find(s => s.id === data.spriteId); + if (!target) return; + target.sounds.push({ name: data.name, dataURL: data.dataURL }); + if (activeSprite?.id === target.id) renderSoundsList(); + break; + } + case "renameCostume": { + const target = sprites.find(s => s.id === data.spriteId); + if (!target) return; + const costume = target.costumes.find(c => c.name === data.oldName); + if (costume) costume.name = data.newName; + if (activeSprite?.id === target.id) renderCostumesList(); + break; + } + case "deleteCostume": { + const target = sprites.find(s => s.id === data.spriteId); + if (!target) return; + target.costumes = target.costumes.filter(c => c.name !== data.name); + if (activeSprite?.id === target.id) renderCostumesList(); + break; + } + case "renameSound": { + const target = sprites.find(s => s.id === data.spriteId); + if (!target) return; + const sound = target.sounds.find(s => s.name === data.oldName); + if (sound) sound.name = data.newName; + if (activeSprite?.id === target.id) renderSoundsList(); + break; + } + case "deleteSound": { + const target = sprites.find(s => s.id === data.spriteId); + if (!target) return; + target.sounds = target.sounds.filter(s => s.name !== data.name); + if (activeSprite?.id === target.id) renderSoundsList(); + break; + } + // ADD THESE NEW CASES: + case "addBackdrop": { + addBackdrop(data.name, data.texture, false); + break; + } + case "deleteBackdrop": { + deleteBackdrop(data, false); + break; + } + case "setBackdrop": { + if (typeof data === "number") { + setBackdrop(data); + } else if (typeof data === "string") { + setBackdropByName(data); + } + break; + } + case "setProjectName": { + projectName = data; + const nameInput = document.getElementById("project-name-input"); + if (nameInput) nameInput.value = projectName; + break; + } + } + }); + + currentSocket.on("blocklyUpdate", ({ spriteId, event, from }) => { + if (from === currentSocket?.id) return; + + if (!event || typeof event !== "object") { + console.warn("received bad blockly update (skipping):", event); + return; + } + + const sprite = sprites.find(s => s.id === spriteId); + if (!sprite) return; + + let _workspace, + temp = false; + + if (activeSprite.id === spriteId) { + _workspace = workspace; + } else { + temp = true; + _workspace = new Blockly.Workspace({ + readOnly: true, + plugins: { + connectionChecker: "CustomChecker", + }, + }); + + const xml = Blockly.utils.xml.textToDom(sprite.code || ""); + Blockly.Xml.domToWorkspace(xml, _workspace); + } + + Blockly.Events.disable(); + try { + Blockly.Events.fromJson(event, _workspace).run(true); + } catch (err) { + console.error("blockly update error:", err, event); + } finally { + if ( + event.type === Blockly.Events.BLOCK_CHANGE && + event.element === "mutation" + ) { + updateAllFunctionCalls(workspace); + } + + if (temp) { + const newXml = Blockly.Xml.domToText( + Blockly.Xml.workspaceToDom(_workspace) + ); + sprite.code = newXml; + + _workspace.dispose(); + } + + Blockly.Events.enable(); + } + }); + + currentSocket.on("invitesStatus", ({ enabled }) => { + invitesEnabled = enabled; + + const toggleInvites = document.querySelector( + '[data-row="1"][data-col="0"]' + ); + if (toggleInvites) + toggleInvites.textContent = enabled + ? "Disable Invites" + : "Enable Invites"; + + const copyLink = document.querySelector('[data-row="1"][data-col="1"]'); + if (copyLink) copyLink.disabled = !enabled; + }); + + currentSocket.on("kicked", () => { + currentSocket.disconnect(); + showNotification({ message: "You were kicked from the room" }); + }); + + return currentSocket; +} + +function updateUsersList() { + const container = document.getElementById("room-users"); + if (!liveShare) return; + + if (connectedUsers.length === 0) { + liveShare.innerHTML = ` + + Live Share + `; + if (container) container.innerHTML = "No users connected"; + return; + } + + if (!container) return; + + container.innerHTML = connectedUsers + .map(u => { + const canKick = amHost && !u.isHost; + return ` +
+ + ${u.isHost ? "👑 " : ""}${u.username} + ${ + canKick + ? `` + : "" + } +
`; + }) + .join(""); + + liveShare.innerHTML = ` + + Live Share (${connectedUsers.length}) + `; + + if (amHost) { + container.querySelectorAll(".kick-btn").forEach(btn => + btn.addEventListener("click", e => { + const targetUserId = e.target.dataset.id; + if (confirm("Kick this user?")) + currentSocket.emit("kickUser", { roomId: currentRoom, targetUserId }); + }) + ); + } +} + +const liveShare = document.getElementById("liveshare-button"); +liveShare.addEventListener("click", async () => { + let roomExisted = currentSocket !== null && currentRoom !== null; + + function showRoomPopup() { + const shareUrl = + window.location.origin + + window.location.pathname + + `?room=${currentRoom}`; + + const invitesLabel = invitesEnabled ? "Disable Invites" : "Enable Invites"; + const buttons = [ + amHost + ? { + type: "button", + label: invitesLabel, + onClick: () => { + const newStatus = !invitesEnabled; + invitesEnabled = newStatus; + currentSocket.emit("toggleInvites", { + roomId: currentRoom, + enabled: newStatus, + }); + }, + } + : invitesLabel, + { + type: "button", + className: "primary", + label: "Copy Link", + disabled: !invitesEnabled, + onClick: async () => { + try { + await navigator.clipboard.writeText(shareUrl); + showNotification({ message: "Copied room link!" }); + } catch (e) { + console.error("Copy failed", e); + window.alert(shareUrl); + } + }, + }, + { + type: "button", + className: "danger", + label: amHost ? "Close room" : "Leave room", + onClick: popup => { + showNotification({ + message: amHost ? "Room closed" : "Left room", + }); + + popup.remove(); + + currentSocket.disconnect(); + currentSocket = null; + currentRoom = null; + amHost = false; + }, + }, + ]; + + const rows = [ + [ + "Users:", + { + type: "custom", + html: `
`, + }, + ], + buttons, + ]; + + showPopup({ + title: roomExisted ? "Current Room" : "Room Created", + rows, + }); + + updateUsersList(); + } + + createSession(); + + if (!roomExisted) { + const token = getToken(); + if (!token) { + showNotification({ + message: "You must be logged in to create a shared room", + }); + } else { + currentSocket.emit("createRoom", { token }, res => { + if (res?.error) { + console.error(res.error); + showNotification({ message: `Error: ${res.error}` }); + return; + } + amHost = true; + currentRoom = res.roomId; + showRoomPopup(); + }); + } + } else showRoomPopup(); +}); + +const urlParams = new URLSearchParams(window.location.search); +const roomId = urlParams.get("room"); +if (roomId) { + const token = getToken(); + if (!token) { + showNotification({ + message: "You must be logged in to join a shared room", + }); + } else { + createSession(); + + currentSocket.emit("joinRoom", { token, roomId }, res => { + if (res?.error) { + showNotification({ message: `Error: ${res.error}` }); + return; + } + + currentRoom = roomId; + amHost = false; + + console.log(`joined room ${roomId} successfully`); + }); + } +} else { + let spriteData = addSprite(); + setActiveSprite(spriteData); +} + +const ignoredEvents = new Set([ + Blockly.Events.VIEWPORT_CHANGE, + Blockly.Events.SELECTED, + Blockly.Events.CLICK, + Blockly.Events.TOOLBOX_ITEM_SELECT, + Blockly.Events.TRASHCAN_OPEN, + Blockly.Events.FINISHED_LOADING, + Blockly.Events.BLOCK_FIELD_INTERMEDIATE_CHANGE, + Blockly.Events.BLOCK_DRAG, + Blockly.Events.THEME_CHANGE, + Blockly.Events.BUBBLE_OPEN, + "backpack_change", +]); + +function sanitizeEvent(event) { + const raw = event.toJson(); + delete raw.workspaceId; + delete raw.recordUndo; + return JSON.parse(JSON.stringify(raw)); +} + +workspace.addChangeListener(event => { + if (!activeSprite || ignoredEvents.has(event.type)) return; + + activeSprite.code = Blockly.Xml.domToText( + Blockly.Xml.workspaceToDom(workspace) + ); + + if (currentSocket && currentRoom) { + const json = sanitizeEvent(event); + + currentSocket.emit("blocklyUpdate", { + roomId: currentRoom, + spriteId: activeSprite.id, + event: json, + }); + } +}); + +workspace.addChangeListener(Blockly.Events.disableOrphans); + +class TheDragger extends Blockly.dragging.Dragger { + setDraggable(draggable) { + this.draggable = draggable; + } +} + +Blockly.registry.register( + Blockly.registry.Type.BLOCK_DRAGGER, + Blockly.registry.DEFAULT, + TheDragger, + true +); + +function updateAllFunctionCalls(workspace) { + const allBlocks = workspace.getAllBlocks(false); + const defs = allBlocks.filter(b => b.type === "functions_definition"); + const defMap = {}; + defs.forEach(def => (defMap[def.functionId_] = def)); + + const calls = allBlocks.filter(b => b.type === "functions_call"); + + Blockly.Events.disable(); + try { + calls.forEach(callBlock => { + const def = defs.find(d => d.functionId_ === callBlock.functionId_); + if (!def) return; + + def.updateReturnState_(); + callBlock.matchDefinition(def); + }); + } finally { + Blockly.Events.enable(); + } +} + +workspace.addChangeListener(event => { + if (event.isUiEvent || event.isBlank) return; + + const block = workspace.getBlockById(event?.newParentId ?? event?.oldParentId ?? event?.blockId); + + if (!block || block?.getRootBlock()?.type !== "functions_definition") return; + + updateAllFunctionCalls(workspace); +}); + +workspace.updateAllFunctionCalls = () => { + updateAllFunctionCalls(workspace); +}; diff --git a/src/scripts/index.js b/src/scripts/index.js new file mode 100644 index 0000000..8bc08b9 --- /dev/null +++ b/src/scripts/index.js @@ -0,0 +1,4 @@ +import { setupThemeButton, setupUserTag } from "../functions/theme"; + +setupThemeButton(); +setupUserTag(); \ No newline at end of file diff --git a/src/scripts/login.js b/src/scripts/login.js new file mode 100644 index 0000000..37e57be --- /dev/null +++ b/src/scripts/login.js @@ -0,0 +1,59 @@ +import "@fortawesome/fontawesome-free/css/all.min.css"; +import { toggleIcons, toggleTheme } from "../functions/theme"; +import config from "../config"; + +toggleTheme(); +toggleIcons(); + +const usernameInput = document.getElementById("username"); +const passwordInput = document.getElementById("password"); +const loginButton = document.getElementById("login"); + +async function onLoginClick(e) { + e.preventDefault(); + + const username = usernameInput.value.trim(); + const password = passwordInput.value; + + if (!username || !password) { + alert("Please enter both username and password."); + return; + } + + loginButton.disabled = true; + loginButton.dataset.origHtml = loginButton.innerHTML; + loginButton.innerHTML = `...`; + + try { + const response = await fetch(`${config.apiUrl}/users/login`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ username, password }), + }); + + if (!response.ok) { + let errText = `Request failed: ${response.status} ${response.statusText}`; + try { + const errJson = await response.json(); + + errText = errJson.message || JSON.stringify(errJson); + } catch (err) {} + throw new Error(errText); + } + + const data = await response.json(); + if (data.token) localStorage.setItem("tooken", data.token); + + window.location.href = "/"; + } catch (err) { + console.error(err); + alert("Login error: " + err.message); + + loginButton.disabled = false; + loginButton.innerHTML = loginButton.dataset.origHtml; + } +} + +loginButton.addEventListener("click", onLoginClick); diff --git a/src/scripts/signup.js b/src/scripts/signup.js new file mode 100644 index 0000000..8351b3a --- /dev/null +++ b/src/scripts/signup.js @@ -0,0 +1,59 @@ +import "@fortawesome/fontawesome-free/css/all.min.css"; +import { toggleIcons, toggleTheme } from "../functions/theme"; +import config from "../config"; + +toggleTheme(); +toggleIcons(); + +const usernameInput = document.getElementById("username"); +const passwordInput = document.getElementById("password"); +const loginButton = document.getElementById("login"); + +async function onSignupClick(e) { + e.preventDefault(); + + const username = usernameInput.value.trim(); + const password = passwordInput.value; + + if (!username || !password) { + alert("Please enter both username and password."); + return; + } + + loginButton.disabled = true; + loginButton.dataset.origHtml = loginButton.innerHTML; + loginButton.innerHTML = `...`; + + try { + const response = await fetch(`${config.apiUrl}/users/create`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ username, password }), + }); + + if (!response.ok) { + let errText = `Request failed: ${response.status} ${response.statusText}`; + try { + const errJson = await response.json(); + + errText = errJson.message || JSON.stringify(errJson); + } catch (err) {} + throw new Error(errText); + } + + const data = await response.json(); + if (data.token) localStorage.setItem("tooken", data.token); + + window.location.href = "/"; + } catch (err) { + console.error(err); + alert("Login error: " + err.message); + + loginButton.disabled = false; + loginButton.innerHTML = loginButton.dataset.origHtml; + } +} + +loginButton.addEventListener("click", onSignupClick); diff --git a/src/scripts/testExt.js b/src/scripts/testExt.js new file mode 100644 index 0000000..060047d --- /dev/null +++ b/src/scripts/testExt.js @@ -0,0 +1,128 @@ +class Extension { + id = "ddeTestExtension"; + registerCategory() { + return { + name: "Test Extension", + color: "#858585", + }; + } + registerBlocks() { + return [ + { + type: "statement", + id: "evil", + text: "evil block", + color: "#FF0000", + }, + { + type: "cap", + id: "statement", + fields: { poop: { kind: "statement" } }, + text: "i want statement [poop]", + }, + { + type: "statement", + id: "statementA", + text: "type statement A", + statementType: "statementA", + color: "#85c25c", + }, + { + type: "statement", + id: "onlyStatementA", + fields: { code: { kind: "statement", accepts: "statementA" } }, + text: "only statement A [code]", + color: "#69974a", + }, + { + type: "statement", + id: "if", + fields: { + bool: { kind: "value", type: "Boolean", default: true }, + code: { kind: "statement" }, + }, + text: "if [bool] then [code]", + }, + { + type: "statement", + id: "ifElse", + fields: { + bool: { kind: "value", type: "Boolean", default: true }, + code: { kind: "statement" }, + codeElse: { kind: "statement" }, + }, + text: "if [bool] then [code] else [codeElse]", + }, + { + type: "statement", + id: "menu", + fields: { + hi: { + kind: "menu", + items: ["normal", { text: "ABC display", value: "abc" }], + default: "abc", + }, + }, + text: "menu [hi]", + }, + { + type: "output", + id: "random1", + text: "random (output shape 1)", + outputShape: 1, + }, + { + type: "output", + id: "random2", + text: "random (output shape 2)", + outputShape: 2, + }, + { + type: "output", + id: "random3", + text: "random (output shape 3)", + outputShape: 3, + }, + { + type: "output", + id: "random4", + text: "random (output shape 4)", + outputShape: 4, + }, + { + type: "output", + id: "random5", + text: "random (output shape 5)", + outputShape: 5, + }, + ]; + } + registerCode() { + return { + statement: (inputs) => { + console.log(inputs.poop?.()); + }, + if: (inputs) => { + console.log(inputs); + if (inputs.bool) inputs.code?.(); + }, + ifElse: (inputs) => { + console.log(inputs); + if (inputs.bool) inputs.code?.(); + else inputs.codeElse?.(); + }, + evil: () => { + console.warn("evil is near"); + }, + random1: () => Math.random(), + random2: () => Math.random(), + random3: () => Math.random(), + random4: () => Math.random(), + random5: () => Math.random(), + actuallyBoolean: () => true, + menu: (inputs) => window.alert(inputs.hi), + }; + } +} + +registerExtension(Extension); diff --git a/src/scripts/userprofile.js b/src/scripts/userprofile.js new file mode 100644 index 0000000..31ec2e9 --- /dev/null +++ b/src/scripts/userprofile.js @@ -0,0 +1,129 @@ +import config from "../config"; +import { cache } from "../cache"; +import { showPopup } from "../functions/utils"; +import { setupThemeButton, setupUserTag } from "../functions/theme"; + +const allowedFileFormats = ["jpeg", "png", "webp", "avif", "gif", "tiff"]; +const profileDiv = document.getElementById("userProfile"); + +const urlParams = new URLSearchParams(window.location.search); +const identifier = urlParams.get("id") || urlParams.get("username"); + +setupThemeButton(); +setupUserTag(); + +if (profileDiv && identifier) { + fetch(`${config.apiUrl}/users/${identifier}`, { + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => response.json()) + .then((response) => { + profileDiv.innerHTML = ` + + ${response.username} + `; + + if (cache.user && cache.user.id === response.id) { + const avatarEl = document.getElementById("profileAvatar"); + avatarEl.style.cursor = "pointer"; + avatarEl.addEventListener("click", async () => { + showPopup({ + title: "Avatar", + rows: [ + [ + { + type: "button", + label: "Upload avatar", + className: "primary", + onClick: async (popup) => { + const input = document.createElement("input"); + input.type = "file"; + input.accept = allowedFileFormats + .map((f) => `image/${f}`) + .join(","); + input.style.display = "none"; + input.click(); + + input.onchange = async () => { + const file = input.files[0]; + if (!file) return; + + const format = file.type.split("/")[1]; + if (!allowedFileFormats.includes(format)) { + alert(`Unsupported format: ${format}`); + return; + } + + const formData = new FormData(); + formData.append("avatar", file); + + try { + const res = await fetch( + `${config.apiUrl}/users/me/avatar`, + { + method: "POST", + headers: { + Authorization: localStorage.getItem("tooken"), + }, + body: formData, + } + ); + + const data = await res.json(); + if (!res.ok) + throw new Error(data.error || "Upload failed"); + + window.location.reload(); + } catch (err) { + console.error(err); + alert("Error uploading: " + err.message); + } finally { + popup.remove(); + input.remove(); + } + }; + }, + }, + { + type: "button", + label: "Remove avatar", + className: "danger", + onClick: async (popup) => { + if (!confirm("Remove your avatar?")) return; + try { + const res = await fetch( + `${config.apiUrl}/users/me/avatar`, + { + method: "DELETE", + headers: { + Authorization: localStorage.getItem("tooken"), + }, + } + ); + const data = await res.json(); + if (!res.ok) + throw new Error( + data.error || "Failed to remove avatar" + ); + + avatarEl.src = "default-avatar.png"; + popup.remove(); + } catch (err) { + console.error(err); + alert("Error deleting: " + err.message); + } + }, + }, + ], + ], + }); + }); + } + }) + .catch((err) => { + console.error(err); + alert("Login error: " + err.message); + }); +} diff --git a/src/userprofile.css b/src/userprofile.css new file mode 100644 index 0000000..2e85537 --- /dev/null +++ b/src/userprofile.css @@ -0,0 +1,27 @@ +.info { + padding: 1rem; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; +} + +#userProfile { + display: flex; + align-items: center; + justify-content: center; + text-align: center; + font-size: x-large; + gap: 0.5rem; + padding: 0.5rem; + border-radius: 0.5rem; + background-color: var(--color2); + color: var(--dark); +} + +#userProfile img { + width: 3rem; + height: 3rem; + border-radius: 20%; +} diff --git a/user.html b/user.html new file mode 100644 index 0000000..1083e31 --- /dev/null +++ b/user.html @@ -0,0 +1,61 @@ + + + + + + + User - NeoIDE + + + + + + + + + + + + + + + + + + +
+
+ + + +
+
+ +
+
+ +
+
+
+ + + + + + + \ No newline at end of file diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000..93b624b --- /dev/null +++ b/vercel.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://openapi.vercel.sh/vercel.json", + "cleanUrls": true +} diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..79d636c --- /dev/null +++ b/vite.config.js @@ -0,0 +1,17 @@ +import { defineConfig } from 'vite'; +import { resolve } from 'path'; + +export default defineConfig({ + build: { + rollupOptions: { + input: { + main: resolve(__dirname, 'index.html'), + editor: resolve(__dirname, 'editor.html'), + login: resolve(__dirname, 'login.html'), + signup: resolve(__dirname, 'signup.html'), + user: resolve(__dirname, 'user.html'), + }, + treeshake: false, + }, + }, +});