diff --git a/bjsEditorPlugin/.gitignore b/bjsEditorPlugin/.gitignore new file mode 100644 index 0000000..3c25e1e --- /dev/null +++ b/bjsEditorPlugin/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +*.log diff --git a/bjsEditorPlugin/README.md b/bjsEditorPlugin/README.md new file mode 100644 index 0000000..b4eef6f --- /dev/null +++ b/bjsEditorPlugin/README.md @@ -0,0 +1,50 @@ +# Space Game BabylonJS Editor Plugin + +Export BabylonJS Editor scenes to Space Game LevelConfig format. + +## Installation + +1. Build the plugin: + ```bash + cd bjsEditorPlugin + npm install + npm run build + ``` + +2. Install in BabylonJS Editor: + - Open BabylonJS Editor + - Edit → Preferences → Plugins + - Click "Add" and select this folder + - Apply and restart Editor + +## Usage + +1. Create a workspace in BabylonJS Editor +2. Copy the script components from `editorScripts/` to your workspace's `src/scenes/` folder +3. Place meshes in your scene and attach the appropriate scripts: + - `AsteroidComponent` - for asteroids + - `ShipComponent` - for player spawn point + - `SunComponent` - for the sun + - `PlanetComponent` - for planets + - `TargetComponent` - for orbit/movement targets +4. Configure properties in the Inspector panel +5. Space Game → Export Level Config... + +## Script Components + +The `editorScripts/` folder contains TypeScript components to use in your Editor workspace. +These expose game-specific properties (velocities, targets, etc.) in the Inspector. + +## Plugin Menu + +- **Export Level Config...** - Downloads level.json file +- **Export to Clipboard** - Copies JSON to clipboard + +## Development + +```bash +npm run watch # Watch mode for development +npm run build # Production build +``` + +Debug in Editor: CTRL+ALT+i to open DevTools, F5 to reload plugin. diff --git a/bjsEditorPlugin/editorScripts/AsteroidComponent.ts b/bjsEditorPlugin/editorScripts/AsteroidComponent.ts new file mode 100644 index 0000000..bf33a39 --- /dev/null +++ b/bjsEditorPlugin/editorScripts/AsteroidComponent.ts @@ -0,0 +1,30 @@ +/** + * BabylonJS Editor script component for asteroids + * Copy this to your Editor workspace: src/scenes/scripts/AsteroidComponent.ts + * + * Attach to asteroid meshes to expose game properties in Inspector. + */ +import { Mesh } from "@babylonjs/core/Meshes/mesh"; + +import { + visibleAsNumber, + visibleAsString, + visibleAsVector3, +} from "babylonjs-editor-tools"; + +export default class AsteroidComponent extends Mesh { + @visibleAsVector3("Linear Velocity", { step: 0.1 }) + public linearVelocity = { x: 0, y: 0, z: 0 }; + + @visibleAsVector3("Angular Velocity", { step: 0.01 }) + public angularVelocity = { x: 0, y: 0, z: 0 }; + + @visibleAsNumber("Mass", { min: 1, max: 1000, step: 10 }) + public mass: number = 200; + + @visibleAsString("Target ID", { description: "Reference to a TargetComponent node" }) + public targetId: string = ""; + + @visibleAsString("Target Mode", { description: "orbit | moveToward | (empty)" }) + public targetMode: string = ""; +} diff --git a/bjsEditorPlugin/editorScripts/BaseComponent.ts b/bjsEditorPlugin/editorScripts/BaseComponent.ts new file mode 100644 index 0000000..f6bc26a --- /dev/null +++ b/bjsEditorPlugin/editorScripts/BaseComponent.ts @@ -0,0 +1,17 @@ +/** + * BabylonJS Editor script component for the start base + * Copy this to your Editor workspace: src/scenes/scripts/BaseComponent.ts + * + * Attach to a mesh to mark it as the start base (yellow cylinder constraint zone). + */ +import { Mesh } from "@babylonjs/core/Meshes/mesh"; + +import { visibleAsString } from "babylonjs-editor-tools"; + +export default class BaseComponent extends Mesh { + @visibleAsString("Base GLB Path", { description: "Path to base GLB model" }) + public baseGlbPath: string = "base.glb"; + + @visibleAsString("Landing GLB Path", { description: "Path to landing zone GLB" }) + public landingGlbPath: string = ""; +} diff --git a/bjsEditorPlugin/editorScripts/PlanetComponent.ts b/bjsEditorPlugin/editorScripts/PlanetComponent.ts new file mode 100644 index 0000000..2a3109f --- /dev/null +++ b/bjsEditorPlugin/editorScripts/PlanetComponent.ts @@ -0,0 +1,17 @@ +/** + * BabylonJS Editor script component for planets + * Copy this to your Editor workspace: src/scenes/scripts/PlanetComponent.ts + * + * Attach to a mesh to configure planet properties. + */ +import { Mesh } from "@babylonjs/core/Meshes/mesh"; + +import { visibleAsNumber, visibleAsString } from "babylonjs-editor-tools"; + +export default class PlanetComponent extends Mesh { + @visibleAsNumber("Diameter", { min: 10, max: 1000, step: 10 }) + public diameter: number = 100; + + @visibleAsString("Texture Path", { description: "Path to planet texture" }) + public texturePath: string = ""; +} diff --git a/bjsEditorPlugin/editorScripts/ShipComponent.ts b/bjsEditorPlugin/editorScripts/ShipComponent.ts new file mode 100644 index 0000000..061df13 --- /dev/null +++ b/bjsEditorPlugin/editorScripts/ShipComponent.ts @@ -0,0 +1,17 @@ +/** + * BabylonJS Editor script component for player ship spawn + * Copy this to your Editor workspace: src/scenes/scripts/ShipComponent.ts + * + * Attach to a mesh/transform node to mark player spawn point. + */ +import { Mesh } from "@babylonjs/core/Meshes/mesh"; + +import { visibleAsVector3 } from "babylonjs-editor-tools"; + +export default class ShipComponent extends Mesh { + @visibleAsVector3("Start Velocity", { step: 0.1 }) + public linearVelocity = { x: 0, y: 0, z: 0 }; + + @visibleAsVector3("Start Angular Vel", { step: 0.01 }) + public angularVelocity = { x: 0, y: 0, z: 0 }; +} diff --git a/bjsEditorPlugin/editorScripts/SunComponent.ts b/bjsEditorPlugin/editorScripts/SunComponent.ts new file mode 100644 index 0000000..895a4f1 --- /dev/null +++ b/bjsEditorPlugin/editorScripts/SunComponent.ts @@ -0,0 +1,17 @@ +/** + * BabylonJS Editor script component for the sun + * Copy this to your Editor workspace: src/scenes/scripts/SunComponent.ts + * + * Attach to a mesh to mark it as the sun. Position from transform. + */ +import { Mesh } from "@babylonjs/core/Meshes/mesh"; + +import { visibleAsNumber } from "babylonjs-editor-tools"; + +export default class SunComponent extends Mesh { + @visibleAsNumber("Diameter", { min: 10, max: 200, step: 5 }) + public diameter: number = 50; + + @visibleAsNumber("Intensity", { min: 0, max: 5000000, step: 100000 }) + public intensity: number = 1000000; +} diff --git a/bjsEditorPlugin/editorScripts/TargetComponent.ts b/bjsEditorPlugin/editorScripts/TargetComponent.ts new file mode 100644 index 0000000..bf60d2f --- /dev/null +++ b/bjsEditorPlugin/editorScripts/TargetComponent.ts @@ -0,0 +1,15 @@ +/** + * BabylonJS Editor script component for orbit/movement targets + * Copy this to your Editor workspace: src/scenes/scripts/TargetComponent.ts + * + * Attach to a TransformNode to create an invisible target point. + * Asteroids can reference this by targetId to orbit or move toward. + */ +import { TransformNode } from "@babylonjs/core/Meshes/transformNode"; + +import { visibleAsString } from "babylonjs-editor-tools"; + +export default class TargetComponent extends TransformNode { + @visibleAsString("Display Name", { description: "Friendly name for this target" }) + public displayName: string = "Target"; +} diff --git a/bjsEditorPlugin/package-lock.json b/bjsEditorPlugin/package-lock.json new file mode 100644 index 0000000..aeeaf7e --- /dev/null +++ b/bjsEditorPlugin/package-lock.json @@ -0,0 +1,1272 @@ +{ + "name": "babylonjs-editor-space-game-plugin", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "babylonjs-editor-space-game-plugin", + "version": "1.0.0", + "dependencies": { + "@auth0/auth0-spa-js": "^2.1.3", + "@supabase/supabase-js": "^2.45.0" + }, + "devDependencies": { + "@babylonjs/core": "^6.0.0", + "typescript": "^5.0.0" + }, + "peerDependencies": { + "babylonjs-editor": "^4.0.0" + } + }, + "node_modules/@auth0/auth0-spa-js": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@auth0/auth0-spa-js/-/auth0-spa-js-2.10.0.tgz", + "integrity": "sha512-eQhtxp19foKD7csTUariaU7YgwElVAmSJQSk2USuaP1LCqzN/iWhQS/vtVYiSozvSZPv8IOwN5UkBUt+rJAg8w==", + "license": "MIT", + "dependencies": { + "browser-tabs-lock": "^1.2.15", + "dpop": "^2.1.1", + "es-cookie": "~1.3.2" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babylonjs/core": { + "version": "6.49.0", + "resolved": "https://registry.npmjs.org/@babylonjs/core/-/core-6.49.0.tgz", + "integrity": "sha512-jU/JyqebRqqziNwHLcBYzANrVRd9S55yNZEjejwg2p4I8NRnoBBNgf4wuUVw17UKNHc1v3KD/Vnr5C2+dIWAqQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@blueprintjs/core": { + "version": "3.28.1", + "resolved": "https://registry.npmjs.org/@blueprintjs/core/-/core-3.28.1.tgz", + "integrity": "sha512-Ws4+FrtHh5U4TMaqhFW1PXajJobn2By72LOxu18q8ksY4CuyUzvo8xIOooViQdA7ZLx9pwhC6dytsYdykwRy0w==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@blueprintjs/icons": "^3.18.0", + "@types/dom4": "^2.0.1", + "classnames": "^2.2", + "dom4": "^2.1.5", + "normalize.css": "^8.0.1", + "popper.js": "^1.15.0", + "react-lifecycles-compat": "^3.0.4", + "react-popper": "^1.3.7", + "react-transition-group": "^2.9.0", + "resize-observer-polyfill": "^1.5.1", + "tslib": "~1.10.0" + }, + "bin": { + "upgrade-blueprint-2.0.0-rename": "scripts/upgrade-blueprint-2.0.0-rename.sh", + "upgrade-blueprint-3.0.0-rename": "scripts/upgrade-blueprint-3.0.0-rename.sh" + }, + "peerDependencies": { + "react": "^15.3.0 || 16", + "react-dom": "^15.3.0 || 16" + } + }, + "node_modules/@blueprintjs/icons": { + "version": "3.33.0", + "resolved": "https://registry.npmjs.org/@blueprintjs/icons/-/icons-3.33.0.tgz", + "integrity": "sha512-Q6qoSDIm0kRYQZISm59UUcDCpV3oeHulkLuh3bSlw0HhcSjvEQh2PSYbtaifM60Q4aK4PCd6bwJHg7lvF1x5fQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "classnames": "^2.2", + "tslib": "~2.3.1" + } + }, + "node_modules/@blueprintjs/icons/node_modules/tslib": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", + "license": "0BSD", + "peer": true + }, + "node_modules/@blueprintjs/select": { + "version": "3.13.2", + "resolved": "https://registry.npmjs.org/@blueprintjs/select/-/select-3.13.2.tgz", + "integrity": "sha512-cx/pUqAbbGJpBQgmdc9Cwj8CKbDbgq6dxcG5qTXs3nXNZQRO5KtwaKSJEzSxO9PhmxxpEn6FrScukoevqHwHvw==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@blueprintjs/core": "^3.28.1", + "classnames": "^2.2", + "tslib": "~1.10.0" + }, + "peerDependencies": { + "react": "^15.3.0 || 16", + "react-dom": "^15.3.0 || 16" + } + }, + "node_modules/@hypnosphi/create-react-context": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@hypnosphi/create-react-context/-/create-react-context-0.3.1.tgz", + "integrity": "sha512-V1klUed202XahrWJLLOT3EXNeCpFHCcJntdFGI15ntCwau+jfT386w7OFTMaCqOgXUH1fa0w/I1oZs+i/Rfr0A==", + "license": "MIT", + "peer": true, + "dependencies": { + "gud": "^1.0.0", + "warning": "^4.0.3" + }, + "peerDependencies": { + "prop-types": "^15.0.0", + "react": ">=0.14.0" + } + }, + "node_modules/@supabase/auth-js": { + "version": "2.86.2", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.86.2.tgz", + "integrity": "sha512-7k8IAhgSnZuD9Zex2+ohHKY3aWGDd4ls0xlxMGl3/jPyHSSXrIYfmtJyUH0+DPd4B3psBqHC0Ev0/nZEHdW58w==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/auth-js/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@supabase/functions-js": { + "version": "2.86.2", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.86.2.tgz", + "integrity": "sha512-OLpy3NIlj7q3yGMFwUpPkDPJbRx4aU+u73SiXqiMnA5ARwzVcOReSzI2u4oOqioE+3ud0fRx7sRsfoklBwYOmg==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/functions-js/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@supabase/postgrest-js": { + "version": "2.86.2", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.86.2.tgz", + "integrity": "sha512-KVgOF2QASvUfQnzMGAmxR7f3ZF/eZ8PFp2F5Q7SAPQlmB83FEaZ7C/QMzfVXXqkMbotfh96xcaBNSKnxowFObA==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/postgrest-js/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@supabase/realtime-js": { + "version": "2.86.2", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.86.2.tgz", + "integrity": "sha512-uLUYrOMeK1qXHISxdMFVfBs0sGV5PmqYewIHvLBnMYbb//LERojxfKlVSJBgZ+aAwxANmtQKcprjGZI7DJ6lNQ==", + "license": "MIT", + "dependencies": { + "@types/phoenix": "^1.6.6", + "@types/ws": "^8.18.1", + "tslib": "2.8.1", + "ws": "^8.18.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/realtime-js/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@supabase/storage-js": { + "version": "2.86.2", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.86.2.tgz", + "integrity": "sha512-zyR4PkO7R4f4/xRBVJho3Dm7y4512BoCqGmD7LjNV2GVtWt8vEmambiuMB2Ty3l76mqw+ynQyHY8yFWSERrHXA==", + "license": "MIT", + "dependencies": { + "iceberg-js": "^0.8.0", + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/storage-js/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@supabase/supabase-js": { + "version": "2.86.2", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.86.2.tgz", + "integrity": "sha512-KXoiqFf7zZhL/+lj7oBFFUvVDQ6gy03v9wQ5E++f7xiJUuqmI4DuBhrv8uFo6B2EGTQTA3vkXjbxmYIug/zfWw==", + "license": "MIT", + "dependencies": { + "@supabase/auth-js": "2.86.2", + "@supabase/functions-js": "2.86.2", + "@supabase/postgrest-js": "2.86.2", + "@supabase/realtime-js": "2.86.2", + "@supabase/storage-js": "2.86.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@types/dat.gui": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@types/dat.gui/-/dat.gui-0.7.5.tgz", + "integrity": "sha512-5AqLThlTiuDSOZA7XZFogRj/UdGKn/iIfdFPuh37kY4s7TjTt+YUOlUmcCrY6wAYFFyThtt2z8qZlYcdkhJZ5w==", + "license": "MIT", + "peer": true + }, + "node_modules/@types/dom4": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/dom4/-/dom4-2.0.4.tgz", + "integrity": "sha512-PD+wqNhrjWFjAlSVd18jvChZvOXB2SOwAILBmuYev5zswBats5qmzs/QFoooLKd2omj9BT05a8MeSeRmXLGY+Q==", + "license": "MIT", + "peer": true + }, + "node_modules/@types/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-UoOfVEzAUpeSPmjm7h1uk5MH6KZma2z2O7a75onTGjnNvAvMVrPzPL/vBbT65iIGHWj6rokwfmYcmxmlSf2uwg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "13.9.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-13.9.0.tgz", + "integrity": "sha512-0ARSQootUG1RljH2HncpsY2TJBfGQIKOOi7kxzUY6z54ePu/ZD+wJA8zI2Q6v8rol2qpG/rvqsReco8zNMPvhQ==", + "license": "MIT" + }, + "node_modules/@types/phoenix": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz", + "integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==", + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "license": "MIT", + "peer": true + }, + "node_modules/@types/react": { + "version": "16.9.19", + "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.19.tgz", + "integrity": "sha512-LJV97//H+zqKWMms0kvxaKYJDG05U2TtQB3chRLF8MPNs+MQh/H1aGlyDUxjaHvu08EAGerdX2z4LTBc7ns77A==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/prop-types": "*", + "csstype": "^2.2.0" + } + }, + "node_modules/@types/react-dom": { + "version": "16.9.5", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.5.tgz", + "integrity": "sha512-BX6RQ8s9D+2/gDhxrj8OW+YD4R+8hj7FEM/OJHGNR0KipE1h1mSsf39YeyC81qafkq+N3rU3h3RFbLSwE5VqUg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/babylonjs": { + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/babylonjs/-/babylonjs-5.19.0.tgz", + "integrity": "sha512-ZcJ1l7AyTRLWcJ0x5cmjzlBkqm3guDf24INNXHRNjDHv+/vwUGwBh9wbFAhmeQ6NfPCgrQPw2XlbA6xsfHR0lg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "peer": true + }, + "node_modules/babylonjs-editor": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/babylonjs-editor/-/babylonjs-editor-4.5.0.tgz", + "integrity": "sha512-EdGqvrD1PIW0yzXrbD2e8TiBFDZ2+pXga3572ks1iSiO5OckcG+jrao7O4p/YFBkZIiiTQnvK9MVk6Qtc3mh6A==", + "license": "(Apache-2.0)", + "peer": true, + "dependencies": { + "@blueprintjs/core": "3.28.1", + "@blueprintjs/select": "3.13.2", + "@types/dat.gui": "0.7.5", + "@types/fs-extra": "8.1.0", + "@types/node": "13.9.0", + "@types/react": "16.9.19", + "@types/react-dom": "16.9.5", + "babylonjs": "5.19.0", + "babylonjs-gui": "5.19.0", + "babylonjs-inspector": "5.19.0", + "babylonjs-loaders": "5.19.0", + "babylonjs-materials": "5.19.0", + "babylonjs-node-editor": "5.19.0", + "babylonjs-post-process": "5.19.0", + "babylonjs-procedural-textures": "5.19.0", + "babylonjs-serializers": "5.19.0", + "filenamify": "4.2.0", + "react": "16.12.0", + "react-dom": "16.12.0", + "typescript": "4.6.3" + } + }, + "node_modules/babylonjs-editor/node_modules/typescript": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.3.tgz", + "integrity": "sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==", + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/babylonjs-gltf2interface": { + "version": "5.57.1", + "resolved": "https://registry.npmjs.org/babylonjs-gltf2interface/-/babylonjs-gltf2interface-5.57.1.tgz", + "integrity": "sha512-RZnaKfJ6Q/AYLdIjBYMRxCW/HPEC8jabAL1U8wJ0KVziw6NSbSV6S80S22fUCPTyaZ7nCekn1TYg1IPmJ/eA6w==", + "license": "Apache-2.0", + "peer": true + }, + "node_modules/babylonjs-gui": { + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/babylonjs-gui/-/babylonjs-gui-5.19.0.tgz", + "integrity": "sha512-4YMgj45lRdl/l5plz3m8VeL1EuW7z3CZBoOpPSG/MMVkXmr2NDBUd9ssb+YAv8k3LUxkM1dfeTPYe5tW66xgWw==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "babylonjs": "^5.19.0" + } + }, + "node_modules/babylonjs-gui-editor": { + "version": "5.57.1", + "resolved": "https://registry.npmjs.org/babylonjs-gui-editor/-/babylonjs-gui-editor-5.57.1.tgz", + "integrity": "sha512-OF1whNntqqIGo1BPDTB39tNm1eHCXaXaQ8y0pw27PRstOfbVYNo9VWIl6sBCnDE1LIGYA6qBtXLo0qq+vmlUqw==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "babylonjs": "^5.57.1", + "babylonjs-gui": "^5.57.1" + } + }, + "node_modules/babylonjs-gui-editor/node_modules/babylonjs": { + "version": "5.57.1", + "resolved": "https://registry.npmjs.org/babylonjs/-/babylonjs-5.57.1.tgz", + "integrity": "sha512-X1t3mi8GuJjFVziN1yBJtekphilGN9VfOHm2tn/H6gra+WS7UZkrOOHLlKwYEXKdU73opxOR95jHXmv692KR6g==", + "hasInstallScript": true, + "license": "Apache-2.0", + "peer": true + }, + "node_modules/babylonjs-gui-editor/node_modules/babylonjs-gui": { + "version": "5.57.1", + "resolved": "https://registry.npmjs.org/babylonjs-gui/-/babylonjs-gui-5.57.1.tgz", + "integrity": "sha512-gOfRlC+XAyPqEF16Agd7W7g+j+9aLbJJ34/jiUgFLTFcAHZGHnh+kYX9+fRfjb8v0nRm633p8al0s8nUFoXl3A==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "babylonjs": "^5.57.1" + } + }, + "node_modules/babylonjs-inspector": { + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/babylonjs-inspector/-/babylonjs-inspector-5.19.0.tgz", + "integrity": "sha512-k4UcRqUmmTvJHDA7ur5j4eAhDc7i2N368qUJ+2ccomgK42OPEijalHRHCKGMeM7i6xD3O4ZB0p42+/OxkUlFgQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "babylonjs": "^5.19.0", + "babylonjs-gui": "^5.19.0", + "babylonjs-gui-editor": "^5.19.0", + "babylonjs-loaders": "^5.19.0", + "babylonjs-materials": "^5.19.0", + "babylonjs-serializers": "^5.19.0" + } + }, + "node_modules/babylonjs-loaders": { + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/babylonjs-loaders/-/babylonjs-loaders-5.19.0.tgz", + "integrity": "sha512-9/lFhsLTy4ASSaGx8TqmkTz/RBQrXVkbEMcQTqRHNiKWdSpddUiyZNb0xNGPCPLE6pet3YE9bcCcRV0zbfPRXA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "babylonjs": "^5.19.0", + "babylonjs-gltf2interface": "^5.19.0" + } + }, + "node_modules/babylonjs-materials": { + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/babylonjs-materials/-/babylonjs-materials-5.19.0.tgz", + "integrity": "sha512-t+3uLygCtP5PTVnFzEcH+uyq8JWHmQd3DtDcA0pRB3SoxervVRMe8O5P2v8CeQkSeQMEX3RqpWw3ocPoA+THBA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "babylonjs": "^5.19.0" + } + }, + "node_modules/babylonjs-node-editor": { + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/babylonjs-node-editor/-/babylonjs-node-editor-5.19.0.tgz", + "integrity": "sha512-X0Qe0fMvE1bWxTVrlYLdhtpFkKiE24RWExRDdkUNTsO82PS0VFdbHU8uLBw2VK6xZglRzTe3qeMvM01Fpr3/Og==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "babylonjs": "^5.19.0" + } + }, + "node_modules/babylonjs-post-process": { + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/babylonjs-post-process/-/babylonjs-post-process-5.19.0.tgz", + "integrity": "sha512-zrNvOWH9bXehEokEtKGHrdsokpYG32eP4RqnagU7TgCyXONDOqjvEMQBNiYoS3+ER1yPbVij/DX0f4UQrzVB3Q==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "babylonjs": "^5.19.0" + } + }, + "node_modules/babylonjs-procedural-textures": { + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/babylonjs-procedural-textures/-/babylonjs-procedural-textures-5.19.0.tgz", + "integrity": "sha512-PKOXdIg+sW2U0gPjcP2b6IqAyzpVpvxxU1y8Vshs6w8OaEjt/ewgNt+dKEwYBB+DVBYkC/SRAi7ofmBKkjFKsg==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "babylonjs": "^5.19.0" + } + }, + "node_modules/babylonjs-serializers": { + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/babylonjs-serializers/-/babylonjs-serializers-5.19.0.tgz", + "integrity": "sha512-ywHAAA07zODjXHFf4N1wPChOQMK7AeefbickLmDus0fbhiw7Q5SeTEzwjnZBSZRyBlwJL5eeCA8/i30if1R4jA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "babylonjs": "^5.19.0", + "babylonjs-gltf2interface": "^5.19.0" + } + }, + "node_modules/browser-tabs-lock": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/browser-tabs-lock/-/browser-tabs-lock-1.3.0.tgz", + "integrity": "sha512-g6nHaobTiT0eMZ7jh16YpD2kcjAp+PInbiVq3M1x6KKaEIVhT4v9oURNIpZLOZ3LQbQ3XYfNhMAb/9hzNLIWrw==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "lodash": ">=4.17.21" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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", + "peer": true, + "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", + "peer": true, + "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/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT", + "peer": true + }, + "node_modules/csstype": { + "version": "2.6.21", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.21.tgz", + "integrity": "sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==", + "license": "MIT", + "peer": true + }, + "node_modules/deep-equal": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.2.tgz", + "integrity": "sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==", + "license": "MIT", + "peer": true, + "dependencies": { + "is-arguments": "^1.1.1", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.5.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "peer": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "license": "MIT", + "peer": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/dom-helpers": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz", + "integrity": "sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/runtime": "^7.1.2" + } + }, + "node_modules/dom4": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/dom4/-/dom4-2.1.6.tgz", + "integrity": "sha512-JkCVGnN4ofKGbjf5Uvc8mmxaATIErKQKSgACdBXpsQ3fY6DlIpAyWfiBSrGkttATssbDCp3psiAKWXk5gmjycA==", + "peer": true + }, + "node_modules/dpop": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/dpop/-/dpop-2.1.1.tgz", + "integrity": "sha512-J0Of2JTiM4h5si0tlbPQ/lkqfZ5wAEVkKYBhkwyyANnPJfWH4VsR5uIkZ+T+OSPIwDYUg1fbd5Mmodd25HjY1w==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "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", + "peer": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-cookie": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/es-cookie/-/es-cookie-1.3.2.tgz", + "integrity": "sha512-UTlYYhXGLOy05P/vKVT2Ui7WtC7NiRzGtJyAKKn32g5Gvcjn7KAClLPWlipCtxIus934dFg9o9jXiBL0nP+t9Q==", + "license": "MIT" + }, + "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", + "peer": true, + "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", + "peer": true, + "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", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/filename-reserved-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", + "integrity": "sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/filenamify": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-4.2.0.tgz", + "integrity": "sha512-pkgE+4p7N1n7QieOopmn3TqJaefjdWXwEkj2XLZJLKfOgcQKkn11ahvGNgTD8mLggexLiDFQxeTs14xVU22XPA==", + "license": "MIT", + "peer": true, + "dependencies": { + "filename-reserved-regex": "^2.0.0", + "strip-outer": "^1.0.1", + "trim-repeated": "^1.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "license": "MIT", + "peer": true, + "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", + "peer": true, + "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", + "peer": true, + "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", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gud": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gud/-/gud-1.0.0.tgz", + "integrity": "sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw==", + "license": "MIT", + "peer": true + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "peer": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "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", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "peer": true, + "dependencies": { + "has-symbols": "^1.0.3" + }, + "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", + "peer": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/iceberg-js": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz", + "integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "license": "MIT", + "peer": true, + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "license": "MIT", + "peer": true, + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "license": "MIT", + "peer": true, + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT", + "peer": true + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "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", + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/normalize.css": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/normalize.css/-/normalize.css-8.0.1.tgz", + "integrity": "sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg==", + "license": "MIT", + "peer": true + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/popper.js": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", + "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==", + "deprecated": "You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1", + "license": "MIT", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/react": { + "version": "16.12.0", + "resolved": "https://registry.npmjs.org/react/-/react-16.12.0.tgz", + "integrity": "sha512-fglqy3k5E+81pA8s+7K0/T3DBCF0ZDOher1elBFzF7O6arXJgzyu/FW+COxFvAWXJoJN9KIZbT2LXlukwphYTA==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "16.12.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.12.0.tgz", + "integrity": "sha512-LMxFfAGrcS3kETtQaCkTKjMiifahaMySFDn71fZUNpPHZQEzmk/GiAeIT8JSOrHB23fnuCOMruL2a8NYlw+8Gw==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2", + "scheduler": "^0.18.0" + }, + "peerDependencies": { + "react": "^16.0.0" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT", + "peer": true + }, + "node_modules/react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==", + "license": "MIT", + "peer": true + }, + "node_modules/react-popper": { + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-1.3.11.tgz", + "integrity": "sha512-VSA/bS+pSndSF2fiasHK/PTEEAyOpX60+H5EPAjoArr8JGm+oihu4UbrqcEBpQibJxBVCpYyjAX7abJ+7DoYVg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/runtime": "^7.1.2", + "@hypnosphi/create-react-context": "^0.3.1", + "deep-equal": "^1.1.1", + "popper.js": "^1.14.4", + "prop-types": "^15.6.1", + "typed-styles": "^0.0.7", + "warning": "^4.0.2" + }, + "peerDependencies": { + "react": "0.14.x || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/react-transition-group": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.9.0.tgz", + "integrity": "sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "dom-helpers": "^3.4.0", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2", + "react-lifecycles-compat": "^3.0.4" + }, + "peerDependencies": { + "react": ">=15.0.0", + "react-dom": ">=15.0.0" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", + "license": "MIT", + "peer": true + }, + "node_modules/scheduler": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.18.0.tgz", + "integrity": "sha512-agTSHR1Nbfi6ulI0kYNK0203joW2Y5W4po4l+v03tOoiJKpTBbxpNhWDvqc/4IcOw+KLmSiQLTasZ4cab2/UWQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "peer": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/strip-outer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", + "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", + "license": "MIT", + "peer": true, + "dependencies": { + "escape-string-regexp": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/trim-repeated": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", + "integrity": "sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==", + "license": "MIT", + "peer": true, + "dependencies": { + "escape-string-regexp": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tslib": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", + "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==", + "license": "Apache-2.0", + "peer": true + }, + "node_modules/typed-styles": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/typed-styles/-/typed-styles-0.0.7.tgz", + "integrity": "sha512-pzP0PWoZUhsECYjABgCGQlRGL1n7tOHsgwYv3oIiEpJwGhFTuty/YNeduxQYzXXa3Ge5BdT6sHYIQYpl4uJ+5Q==", + "license": "MIT", + "peer": true + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "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 + } + } + } + } +} diff --git a/bjsEditorPlugin/package.json b/bjsEditorPlugin/package.json new file mode 100644 index 0000000..80cdfc7 --- /dev/null +++ b/bjsEditorPlugin/package.json @@ -0,0 +1,21 @@ +{ + "name": "babylonjs-editor-space-game-plugin", + "version": "1.0.0", + "description": "Export BabylonJS Editor scenes to Space Game LevelConfig format", + "main": "dist/index.js", + "scripts": { + "build": "tsc && node -e \"const fs=require('fs');const p=JSON.parse(fs.readFileSync('package.json'));p.main='index.js';fs.writeFileSync('dist/package.json',JSON.stringify(p,null,2))\"", + "watch": "tsc -w" + }, + "dependencies": { + "@auth0/auth0-spa-js": "^2.1.3", + "@supabase/supabase-js": "^2.45.0" + }, + "devDependencies": { + "typescript": "^5.0.0", + "@babylonjs/core": "^6.0.0" + }, + "peerDependencies": { + "babylonjs-editor": "^4.0.0" + } +} diff --git a/bjsEditorPlugin/src/cameraSpeed.ts b/bjsEditorPlugin/src/cameraSpeed.ts new file mode 100644 index 0000000..e3115df --- /dev/null +++ b/bjsEditorPlugin/src/cameraSpeed.ts @@ -0,0 +1,37 @@ +/** + * Camera speed persistence and application + */ +import { showNotification } from "./utils"; + +const CAMERA_SPEED_KEY = "space-game-camera-speed"; +const DEFAULT_CAMERA_SPEED = 1; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +let editorRef: any = null; + +export function initCameraSpeed(editor: any): void { + editorRef = editor; + applyCameraSpeed(getSavedCameraSpeed()); +} + +export function getSavedCameraSpeed(): number { + const saved = localStorage.getItem(CAMERA_SPEED_KEY); + return saved ? parseFloat(saved) : DEFAULT_CAMERA_SPEED; +} + +export function saveCameraSpeed(speed: number): void { + localStorage.setItem(CAMERA_SPEED_KEY, String(speed)); +} + +export function applyCameraSpeed(speed: number): void { + const camera = editorRef?.layout?.preview?.camera; + if (camera) { + camera.speed = speed; + } +} + +export function handleCameraSpeedChange(speed: number): void { + saveCameraSpeed(speed); + applyCameraSpeed(speed); + showNotification(`Camera speed set to ${speed}`); +} diff --git a/bjsEditorPlugin/src/cloudLevelHandlers.ts b/bjsEditorPlugin/src/cloudLevelHandlers.ts new file mode 100644 index 0000000..99151ed --- /dev/null +++ b/bjsEditorPlugin/src/cloudLevelHandlers.ts @@ -0,0 +1,149 @@ +/** + * Handlers for cloud level browsing and loading + */ +import { getOfficialLevels, getMyLevels, saveLevel, CloudLevelEntry } from "./services/pluginLevelService"; +import { showLevelBrowserModal, closeLevelBrowserModal } from "./levelBrowser/levelBrowserModal"; +import { showSaveLevelModal, closeSaveLevelModal } from "./levelBrowser/saveLevelModal"; +import { updateAuthSection } from "./levelBrowser/authStatus"; +import { importLevelConfig } from "./levelImporter"; +import { exportLevelConfig } from "./exporter"; +import { showNotification } from "./utils"; +import { isAuthenticated } from "./services/pluginAuth"; +import { Scene } from "@babylonjs/core/scene"; + +let sceneGetter: () => Scene | null = () => null; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +let editorRef: any = null; + +export function initCloudHandlers(getScene: () => Scene | null, editor: unknown): void { + sceneGetter = getScene; + editorRef = editor; +} + +export async function handleBrowseOfficial(): Promise { + try { + showNotification("Loading levels..."); + const levels = await getOfficialLevels(); + showLevelBrowserModal(levels, "Official Levels", { + onSelectLevel: handleLoadLevel, + onClose: closeLevelBrowserModal, + }); + } catch (err) { + console.error("Browse official error:", err); + showNotification("Failed to fetch levels", true); + } +} + +export async function handleBrowseMyLevels(): Promise { + if (!isAuthenticated()) { + showNotification("Sign in to view your levels", true); + return; + } + + try { + showNotification("Loading levels..."); + const levels = await getMyLevels(); + showLevelBrowserModal(levels, "My Levels", { + onSelectLevel: handleLoadLevel, + onClose: closeLevelBrowserModal, + onSaveNew: handleSaveNewLevel, + onSaveExisting: handleSaveExistingLevel, + }); + } catch (err) { + console.error("Browse my levels error:", err); + showNotification("Failed to fetch levels", true); + } +} + +export function handleAuthChange(): void { + const authSection = document.getElementById("auth-status-section"); + if (authSection) { + updateAuthSection(authSection, handleAuthChange); + } +} + +function handleLoadLevel(level: CloudLevelEntry): void { + const scene = sceneGetter(); + if (!scene) { + showNotification("No scene loaded", true); + return; + } + + try { + closeLevelBrowserModal(); + showNotification(`Loading: ${level.name}...`); + importLevelConfig(scene, level.config, () => { + refreshEditorGraph(); + showNotification(`Loaded: ${level.name}`); + }); + } catch (err) { + console.error("Import error:", err); + showNotification("Failed to import level", true); + } +} + +function handleSaveNewLevel(): void { + closeLevelBrowserModal(); + showSaveLevelModal( + async ({ name, difficulty }) => { + const scene = sceneGetter(); + if (!scene) { + showNotification("No scene loaded", true); + closeSaveLevelModal(); + return; + } + + try { + showNotification("Saving level..."); + const configJson = exportLevelConfig(scene); + const config = JSON.parse(configJson); + const levelId = await saveLevel(name, difficulty, config); + + closeSaveLevelModal(); + if (levelId) { + showNotification(`Saved: ${name}`); + } else { + showNotification("Failed to save level", true); + } + } catch (err) { + console.error("Save error:", err); + showNotification("Failed to save level", true); + closeSaveLevelModal(); + } + }, + closeSaveLevelModal + ); +} + +async function handleSaveExistingLevel(level: CloudLevelEntry): Promise { + const scene = sceneGetter(); + if (!scene) { + showNotification("No scene loaded", true); + return; + } + + try { + showNotification(`Saving ${level.name}...`); + const configJson = exportLevelConfig(scene); + const config = JSON.parse(configJson); + const result = await saveLevel(level.name, level.difficulty, config, level.id); + + if (result) { + showNotification(`Saved: ${level.name}`); + } else { + showNotification("Failed to save level", true); + } + } catch (err) { + console.error("Save error:", err); + showNotification("Failed to save level", true); + } +} + +function refreshEditorGraph(): void { + try { + editorRef?.layout?.graph?.refresh?.(); + editorRef?.layout?.preview?.forceUpdate?.(); + } catch (err) { + console.warn("Could not refresh editor graph:", err); + } +} diff --git a/bjsEditorPlugin/src/config.ts b/bjsEditorPlugin/src/config.ts new file mode 100644 index 0000000..c1e128d --- /dev/null +++ b/bjsEditorPlugin/src/config.ts @@ -0,0 +1,10 @@ +/** + * Plugin configuration - hardcoded values for editor context + * These are public/client-safe values (appear in browser bundles) + */ +export const PLUGIN_CONFIG = { + SUPABASE_URL: "https://ezipploqzuphwsptvvdv.supabase.co", + SUPABASE_ANON_KEY: + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImV6aXBwbG9xenVwaHdzcHR2dmR2Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjQxMDk2NTYsImV4cCI6MjA3OTY4NTY1Nn0.CjpAh8v0c54KAYCPuLmrgrcHFAOVRxOEQCW8zZ9lwzA", + WEBSITE_URL: "https://www.flatearthdefense.com", +}; diff --git a/bjsEditorPlugin/src/configBuilders/asteroidBuilder.ts b/bjsEditorPlugin/src/configBuilders/asteroidBuilder.ts new file mode 100644 index 0000000..1e5cf63 --- /dev/null +++ b/bjsEditorPlugin/src/configBuilders/asteroidBuilder.ts @@ -0,0 +1,44 @@ +/** + * Builds AsteroidConfig[] from meshes with AsteroidComponent + */ +import { AbstractMesh } from "@babylonjs/core/Meshes/abstractMesh"; +import { AsteroidConfig, Vector3Array } from "../types"; +import { getScriptValues } from "../scriptUtils"; + +export function buildAsteroidConfigs(meshes: AbstractMesh[]): AsteroidConfig[] { + return meshes.map((mesh, index) => buildSingleAsteroid(mesh, index)); +} + +function buildSingleAsteroid(mesh: AbstractMesh, index: number): AsteroidConfig { + const script = getScriptValues(mesh); + const rotation = toVector3Array(mesh.rotation); + const hasRotation = rotation[0] !== 0 || rotation[1] !== 0 || rotation[2] !== 0; + + return { + id: mesh.name || `asteroid-${index}`, + position: toVector3Array(mesh.position), + rotation: hasRotation ? rotation : undefined, + scale: mesh.scaling.x, + linearVelocity: extractVector3(script.linearVelocity, [0, 0, 0]), + angularVelocity: extractVector3(script.angularVelocity, [0, 0, 0]), + mass: (script.mass as number) ?? 200, + targetId: (script.targetId as string) || undefined, + targetMode: parseTargetMode(script.targetMode as string) + }; +} + +function toVector3Array(v: { x: number; y: number; z: number }): Vector3Array { + return [v.x, v.y, v.z]; +} + +function extractVector3(v: unknown, defaultVal: Vector3Array): Vector3Array { + if (!v) return defaultVal; + if (Array.isArray(v)) return v as Vector3Array; + const vec = v as { x?: number; y?: number; z?: number }; + return [vec.x ?? 0, vec.y ?? 0, vec.z ?? 0]; +} + +function parseTargetMode(mode: string): 'orbit' | 'moveToward' | undefined { + if (mode === 'orbit' || mode === 'moveToward') return mode; + return undefined; +} diff --git a/bjsEditorPlugin/src/configBuilders/baseBuilder.ts b/bjsEditorPlugin/src/configBuilders/baseBuilder.ts new file mode 100644 index 0000000..9ee2b7c --- /dev/null +++ b/bjsEditorPlugin/src/configBuilders/baseBuilder.ts @@ -0,0 +1,51 @@ +/** + * Builds StartBaseConfig from mesh with BaseComponent + */ +import { AbstractMesh } from "@babylonjs/core/Meshes/abstractMesh"; +import { StartBaseConfig, Vector3Array } from "../types"; +import { getScriptValues } from "../scriptUtils"; + +export function buildBaseConfig(mesh: AbstractMesh | null): StartBaseConfig | undefined { + if (!mesh) { + return undefined; + } + + const script = getScriptValues(mesh); + const glbPath = extractGlbPath(mesh, script); + const rotation = toVector3Array(mesh.rotation); + const hasRotation = rotation[0] !== 0 || rotation[1] !== 0 || rotation[2] !== 0; + + return { + position: toVector3Array(mesh.position), + rotation: hasRotation ? rotation : undefined, + baseGlbPath: glbPath || undefined, + landingGlbPath: (script.landingGlbPath as string) || undefined, + }; +} + +function extractGlbPath(mesh: AbstractMesh, script: Record): string | null { + // 1. Check script property first (manual override) + if (script.baseGlbPath) return script.baseGlbPath as string; + + // 2. Check mesh metadata for source file path + const meta = mesh.metadata as Record | undefined; + if (meta?.sourcePath) return extractFilename(meta.sourcePath); + if (meta?.gltf?.sourcePath) return extractFilename(meta.gltf.sourcePath); + + // 3. Derive from mesh name if it looks like a GLB reference + const name = mesh.name || ""; + if (name.endsWith(".glb") || name.endsWith(".gltf")) return name; + + // 4. Check if name contains path separator and extract filename + if (name.includes("/")) return extractFilename(name); + + return null; +} + +function extractFilename(path: string): string { + return path.split("/").pop() || path; +} + +function toVector3Array(v: { x: number; y: number; z: number }): Vector3Array { + return [v.x, v.y, v.z]; +} diff --git a/bjsEditorPlugin/src/configBuilders/planetBuilder.ts b/bjsEditorPlugin/src/configBuilders/planetBuilder.ts new file mode 100644 index 0000000..84db341 --- /dev/null +++ b/bjsEditorPlugin/src/configBuilders/planetBuilder.ts @@ -0,0 +1,31 @@ +/** + * Builds PlanetConfig[] from meshes with PlanetComponent + */ +import { AbstractMesh } from "@babylonjs/core/Meshes/abstractMesh"; +import { PlanetConfig, Vector3Array } from "../types"; +import { getScriptValues } from "../scriptUtils"; + +export function buildPlanetConfigs(meshes: AbstractMesh[]): PlanetConfig[] { + return meshes.map(buildSinglePlanet); +} + +function buildSinglePlanet(mesh: AbstractMesh): PlanetConfig { + const script = getScriptValues(mesh); + + return { + name: mesh.name || "planet", + position: toVector3Array(mesh.position), + diameter: (script.diameter as number) ?? 100, + texturePath: (script.texturePath as string) || "planet_texture.jpg", + rotation: hasRotation(mesh) ? toVector3Array(mesh.rotation) : undefined + }; +} + +function toVector3Array(v: { x: number; y: number; z: number }): Vector3Array { + return [v.x, v.y, v.z]; +} + +function hasRotation(mesh: AbstractMesh): boolean { + const r = mesh.rotation; + return r.x !== 0 || r.y !== 0 || r.z !== 0; +} diff --git a/bjsEditorPlugin/src/configBuilders/shipBuilder.ts b/bjsEditorPlugin/src/configBuilders/shipBuilder.ts new file mode 100644 index 0000000..9ad6c8d --- /dev/null +++ b/bjsEditorPlugin/src/configBuilders/shipBuilder.ts @@ -0,0 +1,38 @@ +/** + * Builds ShipConfig from mesh with ShipComponent + */ +import { AbstractMesh } from "@babylonjs/core/Meshes/abstractMesh"; +import { ShipConfig, Vector3Array } from "../types"; +import { getScriptValues } from "../scriptUtils"; + +export function buildShipConfig(mesh: AbstractMesh | null): ShipConfig { + if (!mesh) { + return { position: [0, 1, 0] }; + } + + const script = getScriptValues(mesh); + + return { + position: toVector3Array(mesh.position), + rotation: mesh.rotation ? toVector3Array(mesh.rotation) : undefined, + linearVelocity: extractVector3OrUndefined(script.linearVelocity), + angularVelocity: extractVector3OrUndefined(script.angularVelocity) + }; +} + +function toVector3Array(v: { x: number; y: number; z: number }): Vector3Array { + return [v.x, v.y, v.z]; +} + +function extractVector3OrUndefined(v: unknown): Vector3Array | undefined { + if (!v) return undefined; + if (Array.isArray(v)) { + const arr = v as number[]; + if (arr[0] === 0 && arr[1] === 0 && arr[2] === 0) return undefined; + return arr as Vector3Array; + } + const vec = v as { x?: number; y?: number; z?: number }; + const arr: Vector3Array = [vec.x ?? 0, vec.y ?? 0, vec.z ?? 0]; + if (arr[0] === 0 && arr[1] === 0 && arr[2] === 0) return undefined; + return arr; +} diff --git a/bjsEditorPlugin/src/configBuilders/sunBuilder.ts b/bjsEditorPlugin/src/configBuilders/sunBuilder.ts new file mode 100644 index 0000000..2c4c4db --- /dev/null +++ b/bjsEditorPlugin/src/configBuilders/sunBuilder.ts @@ -0,0 +1,39 @@ +/** + * Builds SunConfig from mesh with SunComponent + */ +import { AbstractMesh } from "@babylonjs/core/Meshes/abstractMesh"; +import { SunConfig, Vector3Array } from "../types"; +import { getScriptValues } from "../scriptUtils"; + +const DEFAULT_SUN: SunConfig = { + position: [0, 0, 400], + diameter: 50 +}; + +export function buildSunConfig(mesh: AbstractMesh | null): SunConfig { + if (!mesh) { + return DEFAULT_SUN; + } + + const script = getScriptValues(mesh); + + const rotation = toVector3Array(mesh.rotation); + const hasRotation = rotation[0] !== 0 || rotation[1] !== 0 || rotation[2] !== 0; + + return { + position: toVector3Array(mesh.position), + rotation: hasRotation ? rotation : undefined, + diameter: (script.diameter as number) ?? 50, + intensity: (script.intensity as number) ?? 1000000, + scale: hasNonUniformScale(mesh) ? toVector3Array(mesh.scaling) : undefined + }; +} + +function toVector3Array(v: { x: number; y: number; z: number }): Vector3Array { + return [v.x, v.y, v.z]; +} + +function hasNonUniformScale(mesh: AbstractMesh): boolean { + const s = mesh.scaling; + return s.x !== s.y || s.y !== s.z; +} diff --git a/bjsEditorPlugin/src/configBuilders/targetBuilder.ts b/bjsEditorPlugin/src/configBuilders/targetBuilder.ts new file mode 100644 index 0000000..9d99949 --- /dev/null +++ b/bjsEditorPlugin/src/configBuilders/targetBuilder.ts @@ -0,0 +1,24 @@ +/** + * Builds TargetConfig[] from TransformNodes with TargetComponent + */ +import { TransformNode } from "@babylonjs/core/Meshes/transformNode"; +import { TargetConfig, Vector3Array } from "../types"; +import { getScriptValues } from "../scriptUtils"; + +export function buildTargetConfigs(nodes: TransformNode[]): TargetConfig[] { + return nodes.map(buildSingleTarget); +} + +function buildSingleTarget(node: TransformNode): TargetConfig { + const script = getScriptValues(node); + + return { + id: node.name || node.id, + name: (script.displayName as string) || node.name || "Target", + position: toVector3Array(node.position) + }; +} + +function toVector3Array(v: { x: number; y: number; z: number }): Vector3Array { + return [v.x, v.y, v.z]; +} diff --git a/bjsEditorPlugin/src/exporter.ts b/bjsEditorPlugin/src/exporter.ts new file mode 100644 index 0000000..d759eb0 --- /dev/null +++ b/bjsEditorPlugin/src/exporter.ts @@ -0,0 +1,35 @@ +/** + * Main export orchestrator - builds LevelConfig from scene + */ +import { Scene } from "@babylonjs/core/scene"; +import { LevelConfig } from "./types"; +import { collectMeshesByComponent } from "./meshCollector"; +import { buildAsteroidConfigs } from "./configBuilders/asteroidBuilder"; +import { buildBaseConfig } from "./configBuilders/baseBuilder"; +import { buildPlanetConfigs } from "./configBuilders/planetBuilder"; +import { buildShipConfig } from "./configBuilders/shipBuilder"; +import { buildSunConfig } from "./configBuilders/sunBuilder"; +import { buildTargetConfigs } from "./configBuilders/targetBuilder"; + +export function exportLevelConfig(scene: Scene): string { + const meshes = collectMeshesByComponent(scene); + + const config: LevelConfig = { + version: "1.0", + difficulty: "rookie", + timestamp: new Date().toISOString(), + metadata: { + author: "BabylonJS Editor", + description: "Exported from Editor" + }, + ship: buildShipConfig(meshes.ship), + startBase: buildBaseConfig(meshes.base), + sun: buildSunConfig(meshes.sun), + targets: buildTargetConfigs(meshes.targets), + planets: buildPlanetConfigs(meshes.planets), + asteroids: buildAsteroidConfigs(meshes.asteroids), + useOrbitConstraints: true + }; + + return JSON.stringify(config, null, 2); +} diff --git a/bjsEditorPlugin/src/floatingUI.ts b/bjsEditorPlugin/src/floatingUI.ts new file mode 100644 index 0000000..78f59c7 --- /dev/null +++ b/bjsEditorPlugin/src/floatingUI.ts @@ -0,0 +1,93 @@ +/** + * Floating UI panel for Space Game plugin + */ +import { createContent } from "./panelSections"; + +const PANEL_ID = "space-game-plugin-panel"; + +let panelElement: HTMLDivElement | null = null; +let isDragging = false; +let dragOffset = { x: 0, y: 0 }; + +export interface FloatingUICallbacks { + onExport: () => void; + onExportClipboard: () => void; + onApplyUniformScale: (scale: number) => void; + onCameraSpeedChange: (multiplier: number) => void; + getCameraSpeed: () => number; + onBrowseOfficial: () => void; + onBrowseMyLevels: () => void; + onAuthChange: () => void; +} + +export async function createFloatingUI(callbacks: FloatingUICallbacks): Promise { + if (document.getElementById(PANEL_ID)) return; + + panelElement = document.createElement("div"); + panelElement.id = PANEL_ID; + applyPanelStyles(panelElement); + + const header = createHeader(); + const content = await createContent(callbacks); + + panelElement.appendChild(header); + panelElement.appendChild(content); + document.body.appendChild(panelElement); + + setupDragHandlers(header); +} + +export function destroyFloatingUI(): void { + document.getElementById(PANEL_ID)?.remove(); + panelElement = null; +} + +function applyPanelStyles(panel: HTMLDivElement): void { + Object.assign(panel.style, { + position: "fixed", top: "80px", right: "20px", width: "200px", + backgroundColor: "#1e1e1e", border: "1px solid #3c3c3c", borderRadius: "8px", + boxShadow: "0 4px 12px rgba(0,0,0,0.4)", zIndex: "10000", + fontFamily: "system-ui, -apple-system, sans-serif", fontSize: "13px", + color: "#e0e0e0", overflow: "hidden", + }); +} + +function createHeader(): HTMLDivElement { + const header = document.createElement("div"); + Object.assign(header.style, { + padding: "8px 12px", backgroundColor: "#2d2d2d", borderBottom: "1px solid #3c3c3c", + cursor: "move", userSelect: "none", display: "flex", alignItems: "center", gap: "8px", + }); + const icon = document.createElement("span"); + icon.textContent = "\u{1F680}"; + const title = document.createElement("span"); + title.textContent = "Space Game"; + title.style.fontWeight = "500"; + header.append(icon, title); + return header; +} + +function setupDragHandlers(header: HTMLDivElement): void { + header.addEventListener("mousedown", onDragStart); + document.addEventListener("mousemove", onDrag); + document.addEventListener("mouseup", onDragEnd); +} + +function onDragStart(e: MouseEvent): void { + if (!panelElement) return; + isDragging = true; + const rect = panelElement.getBoundingClientRect(); + dragOffset.x = e.clientX - rect.left; + dragOffset.y = e.clientY - rect.top; +} + +function onDrag(e: MouseEvent): void { + if (!isDragging || !panelElement) return; + panelElement.style.left = `${e.clientX - dragOffset.x}px`; + panelElement.style.top = `${e.clientY - dragOffset.y}px`; + panelElement.style.right = "auto"; +} + +function onDragEnd(): void { + isDragging = false; +} diff --git a/bjsEditorPlugin/src/index.ts b/bjsEditorPlugin/src/index.ts new file mode 100644 index 0000000..6fc6ac1 --- /dev/null +++ b/bjsEditorPlugin/src/index.ts @@ -0,0 +1,99 @@ +/** + * BabylonJS Editor plugin entry point + * Uses Editor v5 plugin API: main(), close(), title, description + */ +import { exportLevelConfig } from "./exporter"; +import { createFloatingUI, destroyFloatingUI } from "./floatingUI"; +import { initCameraSpeed, getSavedCameraSpeed, handleCameraSpeedChange } from "./cameraSpeed"; +import { showNotification, copyToClipboard, downloadJson } from "./utils"; +import { initAuth } from "./services/pluginAuth"; +import { initCloudHandlers, handleBrowseOfficial, handleBrowseMyLevels, handleAuthChange } from "./cloudLevelHandlers"; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type Editor = any; + +let editorInstance: Editor; + +export const title = "Space Game"; +export const description = "Export/Import Space Game level configurations"; + +export async function main(editor: Editor): Promise { + editorInstance = editor; + + // Expose for debugging in DevTools + (window as any).spaceGameEditor = editor; + (window as any).exportSpaceGameLevel = handleExport; + (window as any).exportSpaceGameLevelToClipboard = handleExportToClipboard; + + initCameraSpeed(editor); + initCloudHandlers(getScene, editor); + initAuth().catch((err) => console.warn("Auth init failed:", err)); + + await createFloatingUI({ + onExport: handleExport, + onExportClipboard: handleExportToClipboard, + onApplyUniformScale: handleApplyUniformScale, + onCameraSpeedChange: handleCameraSpeedChange, + getCameraSpeed: getSavedCameraSpeed, + onBrowseOfficial: handleBrowseOfficial, + onBrowseMyLevels: handleBrowseMyLevels, + onAuthChange: handleAuthChange, + }); + + console.log("Space Game plugin activated"); +} + +export function close(): void { + destroyFloatingUI(); + console.log("Space Game plugin deactivated"); +} + +async function handleExport(): Promise { + try { + const scene = getScene(); + if (!scene) { + showNotification("No scene loaded", true); + return; + } + const json = exportLevelConfig(scene); + downloadJson(json, "level.json"); + showNotification("Level exported!"); + } catch (err) { + console.error("Export error:", err); + showNotification("Failed to export: " + (err as Error).message, true); + } +} + +async function handleExportToClipboard(): Promise { + try { + const scene = getScene(); + if (!scene) { + showNotification("No scene loaded", true); + return; + } + const json = exportLevelConfig(scene); + await copyToClipboard(json); + showNotification("Copied to clipboard!"); + } catch (err) { + console.error("Clipboard error:", err); + showNotification("Failed to copy: " + (err as Error).message, true); + } +} + +function handleApplyUniformScale(scale: number): void { + const selected = editorInstance?.layout?.inspector?.state?.editedObject; + if (!selected?.scaling) { + showNotification("Select a mesh first", true); + return; + } + selected.scaling.x = scale; + selected.scaling.y = scale; + selected.scaling.z = scale; + showNotification(`Scale set to ${scale}`); +} + +function getScene() { + const scene = editorInstance?.layout?.preview?.scene ?? editorInstance?.scene; + console.log("getScene called - editor:", editorInstance, "scene:", scene); + return scene; +} diff --git a/bjsEditorPlugin/src/levelBrowser/authStatus.ts b/bjsEditorPlugin/src/levelBrowser/authStatus.ts new file mode 100644 index 0000000..c9738ac --- /dev/null +++ b/bjsEditorPlugin/src/levelBrowser/authStatus.ts @@ -0,0 +1,44 @@ +/** + * Auth status display and login/logout buttons + */ +import { isAuthenticated, login, logout } from "../services/pluginAuth"; +import { createButton } from "../uiComponents"; + +export function createAuthSection(onAuthChange: () => void): HTMLDivElement { + const section = document.createElement("div"); + section.id = "auth-status-section"; + section.style.marginBottom = "8px"; + updateAuthSection(section, onAuthChange); + return section; +} + +export function updateAuthSection(section: HTMLElement, onAuthChange: () => void): void { + section.innerHTML = ""; + + if (isAuthenticated()) { + const label = document.createElement("div"); + label.textContent = "Signed in"; + label.style.cssText = "font-size: 11px; color: #888; margin-bottom: 6px;"; + + const btn = createButton("Sign Out", async () => { + await logout(); + onAuthChange(); + }); + btn.style.width = "100%"; + btn.style.backgroundColor = "#6c757d"; + + section.appendChild(label); + section.appendChild(btn); + } else { + const btn = createButton("Sign In", async () => { + try { + await login(); + onAuthChange(); + } catch (err) { + console.error("Login failed:", err); + } + }); + btn.style.width = "100%"; + section.appendChild(btn); + } +} diff --git a/bjsEditorPlugin/src/levelBrowser/levelBrowserModal.ts b/bjsEditorPlugin/src/levelBrowser/levelBrowserModal.ts new file mode 100644 index 0000000..ebd679a --- /dev/null +++ b/bjsEditorPlugin/src/levelBrowser/levelBrowserModal.ts @@ -0,0 +1,123 @@ +/** + * Modal dialog for browsing and selecting levels + */ +import type { CloudLevelEntry } from "../services/pluginLevelService"; + +const MODAL_ID = "level-browser-modal"; + +interface ModalCallbacks { + onSelectLevel: (level: CloudLevelEntry) => void; + onClose: () => void; + onSaveNew?: () => void; + onSaveExisting?: (level: CloudLevelEntry) => void; +} + +export function showLevelBrowserModal(levels: CloudLevelEntry[], title: string, callbacks: ModalCallbacks): void { + closeLevelBrowserModal(); + const overlay = createOverlay(callbacks.onClose); + const modal = createModalContainer(); + modal.appendChild(createHeader(title, callbacks.onClose)); + modal.appendChild(createLevelList(levels, callbacks)); + overlay.appendChild(modal); + document.body.appendChild(overlay); +} + +export function closeLevelBrowserModal(): void { + document.getElementById(MODAL_ID)?.remove(); +} + +function createOverlay(onClose: () => void): HTMLDivElement { + const overlay = document.createElement("div"); + overlay.id = MODAL_ID; + overlay.style.cssText = + "position:fixed;inset:0;background:rgba(0,0,0,0.6);display:flex;align-items:center;justify-content:center;z-index:10002"; + overlay.addEventListener("click", (e) => { if (e.target === overlay) onClose(); }); + return overlay; +} + +function createModalContainer(): HTMLDivElement { + const modal = document.createElement("div"); + modal.style.cssText = + "background:#1e1e1e;border-radius:8px;width:500px;max-height:80vh;display:flex;flex-direction:column;box-shadow:0 8px 32px rgba(0,0,0,0.5)"; + return modal; +} + +function createHeader(title: string, onClose: () => void): HTMLDivElement { + const header = document.createElement("div"); + header.style.cssText = "padding:16px;border-bottom:1px solid #3c3c3c;display:flex;justify-content:space-between;align-items:center"; + const titleEl = document.createElement("span"); + titleEl.textContent = title; + titleEl.style.cssText = "font-weight:600;font-size:16px;color:#e0e0e0"; + const closeBtn = document.createElement("button"); + closeBtn.textContent = "\u2715"; + closeBtn.style.cssText = "background:none;border:none;color:#888;font-size:18px;cursor:pointer;padding:4px 8px"; + closeBtn.addEventListener("click", onClose); + header.append(titleEl, closeBtn); + return header; +} + +function createLevelList(levels: CloudLevelEntry[], callbacks: ModalCallbacks): HTMLDivElement { + const container = document.createElement("div"); + container.style.cssText = "display:flex;flex-direction:column;flex:1;max-height:450px"; + + const list = document.createElement("div"); + list.style.cssText = "overflow-y:auto;flex:1"; + + if (levels.length === 0) { + const empty = document.createElement("div"); + empty.style.cssText = "padding:32px;text-align:center;color:#888"; + empty.textContent = "No levels found"; + list.appendChild(empty); + } else { + for (const level of levels) { + list.appendChild(createLevelItem(level, callbacks)); + } + } + + container.appendChild(list); + + if (callbacks.onSaveNew) { + container.appendChild(createFooter(callbacks.onSaveNew)); + } + + return container; +} + +function createFooter(onSaveNew: () => void): HTMLDivElement { + const footer = document.createElement("div"); + footer.style.cssText = "padding:12px 16px;border-top:1px solid #3c3c3c;display:flex;justify-content:center"; + const saveBtn = document.createElement("button"); + saveBtn.textContent = "+ Save Current Scene as New Level"; + saveBtn.style.cssText = "padding:8px 16px;background:#4fc3f7;border:none;color:#000;border-radius:4px;cursor:pointer;font-weight:500"; + saveBtn.addEventListener("click", onSaveNew); + footer.appendChild(saveBtn); + return footer; +} + +function createLevelItem(level: CloudLevelEntry, callbacks: ModalCallbacks): HTMLDivElement { + const item = document.createElement("div"); + item.style.cssText = "padding:12px 16px;border-bottom:1px solid #2d2d2d;display:flex;justify-content:space-between;align-items:center"; + + const info = document.createElement("div"); + info.style.cssText = "cursor:pointer;flex:1"; + info.innerHTML = `
${level.name}
+
${level.difficulty} | ${level.levelType} | ${new Date(level.updatedAt).toLocaleDateString()}
`; + info.addEventListener("click", () => callbacks.onSelectLevel(level)); + + item.appendChild(info); + + if (callbacks.onSaveExisting && level.levelType !== "official") { + const saveBtn = document.createElement("button"); + saveBtn.textContent = "Save"; + saveBtn.style.cssText = "padding:4px 12px;background:#28a745;border:none;color:#fff;border-radius:4px;cursor:pointer;font-size:12px;margin-left:8px"; + saveBtn.addEventListener("click", (e) => { + e.stopPropagation(); + callbacks.onSaveExisting!(level); + }); + item.appendChild(saveBtn); + } + + item.addEventListener("mouseenter", () => (item.style.backgroundColor = "#2d2d2d")); + item.addEventListener("mouseleave", () => (item.style.backgroundColor = "transparent")); + return item; +} diff --git a/bjsEditorPlugin/src/levelBrowser/saveLevelModal.ts b/bjsEditorPlugin/src/levelBrowser/saveLevelModal.ts new file mode 100644 index 0000000..631a0f9 --- /dev/null +++ b/bjsEditorPlugin/src/levelBrowser/saveLevelModal.ts @@ -0,0 +1,109 @@ +/** + * Modal for saving a level with name and difficulty + */ +const MODAL_ID = "save-level-modal"; + +interface SaveLevelData { + name: string; + difficulty: string; +} + +export function showSaveLevelModal( + onSave: (data: SaveLevelData) => void, + onCancel: () => void +): void { + closeSaveLevelModal(); + const overlay = createOverlay(onCancel); + const modal = createModalContainer(); + modal.appendChild(createHeader(onCancel)); + modal.appendChild(createForm(onSave, onCancel)); + overlay.appendChild(modal); + document.body.appendChild(overlay); +} + +export function closeSaveLevelModal(): void { + document.getElementById(MODAL_ID)?.remove(); +} + +function createOverlay(onCancel: () => void): HTMLDivElement { + const overlay = document.createElement("div"); + overlay.id = MODAL_ID; + overlay.style.cssText = + "position:fixed;inset:0;background:rgba(0,0,0,0.7);display:flex;align-items:center;justify-content:center;z-index:10004"; + overlay.addEventListener("click", (e) => { if (e.target === overlay) onCancel(); }); + return overlay; +} + +function createModalContainer(): HTMLDivElement { + const modal = document.createElement("div"); + modal.style.cssText = + "background:#1e1e1e;border-radius:8px;width:400px;box-shadow:0 8px 32px rgba(0,0,0,0.5)"; + return modal; +} + +function createHeader(onCancel: () => void): HTMLDivElement { + const header = document.createElement("div"); + header.style.cssText = "padding:16px;border-bottom:1px solid #3c3c3c;display:flex;justify-content:space-between"; + const title = document.createElement("span"); + title.textContent = "Save Level"; + title.style.cssText = "font-weight:600;font-size:16px;color:#e0e0e0"; + const closeBtn = document.createElement("button"); + closeBtn.textContent = "\u2715"; + closeBtn.style.cssText = "background:none;border:none;color:#888;font-size:18px;cursor:pointer;padding:0"; + closeBtn.addEventListener("click", onCancel); + header.append(title, closeBtn); + return header; +} + +function createForm(onSave: (data: SaveLevelData) => void, onCancel: () => void): HTMLDivElement { + const form = document.createElement("div"); + form.style.cssText = "padding:20px"; + + // Name input + const nameLabel = document.createElement("label"); + nameLabel.textContent = "Level Name"; + nameLabel.style.cssText = "display:block;color:#aaa;font-size:12px;margin-bottom:4px"; + const nameInput = document.createElement("input"); + nameInput.type = "text"; + nameInput.placeholder = "My Awesome Level"; + nameInput.style.cssText = inputStyle(); + + // Difficulty select + const diffLabel = document.createElement("label"); + diffLabel.textContent = "Difficulty"; + diffLabel.style.cssText = "display:block;color:#aaa;font-size:12px;margin-bottom:4px;margin-top:12px"; + const diffSelect = document.createElement("select"); + diffSelect.style.cssText = inputStyle(); + ["recruit", "pilot", "captain", "commander"].forEach((d) => { + const opt = document.createElement("option"); + opt.value = d; + opt.textContent = d.charAt(0).toUpperCase() + d.slice(1); + diffSelect.appendChild(opt); + }); + + // Buttons + const buttons = document.createElement("div"); + buttons.style.cssText = "display:flex;gap:8px;margin-top:20px;justify-content:flex-end"; + + const cancelBtn = document.createElement("button"); + cancelBtn.textContent = "Cancel"; + cancelBtn.style.cssText = "padding:8px 16px;background:#6c757d;border:none;color:#fff;border-radius:4px;cursor:pointer"; + cancelBtn.addEventListener("click", onCancel); + + const saveBtn = document.createElement("button"); + saveBtn.textContent = "Save Level"; + saveBtn.style.cssText = "padding:8px 16px;background:#4fc3f7;border:none;color:#000;border-radius:4px;cursor:pointer;font-weight:500"; + saveBtn.addEventListener("click", () => { + const name = nameInput.value.trim(); + if (!name) { nameInput.focus(); return; } + onSave({ name, difficulty: diffSelect.value }); + }); + + buttons.append(cancelBtn, saveBtn); + form.append(nameLabel, nameInput, diffLabel, diffSelect, buttons); + return form; +} + +function inputStyle(): string { + return "width:100%;padding:8px;border:1px solid #3c3c3c;border-radius:4px;background:#2d2d2d;color:#e0e0e0;font-size:14px;box-sizing:border-box"; +} diff --git a/bjsEditorPlugin/src/levelBrowser/tokenEntryModal.ts b/bjsEditorPlugin/src/levelBrowser/tokenEntryModal.ts new file mode 100644 index 0000000..b291561 --- /dev/null +++ b/bjsEditorPlugin/src/levelBrowser/tokenEntryModal.ts @@ -0,0 +1,95 @@ +/** + * Modal for entering API token + * User gets token from website profile page + */ +const MODAL_ID = "token-entry-modal"; + +export function showTokenEntryModal( + profileUrl: string, + onSubmit: (token: string) => void, + onCancel: () => void +): void { + closeTokenEntryModal(); + const overlay = createOverlay(onCancel); + const modal = createModalContainer(); + modal.appendChild(createHeader(onCancel)); + modal.appendChild(createContent(profileUrl, onSubmit, onCancel)); + overlay.appendChild(modal); + document.body.appendChild(overlay); +} + +export function closeTokenEntryModal(): void { + document.getElementById(MODAL_ID)?.remove(); +} + +function createOverlay(onCancel: () => void): HTMLDivElement { + const overlay = document.createElement("div"); + overlay.id = MODAL_ID; + overlay.style.cssText = + "position:fixed;inset:0;background:rgba(0,0,0,0.7);display:flex;align-items:center;justify-content:center;z-index:10003"; + overlay.addEventListener("click", (e) => { if (e.target === overlay) onCancel(); }); + return overlay; +} + +function createModalContainer(): HTMLDivElement { + const modal = document.createElement("div"); + modal.style.cssText = + "background:#1e1e1e;border-radius:8px;width:420px;box-shadow:0 8px 32px rgba(0,0,0,0.5)"; + return modal; +} + +function createHeader(onCancel: () => void): HTMLDivElement { + const header = document.createElement("div"); + header.style.cssText = "padding:16px;border-bottom:1px solid #3c3c3c;display:flex;justify-content:space-between"; + const title = document.createElement("span"); + title.textContent = "Sign In with Token"; + title.style.cssText = "font-weight:600;font-size:16px;color:#e0e0e0"; + const closeBtn = document.createElement("button"); + closeBtn.textContent = "\u2715"; + closeBtn.style.cssText = "background:none;border:none;color:#888;font-size:18px;cursor:pointer;padding:0"; + closeBtn.addEventListener("click", onCancel); + header.append(title, closeBtn); + return header; +} + +function createContent( + profileUrl: string, + onSubmit: (token: string) => void, + onCancel: () => void +): HTMLDivElement { + const content = document.createElement("div"); + content.style.cssText = "padding:20px"; + + const instructions = document.createElement("p"); + instructions.style.cssText = "color:#aaa;margin:0 0 16px;font-size:13px;line-height:1.5"; + instructions.innerHTML = ` + 1. Visit your profile page
+ 2. Generate an Editor Token
+ 3. Paste it below + `; + + const input = document.createElement("textarea"); + input.placeholder = "Paste your token here..."; + input.style.cssText = ` + width:100%;height:80px;padding:10px;border:1px solid #3c3c3c;border-radius:4px; + background:#2d2d2d;color:#e0e0e0;font-family:monospace;font-size:12px;resize:none; + box-sizing:border-box; + `; + + const buttons = document.createElement("div"); + buttons.style.cssText = "display:flex;gap:8px;margin-top:16px;justify-content:flex-end"; + + const cancelBtn = document.createElement("button"); + cancelBtn.textContent = "Cancel"; + cancelBtn.style.cssText = "padding:8px 16px;background:#6c757d;border:none;color:#fff;border-radius:4px;cursor:pointer"; + cancelBtn.addEventListener("click", onCancel); + + const submitBtn = document.createElement("button"); + submitBtn.textContent = "Sign In"; + submitBtn.style.cssText = "padding:8px 16px;background:#4fc3f7;border:none;color:#000;border-radius:4px;cursor:pointer;font-weight:500"; + submitBtn.addEventListener("click", () => onSubmit(input.value)); + + buttons.append(cancelBtn, submitBtn); + content.append(instructions, input, buttons); + return content; +} diff --git a/bjsEditorPlugin/src/levelImporter.ts b/bjsEditorPlugin/src/levelImporter.ts new file mode 100644 index 0000000..c4fa913 --- /dev/null +++ b/bjsEditorPlugin/src/levelImporter.ts @@ -0,0 +1,82 @@ +/** + * Imports LevelConfig into the editor scene + * Updates existing GLB meshes and creates asteroid instances + */ +import { Scene } from "@babylonjs/core/scene"; +import { Vector3 } from "@babylonjs/core/Maths/math.vector"; +import { Mesh } from "@babylonjs/core/Meshes/mesh"; +import { InstancedMesh } from "@babylonjs/core/Meshes/instancedMesh"; +import type { LevelConfig, AsteroidConfig } from "./types"; + +const SCRIPTS = { + asteroid: "scripts/editorScripts/AsteroidComponent.ts", + ship: "scripts/editorScripts/ShipComponent.ts", +}; + +export function importLevelConfig(scene: Scene, config: LevelConfig, onComplete?: () => void): void { + updateShip(scene, config); + updateBase(scene, config); + updateAsteroids(scene, config); + onComplete?.(); +} + +function updateShip(scene: Scene, config: LevelConfig): void { + const ship = findMeshByName(scene, "ship.glb") || findMeshByName(scene, "Ship"); + if (!ship) { console.warn("Ship mesh not found"); return; } + ship.position = new Vector3(...config.ship.position); + if (config.ship.rotation) ship.rotation = new Vector3(...config.ship.rotation); +} + +function updateBase(scene: Scene, config: LevelConfig): void { + if (!config.startBase) return; + const base = findMeshByName(scene, "base.glb"); + if (!base) { console.warn("Base mesh not found"); return; } + if (config.startBase.position) base.position = new Vector3(...config.startBase.position); + if (config.startBase.rotation) base.rotation = new Vector3(...config.startBase.rotation); +} + +function updateAsteroids(scene: Scene, config: LevelConfig): void { + const asteroidSource = findAsteroidSource(scene); + if (!asteroidSource) { console.warn("Asteroid source mesh not found"); return; } + clearAsteroidInstances(scene); + config.asteroids.forEach((a) => createAsteroidInstance(asteroidSource, a)); +} + +function findMeshByName(scene: Scene, name: string): Mesh | null { + return scene.meshes.find((m) => m.name === name) as Mesh | null; +} + +function findAsteroidSource(scene: Scene): Mesh | null { + // Find the Asteroid mesh (not instances) - it's a child of asteroid.glb + const asteroidMesh = scene.meshes.find((m) => + m.name === "Asteroid" && !(m instanceof InstancedMesh) + ); + return asteroidMesh as Mesh | null; +} + +function clearAsteroidInstances(scene: Scene): void { + // Clear all instances that have AsteroidComponent script attached + const instances = scene.meshes.filter((m) => + m instanceof InstancedMesh && m.metadata?.scripts?.[0]?.key === SCRIPTS.asteroid + ); + instances.forEach((inst) => inst.dispose()); +} + +function createAsteroidInstance(source: Mesh, a: AsteroidConfig): void { + const instance = source.createInstance(a.id); + instance.position = new Vector3(...a.position); + if (a.rotation) instance.rotation = new Vector3(...a.rotation); + instance.scaling = new Vector3(a.scale, a.scale, a.scale); + instance.metadata = { + scripts: [{ + key: SCRIPTS.asteroid, enabled: true, + values: { + linearVelocity: { type: "vector3", value: a.linearVelocity }, + angularVelocity: { type: "vector3", value: a.angularVelocity ?? [0, 0, 0] }, + mass: { type: "number", value: a.mass ?? 200 }, + targetId: { type: "string", value: a.targetId ?? "" }, + targetMode: { type: "string", value: a.targetMode ?? "" }, + }, + }], + }; +} diff --git a/bjsEditorPlugin/src/meshCollector.ts b/bjsEditorPlugin/src/meshCollector.ts new file mode 100644 index 0000000..72d30d2 --- /dev/null +++ b/bjsEditorPlugin/src/meshCollector.ts @@ -0,0 +1,69 @@ +/** + * Collects meshes from scene grouped by their attached component type + */ +import { Scene } from "@babylonjs/core/scene"; +import { AbstractMesh } from "@babylonjs/core/Meshes/abstractMesh"; +import { TransformNode } from "@babylonjs/core/Meshes/transformNode"; + +import { getScriptName } from "./scriptUtils"; + +export interface CollectedMeshes { + asteroids: AbstractMesh[]; + planets: AbstractMesh[]; + ship: AbstractMesh | null; + sun: AbstractMesh | null; + base: AbstractMesh | null; + targets: TransformNode[]; +} + +export function collectMeshesByComponent(scene: Scene): CollectedMeshes { + const result: CollectedMeshes = { + asteroids: [], + planets: [], + ship: null, + sun: null, + base: null, + targets: [] + }; + + for (const mesh of scene.meshes) { + const scriptName = getScriptName(mesh); + categorizeByScript(scriptName, mesh, result); + } + + collectTargetNodes(scene, result); + return result; +} + +function categorizeByScript( + scriptName: string | null, + mesh: AbstractMesh, + result: CollectedMeshes +): void { + switch (scriptName) { + case "AsteroidComponent": + result.asteroids.push(mesh); + break; + case "PlanetComponent": + result.planets.push(mesh); + break; + case "ShipComponent": + result.ship = mesh; + break; + case "SunComponent": + result.sun = mesh; + break; + case "BaseComponent": + result.base = mesh; + break; + } +} + +function collectTargetNodes(scene: Scene, result: CollectedMeshes): void { + for (const node of scene.transformNodes) { + if (getScriptName(node) === "TargetComponent") { + result.targets.push(node); + } + } +} + diff --git a/bjsEditorPlugin/src/panelSections.ts b/bjsEditorPlugin/src/panelSections.ts new file mode 100644 index 0000000..2cafed1 --- /dev/null +++ b/bjsEditorPlugin/src/panelSections.ts @@ -0,0 +1,72 @@ +/** + * Panel section creators for the floating UI + */ +import { createButton, createNumberInput, createSeparator, createLabel, createRow } from "./uiComponents"; +import { FloatingUICallbacks } from "./floatingUI"; +import { createAuthSection } from "./levelBrowser/authStatus"; + +export async function createContent(callbacks: FloatingUICallbacks): Promise { + const content = document.createElement("div"); + Object.assign(content.style, { padding: "12px", display: "flex", flexDirection: "column", gap: "8px" }); + + content.appendChild(createButton("Export Level", callbacks.onExport)); + content.appendChild(createButton("Copy to Clipboard", callbacks.onExportClipboard)); + content.appendChild(createSeparator()); + content.appendChild(createScaleSection(callbacks.onApplyUniformScale)); + content.appendChild(createSeparator()); + content.appendChild(createCameraSpeedSection(callbacks)); + content.appendChild(createSeparator()); + content.appendChild(await createCloudLevelsSection(callbacks)); + + return content; +} + +async function createCloudLevelsSection(callbacks: FloatingUICallbacks): Promise { + const section = document.createElement("div"); + Object.assign(section.style, { display: "flex", flexDirection: "column", gap: "6px" }); + + section.appendChild(createLabel("Cloud Levels")); + section.appendChild(await createAuthSection(callbacks.onAuthChange)); + + const row = createRow(); + const officialBtn = createButton("Official", callbacks.onBrowseOfficial); + const myBtn = createButton("My Levels", callbacks.onBrowseMyLevels); + officialBtn.style.flex = "1"; + myBtn.style.flex = "1"; + row.appendChild(officialBtn); + row.appendChild(myBtn); + section.appendChild(row); + + return section; +} + +function createScaleSection(onApply: (scale: number) => void): HTMLDivElement { + const row = createRow(); + row.style.width = "100%"; + + const input = createNumberInput(1, "0.1", "0.1"); + const btn = createButton("Scale", () => onApply(parseFloat(input.value) || 1)); + btn.style.padding = "6px 12px"; + btn.style.flexShrink = "0"; + + row.appendChild(input); + row.appendChild(btn); + return row; +} + +function createCameraSpeedSection(callbacks: FloatingUICallbacks): HTMLDivElement { + const section = document.createElement("div"); + Object.assign(section.style, { display: "flex", flexDirection: "column", gap: "6px" }); + + const row = createRow(); + const input = createNumberInput(callbacks.getCameraSpeed(), "0.5", "0.5"); + const btn = createButton("Set", () => callbacks.onCameraSpeedChange(parseFloat(input.value) || 1)); + btn.style.padding = "6px 12px"; + btn.style.flexShrink = "0"; + + row.appendChild(input); + row.appendChild(btn); + section.appendChild(createLabel("Camera Speed")); + section.appendChild(row); + return section; +} diff --git a/bjsEditorPlugin/src/scriptUtils.ts b/bjsEditorPlugin/src/scriptUtils.ts new file mode 100644 index 0000000..227e1cb --- /dev/null +++ b/bjsEditorPlugin/src/scriptUtils.ts @@ -0,0 +1,53 @@ +/** + * Utilities for reading BabylonJS Editor script metadata + * Editor stores scripts in: mesh.metadata.scripts[].values[prop].value + */ +import { AbstractMesh } from "@babylonjs/core/Meshes/abstractMesh"; +import { TransformNode } from "@babylonjs/core/Meshes/transformNode"; + +interface EditorScript { + key: string; + enabled?: boolean; + values?: Record; +} + +type NodeWithMetadata = AbstractMesh | TransformNode; + +/** + * Extract component name from node's attached scripts + * e.g., "scenes/scripts/AsteroidComponent.ts" -> "AsteroidComponent" + */ +export function getScriptName(node: NodeWithMetadata): string | null { + const scripts = getScriptsArray(node); + if (!scripts.length) return null; + + const script = scripts.find(s => s.enabled !== false); + if (!script?.key) return null; + + const filename = script.key.split("/").pop() ?? ""; + return filename.replace(/\.(ts|tsx|js)$/, "") || null; +} + +/** + * Extract flattened property values from node's script + * Converts { prop: { value: x } } to { prop: x } + */ +export function getScriptValues(node: NodeWithMetadata): Record { + const scripts = getScriptsArray(node); + if (!scripts.length) return {}; + + const script = scripts.find(s => s.enabled !== false); + if (!script?.values) return {}; + + const result: Record = {}; + for (const [key, data] of Object.entries(script.values)) { + result[key] = data?.value; + } + return result; +} + +function getScriptsArray(node: NodeWithMetadata): EditorScript[] { + const metadata = node.metadata as { scripts?: EditorScript[] } | null; + const scripts = metadata?.scripts; + return Array.isArray(scripts) ? scripts : []; +} diff --git a/bjsEditorPlugin/src/services/pluginAuth.ts b/bjsEditorPlugin/src/services/pluginAuth.ts new file mode 100644 index 0000000..9ae15a4 --- /dev/null +++ b/bjsEditorPlugin/src/services/pluginAuth.ts @@ -0,0 +1,67 @@ +/** + * Manual token-based auth for Electron plugin + * User generates token on website and pastes it here + */ +import { PLUGIN_CONFIG } from "../config"; +import { showTokenEntryModal, closeTokenEntryModal } from "../levelBrowser/tokenEntryModal"; + +const STORAGE_KEY = "plugin_auth_token"; + +interface StoredAuth { + accessToken: string; + savedAt: number; +} + +let currentAuth: StoredAuth | null = null; + +export async function initAuth(): Promise { + const stored = localStorage.getItem(STORAGE_KEY); + if (stored) { + try { + currentAuth = JSON.parse(stored) as StoredAuth; + } catch { /* ignore invalid stored data */ } + } +} + +export async function login(): Promise { + return new Promise((resolve, reject) => { + showTokenEntryModal( + `${PLUGIN_CONFIG.WEBSITE_URL}/profile`, + (token: string) => { + if (token.trim()) { + storeAuth(token.trim()); + closeTokenEntryModal(); + resolve(); + } else { + reject(new Error("No token provided")); + } + }, + () => { + closeTokenEntryModal(); + reject(new Error("Login cancelled")); + } + ); + }); +} + +export async function logout(): Promise { + currentAuth = null; + localStorage.removeItem(STORAGE_KEY); +} + +export function isAuthenticated(): boolean { + return currentAuth !== null && !!currentAuth.accessToken; +} + +export function getAccessToken(): string | undefined { + if (!currentAuth) return undefined; + return currentAuth.accessToken; +} + +function storeAuth(token: string): void { + currentAuth = { + accessToken: token, + savedAt: Date.now(), + }; + localStorage.setItem(STORAGE_KEY, JSON.stringify(currentAuth)); +} diff --git a/bjsEditorPlugin/src/services/pluginLevelService.ts b/bjsEditorPlugin/src/services/pluginLevelService.ts new file mode 100644 index 0000000..040d011 --- /dev/null +++ b/bjsEditorPlugin/src/services/pluginLevelService.ts @@ -0,0 +1,101 @@ +/** + * Level service for fetching levels from Supabase + */ +import { getSupabaseClient } from "./pluginSupabase"; +import { getAccessToken } from "./pluginAuth"; +import type { LevelConfig } from "../types"; + +export interface CloudLevelEntry { + id: string; + name: string; + description: string | null; + difficulty: string; + levelType: "official" | "private" | "pending_review" | "published" | "rejected"; + config: LevelConfig; + updatedAt: string; +} + +interface LevelRow { + id: string; + name: string; + description: string | null; + difficulty: string; + level_type: string; + config: LevelConfig | string; + updated_at: string; +} + +function rowToEntry(row: LevelRow): CloudLevelEntry { + const config = typeof row.config === "string" ? JSON.parse(row.config) : row.config; + return { + id: row.id, + name: row.name, + description: row.description, + difficulty: row.difficulty, + levelType: row.level_type as CloudLevelEntry["levelType"], + config, + updatedAt: row.updated_at, + }; +} + +export async function getOfficialLevels(): Promise { + const client = getSupabaseClient(); + const { data, error } = await client + .from("levels") + .select("id, name, description, difficulty, level_type, config, updated_at") + .eq("level_type", "official") + .order("sort_order", { ascending: true }); + + if (error || !data) return []; + return data.map(rowToEntry); +} + +export async function getPublishedLevels(): Promise { + const { data, error } = await getSupabaseClient() + .from("levels") + .select("id, name, description, difficulty, level_type, config, updated_at") + .eq("level_type", "published") + .order("created_at", { ascending: false }); + + if (error || !data) return []; + return data.map(rowToEntry); +} + +export async function getMyLevels(): Promise { + const token = getAccessToken(); + if (!token) return []; + + const { data, error } = await getSupabaseClient() + .rpc("get_my_levels_by_token", { p_token: token }); + + if (error || !data) { + console.error("Failed to fetch my levels:", error); + return []; + } + return data.map(rowToEntry); +} + +export async function saveLevel( + name: string, + difficulty: string, + config: LevelConfig, + levelId?: string +): Promise { + const token = getAccessToken(); + if (!token) return null; + + const { data, error } = await getSupabaseClient() + .rpc("save_level_by_token", { + p_token: token, + p_name: name, + p_difficulty: difficulty, + p_config: config, + p_level_id: levelId || null, + }); + + if (error) { + console.error("Failed to save level:", error); + return null; + } + return data; +} diff --git a/bjsEditorPlugin/src/services/pluginSupabase.ts b/bjsEditorPlugin/src/services/pluginSupabase.ts new file mode 100644 index 0000000..d705a98 --- /dev/null +++ b/bjsEditorPlugin/src/services/pluginSupabase.ts @@ -0,0 +1,14 @@ +/** + * Lightweight Supabase client for plugin context + */ +import { createClient, SupabaseClient } from "@supabase/supabase-js"; +import { PLUGIN_CONFIG } from "../config"; + +let client: SupabaseClient | null = null; + +export function getSupabaseClient(): SupabaseClient { + if (!client) { + client = createClient(PLUGIN_CONFIG.SUPABASE_URL, PLUGIN_CONFIG.SUPABASE_ANON_KEY); + } + return client; +} diff --git a/bjsEditorPlugin/src/types.ts b/bjsEditorPlugin/src/types.ts new file mode 100644 index 0000000..5b11e8c --- /dev/null +++ b/bjsEditorPlugin/src/types.ts @@ -0,0 +1,70 @@ +/** + * LevelConfig types - mirror of the game's level config schema + */ +export type Vector3Array = [number, number, number]; + +export interface ShipConfig { + position: Vector3Array; + rotation?: Vector3Array; + linearVelocity?: Vector3Array; + angularVelocity?: Vector3Array; +} + +export interface SunConfig { + position: Vector3Array; + rotation?: Vector3Array; + diameter: number; + intensity?: number; + scale?: Vector3Array; +} + +export interface StartBaseConfig { + position?: Vector3Array; + rotation?: Vector3Array; + baseGlbPath?: string; + landingGlbPath?: string; +} + +export interface TargetConfig { + id: string; + name: string; + position: Vector3Array; +} + +export interface PlanetConfig { + name: string; + position: Vector3Array; + diameter: number; + texturePath: string; + rotation?: Vector3Array; +} + +export interface AsteroidConfig { + id: string; + position: Vector3Array; + rotation?: Vector3Array; + scale: number; + linearVelocity: Vector3Array; + angularVelocity?: Vector3Array; + mass?: number; + targetId?: string; + targetMode?: 'orbit' | 'moveToward'; +} + +export interface LevelConfig { + version: string; + difficulty: string; + timestamp?: string; + metadata?: { + author?: string; + description?: string; + [key: string]: unknown; + }; + ship: ShipConfig; + startBase?: StartBaseConfig; + sun: SunConfig; + targets?: TargetConfig[]; + planets: PlanetConfig[]; + asteroids: AsteroidConfig[]; + useOrbitConstraints?: boolean; +} diff --git a/bjsEditorPlugin/src/uiComponents.ts b/bjsEditorPlugin/src/uiComponents.ts new file mode 100644 index 0000000..069b570 --- /dev/null +++ b/bjsEditorPlugin/src/uiComponents.ts @@ -0,0 +1,65 @@ +/** + * Reusable UI components for the floating panel + */ + +export function createButton(text: string, onClick: () => void): HTMLButtonElement { + const btn = document.createElement("button"); + btn.textContent = text; + Object.assign(btn.style, { + padding: "8px 12px", + backgroundColor: "#0d6efd", + color: "white", + border: "none", + borderRadius: "4px", + cursor: "pointer", + fontSize: "13px", + transition: "background-color 0.2s", + }); + + btn.addEventListener("mouseenter", () => (btn.style.backgroundColor = "#0b5ed7")); + btn.addEventListener("mouseleave", () => (btn.style.backgroundColor = "#0d6efd")); + btn.addEventListener("click", onClick); + + return btn; +} + +export function createNumberInput(value: number, step: string, min: string): HTMLInputElement { + const input = document.createElement("input"); + input.type = "number"; + input.value = String(value); + input.step = step; + input.min = min; + Object.assign(input.style, { + flex: "1", + minWidth: "0", + padding: "6px", + backgroundColor: "#2d2d2d", + border: "1px solid #3c3c3c", + borderRadius: "4px", + color: "#e0e0e0", + fontSize: "13px", + boxSizing: "border-box", + }); + return input; +} + +export function createSeparator(): HTMLDivElement { + const sep = document.createElement("div"); + sep.style.borderTop = "1px solid #3c3c3c"; + sep.style.margin = "4px 0"; + return sep; +} + +export function createLabel(text: string): HTMLLabelElement { + const label = document.createElement("label"); + label.textContent = text; + label.style.fontSize = "12px"; + label.style.color = "#aaa"; + return label; +} + +export function createRow(): HTMLDivElement { + const row = document.createElement("div"); + Object.assign(row.style, { display: "flex", gap: "8px", alignItems: "center" }); + return row; +} diff --git a/bjsEditorPlugin/src/utils.ts b/bjsEditorPlugin/src/utils.ts new file mode 100644 index 0000000..9524320 --- /dev/null +++ b/bjsEditorPlugin/src/utils.ts @@ -0,0 +1,47 @@ +/** + * Utility functions for notifications, clipboard, and file downloads + */ + +export function showNotification(message: string, isError = false): void { + const el = document.createElement("div"); + Object.assign(el.style, { + position: "fixed", + bottom: "20px", + right: "20px", + padding: "12px 20px", + backgroundColor: isError ? "#dc3545" : "#198754", + color: "white", + borderRadius: "6px", + zIndex: "10001", + fontSize: "13px", + boxShadow: "0 4px 12px rgba(0,0,0,0.3)", + }); + el.textContent = message; + document.body.appendChild(el); + setTimeout(() => el.remove(), 3000); +} + +export async function copyToClipboard(text: string): Promise { + if (navigator.clipboard?.writeText) { + await navigator.clipboard.writeText(text); + } else { + const textarea = document.createElement("textarea"); + textarea.value = text; + textarea.style.position = "fixed"; + textarea.style.opacity = "0"; + document.body.appendChild(textarea); + textarea.select(); + document.execCommand("copy"); + document.body.removeChild(textarea); + } +} + +export function downloadJson(content: string, filename: string): void { + const blob = new Blob([content], { type: "application/json" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = filename; + a.click(); + URL.revokeObjectURL(url); +} diff --git a/bjsEditorPlugin/tsconfig.json b/bjsEditorPlugin/tsconfig.json new file mode 100644 index 0000000..0fbc317 --- /dev/null +++ b/bjsEditorPlugin/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "CommonJS", + "lib": ["ES2020", "DOM"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +}