diff --git a/index.html b/index.html index 85b3fa1..5d37b40 100644 --- a/index.html +++ b/index.html @@ -14,551 +14,13 @@ - -
- + + - - - -
-
-
- - -
-

๐Ÿš€ Space Combat VR

-

- Pilot your spaceship through asteroid fields and complete missions -

-
- - -
-

Your Mission

-

- Complete levels to unlock new challenges and the level editor -

-
- -
-
- - -
- - ๐ŸŽฎ How to Play (Click to expand) - -
-
-

VR Controllers (Required for VR)

-
    -
  • Left Thumbstick: Move forward/backward and yaw left/right
  • -
  • Right Thumbstick: Pitch up/down and Roll left/right
  • -
  • Front Trigger: Fire weapon
  • -
-
-
-

Desktop Controls (Preview Mode)

-
    -
  • W/S: Move forward/backward
  • -
  • A/D: Yaw left/right
  • -
  • Arrow Up/Down: Pitch up/down
  • -
  • Arrow Left/Right: Roll left/right
  • -
  • Space: Fire weapon
  • -
-
-
-

- โš ๏ธ Note: This game is designed for VR headsets with controllers. Desktop controls are provided for preview and testing purposes only. -

-
- - - -
-
-
- - -
-
- โ† Back to Game - -

๐Ÿš€ Level Editor

-

Configure and generate custom level configurations

- -
-

Difficulty Presets

-
- - - - - - -
-
- -
- -
-

โš™๏ธ Basic Settings

-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-

๐Ÿš€ Ship

-
- -
-
-
X
- -
-
-
Y
- -
-
-
Z
- -
-
-
-
- - -
-

๐ŸŽฏ Start Base

-
- -
-
-
X
- -
-
-
Y
- -
-
-
Z
- -
-
-
-
- - -
-
- - -
-

โ˜€๏ธ Sun

-
- -
-
-
X
- -
-
-
Y
- -
-
-
Z
- -
-
-
-
- - -
-
- - -
-

๐Ÿช Planets

-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-

โ˜„๏ธ Asteroids

-
- - -
-
- - -
Controls asteroid speed
-
-
- - -
-
- - -
-
- - -
Distance from start base
-
-
- - -
-
-
- -
- - - -
- -
-

๐Ÿ’พ Saved Levels

-
-
- - -
-
- - -
-
- โ† Back to Game - -

๐ŸŽฎ Controller Mapping

-

Customize VR controller button and stick mappings

- -
- -
-

๐Ÿ•น๏ธ Left Stick

-

- Configure what actions the left thumbstick controls. -

- -
- - - -
- -
- - - -
-
- - -
-

๐Ÿ•น๏ธ Right Stick

-

- Configure what actions the right thumbstick controls. -

- -
- - - -
- -
- - - -
-
- - -
-

๐Ÿ”˜ Button Mappings

-

- Configure what actions each controller button performs. -

- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
-
- - -
-

โ„น๏ธ Action Guide

-
-

Yaw: Turn left/right (rotate around vertical axis)

-

Pitch: Nose up/down (rotate around horizontal axis)

-

Roll: Barrel roll (rotate around forward axis)

-

Forward: Forward and backward thrust

-

None: No action assigned

-
-
- - -
-

๐Ÿ’พ Storage Info

-
-

Controller mappings are automatically saved to your browser's local storage and will persist between sessions.

-

- โš ๏ธ Note: Changes will take effect when you start a new level. Restart the current level to see changes. -

-
-
-
- -
- - - -
- -
-
-
- - -
-
- โ† Back to Game - -

โš™๏ธ Game Settings

-

Configure graphics quality and physics settings

- -
- -
-

โš›๏ธ Physics

-

- Disabling physics can significantly improve performance but will prevent gameplay. -

- -
- -
- Required for collisions, shooting, and asteroid movement. Disabling this will prevent gameplay but may help with debugging or viewing the scene. -
-
-
- - -
-

๐Ÿ› Developer

-

- Enable debug logging to console for troubleshooting and development. -

- -
- -
- When enabled, debug messages will be shown in the browser console. Useful for development and troubleshooting issues. -
-
-
- - -
-

๐Ÿš€ Ship Physics

-

- Advanced tuning parameters for ship movement and handling. Adjust these to customize how the ship responds to controls. -

- -
- - -
- Maximum forward/backward speed of the ship. Higher values allow faster movement. -
-
- -
- - -
- Maximum rotation speed of the ship. Higher values allow faster turning. -
-
- -
- - -
- Acceleration power for forward/backward thrust. Higher values = faster acceleration. -
-
- -
- - -
- Torque power for rotation. Higher values = faster rotational acceleration. -
-
-
- - -
-

โ„น๏ธ Quality Level Guide

-
-

Wireframe: Minimal rendering, shows mesh structure only. Best for debugging or very low-end devices.

-

Simple Material: Basic solid colors without textures. Good performance with basic visuals.

-

Full Texture: Standard textures with procedural generation. Recommended for most users.

-

PBR Texture: Physically-based rendering with enhanced materials. Best visual quality but higher GPU usage.

-
-
- - -
-

๐Ÿ’พ Storage Info

-
-

Settings are automatically saved to your browser's local storage and will persist between sessions.

-

- โš ๏ธ Note: Changes will take effect when you start a new level. Restart the current level to see changes. -

-
-
-
- -
- - -
- -
-
-
+ +
+ diff --git a/index.html.backup b/index.html.backup new file mode 100644 index 0000000..85b3fa1 --- /dev/null +++ b/index.html.backup @@ -0,0 +1,564 @@ + + + + + + + Space Game + + + + +
+ + + + + +
+
+
+ + +
+

๐Ÿš€ Space Combat VR

+

+ Pilot your spaceship through asteroid fields and complete missions +

+
+ + +
+

Your Mission

+

+ Complete levels to unlock new challenges and the level editor +

+
+ +
+
+ + +
+ + ๐ŸŽฎ How to Play (Click to expand) + +
+
+

VR Controllers (Required for VR)

+
    +
  • Left Thumbstick: Move forward/backward and yaw left/right
  • +
  • Right Thumbstick: Pitch up/down and Roll left/right
  • +
  • Front Trigger: Fire weapon
  • +
+
+
+

Desktop Controls (Preview Mode)

+
    +
  • W/S: Move forward/backward
  • +
  • A/D: Yaw left/right
  • +
  • Arrow Up/Down: Pitch up/down
  • +
  • Arrow Left/Right: Roll left/right
  • +
  • Space: Fire weapon
  • +
+
+
+

+ โš ๏ธ Note: This game is designed for VR headsets with controllers. Desktop controls are provided for preview and testing purposes only. +

+
+ + + +
+
+
+ + +
+
+ โ† Back to Game + +

๐Ÿš€ Level Editor

+

Configure and generate custom level configurations

+ +
+

Difficulty Presets

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

โš™๏ธ Basic Settings

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

๐Ÿš€ Ship

+
+ +
+
+
X
+ +
+
+
Y
+ +
+
+
Z
+ +
+
+
+
+ + +
+

๐ŸŽฏ Start Base

+
+ +
+
+
X
+ +
+
+
Y
+ +
+
+
Z
+ +
+
+
+
+ + +
+
+ + +
+

โ˜€๏ธ Sun

+
+ +
+
+
X
+ +
+
+
Y
+ +
+
+
Z
+ +
+
+
+
+ + +
+
+ + +
+

๐Ÿช Planets

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

โ˜„๏ธ Asteroids

+
+ + +
+
+ + +
Controls asteroid speed
+
+
+ + +
+
+ + +
+
+ + +
Distance from start base
+
+
+ + +
+
+
+ +
+ + + +
+ +
+

๐Ÿ’พ Saved Levels

+
+
+ + +
+
+ + +
+
+ โ† Back to Game + +

๐ŸŽฎ Controller Mapping

+

Customize VR controller button and stick mappings

+ +
+ +
+

๐Ÿ•น๏ธ Left Stick

+

+ Configure what actions the left thumbstick controls. +

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

๐Ÿ•น๏ธ Right Stick

+

+ Configure what actions the right thumbstick controls. +

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

๐Ÿ”˜ Button Mappings

+

+ Configure what actions each controller button performs. +

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

โ„น๏ธ Action Guide

+
+

Yaw: Turn left/right (rotate around vertical axis)

+

Pitch: Nose up/down (rotate around horizontal axis)

+

Roll: Barrel roll (rotate around forward axis)

+

Forward: Forward and backward thrust

+

None: No action assigned

+
+
+ + +
+

๐Ÿ’พ Storage Info

+
+

Controller mappings are automatically saved to your browser's local storage and will persist between sessions.

+

+ โš ๏ธ Note: Changes will take effect when you start a new level. Restart the current level to see changes. +

+
+
+
+ +
+ + + +
+ +
+
+
+ + +
+
+ โ† Back to Game + +

โš™๏ธ Game Settings

+

Configure graphics quality and physics settings

+ +
+ +
+

โš›๏ธ Physics

+

+ Disabling physics can significantly improve performance but will prevent gameplay. +

+ +
+ +
+ Required for collisions, shooting, and asteroid movement. Disabling this will prevent gameplay but may help with debugging or viewing the scene. +
+
+
+ + +
+

๐Ÿ› Developer

+

+ Enable debug logging to console for troubleshooting and development. +

+ +
+ +
+ When enabled, debug messages will be shown in the browser console. Useful for development and troubleshooting issues. +
+
+
+ + +
+

๐Ÿš€ Ship Physics

+

+ Advanced tuning parameters for ship movement and handling. Adjust these to customize how the ship responds to controls. +

+ +
+ + +
+ Maximum forward/backward speed of the ship. Higher values allow faster movement. +
+
+ +
+ + +
+ Maximum rotation speed of the ship. Higher values allow faster turning. +
+
+ +
+ + +
+ Acceleration power for forward/backward thrust. Higher values = faster acceleration. +
+
+ +
+ + +
+ Torque power for rotation. Higher values = faster rotational acceleration. +
+
+
+ + +
+

โ„น๏ธ Quality Level Guide

+
+

Wireframe: Minimal rendering, shows mesh structure only. Best for debugging or very low-end devices.

+

Simple Material: Basic solid colors without textures. Good performance with basic visuals.

+

Full Texture: Standard textures with procedural generation. Recommended for most users.

+

PBR Texture: Physically-based rendering with enhanced materials. Best visual quality but higher GPU usage.

+
+
+ + +
+

๐Ÿ’พ Storage Info

+
+

Settings are automatically saved to your browser's local storage and will persist between sessions.

+

+ โš ๏ธ Note: Changes will take effect when you start a new level. Restart the current level to see changes. +

+
+
+
+ +
+ + +
+ +
+
+
+ + + + diff --git a/package-lock.json b/package-lock.json index fd31936..ca0c1f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,10 +18,14 @@ "@babylonjs/procedural-textures": "8.36.1", "@babylonjs/serializers": "8.36.1", "@newrelic/browser-agent": "^1.302.0", - "openai": "4.52.3" + "openai": "4.52.3", + "svelte-spa-router": "^4.0.1" }, "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^6.2.1", "@types/node": "^20.0.0", + "svelte": "^5.43.14", + "svelte-preprocess": "^6.0.3", "tsx": "^4.7.1", "typescript": "^5.5.3", "vite": "^7.2.2" @@ -591,6 +595,56 @@ "node": ">=6" } }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@newrelic/browser-agent": { "version": "1.302.0", "resolved": "https://registry.npmjs.org/@newrelic/browser-agent/-/browser-agent-1.302.0.tgz", @@ -931,6 +985,55 @@ "win32" ] }, + "node_modules/@sveltejs/acorn-typescript": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.7.tgz", + "integrity": "sha512-znp1A/Y1Jj4l/Zy7PX5DZKBE0ZNY+5QBngiE21NJkfSTyzzC5iKNWOtwFXKtIrn7MXEFBck4jD95iBNkGjK92Q==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^8.9.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-6.2.1.tgz", + "integrity": "sha512-YZs/OSKOQAQCnJvM/P+F1URotNnYNeU3P2s4oIpzm1uFaqUEqRxUB0g5ejMjEb5Gjb9/PiBI5Ktrq4rUUF8UVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", + "debug": "^4.4.1", + "deepmerge": "^4.3.1", + "magic-string": "^0.30.17", + "vitefu": "^1.1.1" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24" + }, + "peerDependencies": { + "svelte": "^5.0.0", + "vite": "^6.3.0 || ^7.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte-inspector": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-5.0.1.tgz", + "integrity": "sha512-ubWshlMk4bc8mkwWbg6vNvCeT7lGQojE3ijDh3QTR6Zr/R+GXxsGbyH4PExEPpiFmqPhYiVSVmHBjUcVc1JIrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.1" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24" + }, + "peerDependencies": { + "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", + "svelte": "^5.0.0", + "vite": "^6.3.0 || ^7.0.0" + } + }, "node_modules/@types/css-font-loading-module": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/@types/css-font-loading-module/-/css-font-loading-module-0.0.7.tgz", @@ -998,6 +1101,19 @@ "node": ">=6.5" } }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/agentkeepalive": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", @@ -1009,11 +1125,31 @@ "node": ">= 8.0.0" } }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/babylonjs-gltf2interface": { "version": "8.32.1", "resolved": "https://registry.npmjs.org/babylonjs-gltf2interface/-/babylonjs-gltf2interface-8.32.1.tgz", @@ -1037,6 +1173,16 @@ "lodash": ">=4.17.21" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -1054,6 +1200,34 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "peer": true }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -1116,6 +1290,23 @@ "@esbuild/win32-x64": "0.25.12" } }, + "node_modules/esm-env": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esrap": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.1.3.tgz", + "integrity": "sha512-T/Dhhv/QH+yYmiaLz9SA3PW+YyenlnRKDNdtlYJrSOBmNsH4nvPux+mTwx7p+wAedlJrGoZtXNI0a0MjQ2QkVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + } + }, "node_modules/event-target-shim": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", @@ -1218,11 +1409,38 @@ "ms": "^2.0.0" } }, + "node_modules/is-reference": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.6" + } + }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -1381,6 +1599,15 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/regexparam": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/regexparam/-/regexparam-2.0.2.tgz", + "integrity": "sha512-A1PeDEYMrkLrfyOwv2jwihXbo9qxdGD3atBYQA9JJgreAx8/7rC6IUkWOw2NQlOxLp2wL0ifQbh1HuidDfYA6w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", @@ -1439,6 +1666,100 @@ "node": ">=0.10.0" } }, + "node_modules/svelte": { + "version": "5.43.14", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.43.14.tgz", + "integrity": "sha512-pHeUrp1A5S6RGaXhJB7PtYjL1VVjbVrJ2EfuAoPu9/1LeoMaJa/pcdCsCSb0gS4eUHAHnhCbUDxORZyvGK6kOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@sveltejs/acorn-typescript": "^1.0.5", + "@types/estree": "^1.0.5", + "acorn": "^8.12.1", + "aria-query": "^5.3.1", + "axobject-query": "^4.1.0", + "clsx": "^2.1.1", + "esm-env": "^1.2.1", + "esrap": "^2.1.0", + "is-reference": "^3.0.3", + "locate-character": "^3.0.0", + "magic-string": "^0.30.11", + "zimmerframe": "^1.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/svelte-preprocess": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-6.0.3.tgz", + "integrity": "sha512-PLG2k05qHdhmRG7zR/dyo5qKvakhm8IJ+hD2eFRQmMLHp7X3eJnjeupUtvuRpbNiF31RjVw45W+abDwHEmP5OA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.10.2", + "coffeescript": "^2.5.1", + "less": "^3.11.3 || ^4.0.0", + "postcss": "^7 || ^8", + "postcss-load-config": ">=3", + "pug": "^3.0.0", + "sass": "^1.26.8", + "stylus": ">=0.55", + "sugarss": "^2.0.0 || ^3.0.0 || ^4.0.0", + "svelte": "^4.0.0 || ^5.0.0-next.100 || ^5.0.0", + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "coffeescript": { + "optional": true + }, + "less": { + "optional": true + }, + "postcss": { + "optional": true + }, + "postcss-load-config": { + "optional": true + }, + "pug": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/svelte-spa-router": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/svelte-spa-router/-/svelte-spa-router-4.0.1.tgz", + "integrity": "sha512-2JkmUQ2f9jRluijL58LtdQBIpynSbem2eBGp4zXdi7aDY1znbR6yjw0KsonD0aq2QLwf4Yx4tBJQjxIjgjXHKg==", + "license": "MIT", + "dependencies": { + "regexparam": "2.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ItalyPaleAle" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -1571,6 +1892,26 @@ } } }, + "node_modules/vitefu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.1.tgz", + "integrity": "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==", + "dev": true, + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*", + "tests/projects/workspace/packages/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, "node_modules/web-streams-polyfill": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", @@ -1611,6 +1952,13 @@ "engines": { "node": ">= 14.6" } + }, + "node_modules/zimmerframe": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", + "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", + "dev": true, + "license": "MIT" } } } diff --git a/package.json b/package.json index 387b095..02884d4 100644 --- a/package.json +++ b/package.json @@ -24,11 +24,15 @@ "@babylonjs/materials": "8.36.1", "@babylonjs/procedural-textures": "8.36.1", "@babylonjs/serializers": "8.36.1", + "@newrelic/browser-agent": "^1.302.0", "openai": "4.52.3", - "@newrelic/browser-agent": "^1.302.0" -}, + "svelte-spa-router": "^4.0.1" + }, "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^6.2.1", "@types/node": "^20.0.0", + "svelte": "^5.43.14", + "svelte-preprocess": "^6.0.3", "tsx": "^4.7.1", "typescript": "^5.5.3", "vite": "^7.2.2" diff --git a/public/levels/directory.json b/public/levels/directory.json index 9fa1be9..d9aef16 100644 --- a/public/levels/directory.json +++ b/public/levels/directory.json @@ -1,5 +1,5 @@ { - "version": "1.0.5", + "version": "1.0.6", "levels": [ { "id": "rookie-training", diff --git a/public/levels/rookie-training.json b/public/levels/rookie-training.json index 01fc7a0..0694a9f 100644 --- a/public/levels/rookie-training.json +++ b/public/levels/rookie-training.json @@ -11,12 +11,12 @@ "ship": { "position": [ 0, - 1, - 0 + 1.5, + 500 ], "rotation": [ 0, - 0, + 180, 0 ], "linearVelocity": [ diff --git a/server/voices.ts b/server/voices.ts deleted file mode 100644 index 8356019..0000000 --- a/server/voices.ts +++ /dev/null @@ -1,14 +0,0 @@ -import OpenAI from "openai"; -import * as fs from "fs"; - - -async function build() { - const client = new OpenAI({ apiKey: ""}) - const mp3 = await client.audio.speech.create({ - model: 'tts-1-hd', - voice: 'alloy', - input: 'test 1 2 3' - }); - const buffer = Buffer.from(await mp3.arrayBuffer()); - await fs.promises.writeFile('./output.mp3', buffer); -} diff --git a/src/components/auth/UserProfile.svelte b/src/components/auth/UserProfile.svelte new file mode 100644 index 0000000..f9b1113 --- /dev/null +++ b/src/components/auth/UserProfile.svelte @@ -0,0 +1,49 @@ + + +
+ {#if $authStore.isLoading} + Loading... + {:else if $authStore.isAuthenticated && $authStore.user} +
+ {$authStore.user.name || $authStore.user.email} + +
+ {:else} + + {/if} +
+ + diff --git a/src/components/controls/ControlsScreen.svelte b/src/components/controls/ControlsScreen.svelte new file mode 100644 index 0000000..44e728a --- /dev/null +++ b/src/components/controls/ControlsScreen.svelte @@ -0,0 +1,176 @@ + + +
+ โ† Back to Game + +

๐ŸŽฎ Controller Mapping

+

Customize VR controller button and stick mappings

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

Yaw: Turn left/right (rotate around vertical axis)

+

Pitch: Nose up/down (rotate around horizontal axis)

+

Roll: Barrel roll (rotate around forward axis)

+

Forward: Forward and backward thrust

+

None: No action assigned

+
+
+ + +
+
+

Controller mappings are automatically saved to your browser's local storage and will persist between sessions.

+

+ โš ๏ธ Note: Changes will take effect when you start a new level. Restart the current level to see changes. +

+
+
+
+ +
+ + + +
+ + +
+ + diff --git a/src/components/editor/LevelEditor.svelte b/src/components/editor/LevelEditor.svelte new file mode 100644 index 0000000..0c10cff --- /dev/null +++ b/src/components/editor/LevelEditor.svelte @@ -0,0 +1,42 @@ + + +
+ โ† Back to Game + +

๐Ÿ“ Level Editor

+

Create and customize your own asteroid field levels

+ +
+

The level editor is being migrated to Svelte.

+

This component will include:

+ +
+ +
+ +
+
+ + diff --git a/src/components/game/LevelCard.svelte b/src/components/game/LevelCard.svelte new file mode 100644 index 0000000..26f5e7b --- /dev/null +++ b/src/components/game/LevelCard.svelte @@ -0,0 +1,144 @@ + + +
+
+

{directoryEntry.name}

+ {#if !isUnlocked} +
๐Ÿ”’
+ {/if} + {#if !isDefault} +
CUSTOM
+ {/if} +
+ +
+ Difficulty: {directoryEntry.difficulty || 'unknown'} + {#if directoryEntry.estimatedTime} + โ€ข {directoryEntry.estimatedTime} + {/if} +
+ +

{directoryEntry.description}

+ + {#if !isUnlocked && lockReason} +
{lockReason}
+ {/if} + +
+ + + {#if !isDefault && isUnlocked} + + {/if} +
+
+ + diff --git a/src/components/game/LevelSelect.svelte b/src/components/game/LevelSelect.svelte new file mode 100644 index 0000000..916513c --- /dev/null +++ b/src/components/game/LevelSelect.svelte @@ -0,0 +1,94 @@ + + +
+ + +
+ +
+

๐Ÿš€ Space Combat VR

+

+ Pilot your spaceship through asteroid fields and complete missions +

+
+ +
+

Your Mission

+

+ Choose your level and prepare for launch +

+ +
+ {#if !isReady} +
Loading levels...
+ {:else if defaultLevels.size === 0} +
+

No Levels Found

+

No levels available. Please check your installation.

+
+ {:else} + {#each DEFAULT_LEVEL_ORDER as levelId} + {@const entry = defaultLevels.get(levelId)} + {#if entry} + + {/if} + {/each} + + {#if customLevels.size > 0} +
+

Custom Levels

+
+ + {#each Array.from(customLevels.entries()) as [levelId, entry]} + + {/each} + {/if} + {/if} +
+ + + + +
+
+
+ + diff --git a/src/components/game/ProgressBar.svelte b/src/components/game/ProgressBar.svelte new file mode 100644 index 0000000..9e577e5 --- /dev/null +++ b/src/components/game/ProgressBar.svelte @@ -0,0 +1,17 @@ + + +{#if visible} +
+
+
+
+
+{/if} + + diff --git a/src/components/layouts/App.svelte b/src/components/layouts/App.svelte new file mode 100644 index 0000000..681074d --- /dev/null +++ b/src/components/layouts/App.svelte @@ -0,0 +1,82 @@ + + +
+ + +
+ +
+
+ + diff --git a/src/components/layouts/AppHeader.svelte b/src/components/layouts/AppHeader.svelte new file mode 100644 index 0000000..dda0869 --- /dev/null +++ b/src/components/layouts/AppHeader.svelte @@ -0,0 +1,32 @@ + + +{#if visible} +
+
+
+

Space Combat VR

+
+ +
+
+{/if} + + diff --git a/src/components/settings/SettingsScreen.svelte b/src/components/settings/SettingsScreen.svelte new file mode 100644 index 0000000..cae49eb --- /dev/null +++ b/src/components/settings/SettingsScreen.svelte @@ -0,0 +1,147 @@ + + +
+ โ† Back to Game + +

โš™๏ธ Game Settings

+

Configure graphics quality and physics settings

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

Settings are automatically saved to your browser's local storage and will persist between sessions.

+

+ โš ๏ธ Note: Changes will take effect when you start a new level. Restart the current level to see changes. +

+
+
+
+ +
+ + +
+ + +
+ + diff --git a/src/components/shared/Button.svelte b/src/components/shared/Button.svelte new file mode 100644 index 0000000..8a4e5a2 --- /dev/null +++ b/src/components/shared/Button.svelte @@ -0,0 +1,21 @@ + + + + + diff --git a/src/components/shared/Checkbox.svelte b/src/components/shared/Checkbox.svelte new file mode 100644 index 0000000..a72a461 --- /dev/null +++ b/src/components/shared/Checkbox.svelte @@ -0,0 +1,26 @@ + + + + + diff --git a/src/components/shared/FormGroup.svelte b/src/components/shared/FormGroup.svelte new file mode 100644 index 0000000..311f3d1 --- /dev/null +++ b/src/components/shared/FormGroup.svelte @@ -0,0 +1,31 @@ + + +
+ {#if label} + + {/if} + + + + {#if helpText && !error} +
{helpText}
+ {/if} + + {#if error} +
{error}
+ {/if} +
+ + diff --git a/src/components/shared/InfoBox.svelte b/src/components/shared/InfoBox.svelte new file mode 100644 index 0000000..2895ce3 --- /dev/null +++ b/src/components/shared/InfoBox.svelte @@ -0,0 +1,44 @@ + + +{#if visible && message} +
+ {message} +
+{/if} + + diff --git a/src/components/shared/NumberInput.svelte b/src/components/shared/NumberInput.svelte new file mode 100644 index 0000000..878c5a5 --- /dev/null +++ b/src/components/shared/NumberInput.svelte @@ -0,0 +1,37 @@ + + + + + diff --git a/src/components/shared/Section.svelte b/src/components/shared/Section.svelte new file mode 100644 index 0000000..9e1f3ad --- /dev/null +++ b/src/components/shared/Section.svelte @@ -0,0 +1,22 @@ + + +
+ {#if title} +

{icon}{title}

+ {/if} + + {#if description} +

{description}

+ {/if} + + +
+ + diff --git a/src/components/shared/Select.svelte b/src/components/shared/Select.svelte new file mode 100644 index 0000000..4beec19 --- /dev/null +++ b/src/components/shared/Select.svelte @@ -0,0 +1,23 @@ + + + + + diff --git a/src/components/shared/VectorInput.svelte b/src/components/shared/VectorInput.svelte new file mode 100644 index 0000000..cc2cb6b --- /dev/null +++ b/src/components/shared/VectorInput.svelte @@ -0,0 +1,44 @@ + + +
+
+ + +
+
+ + +
+
+ + +
+
+ + diff --git a/src/levels/level1.ts b/src/levels/level1.ts index 970c4b5..ab6c72d 100644 --- a/src/levels/level1.ts +++ b/src/levels/level1.ts @@ -352,7 +352,12 @@ export class Level1 implements Level { // Initialize mission brief (will be shown when entering XR) setLoadingMessage("Initializing mission brief..."); + console.log('[Level1] ========== ABOUT TO INITIALIZE MISSION BRIEF =========='); + console.log('[Level1] _missionBrief object:', this._missionBrief); + console.log('[Level1] Ship exists:', !!this._ship); + console.log('[Level1] Ship ID in scene:', DefaultScene.MainScene.getNodeById('Ship') !== null); this._missionBrief.initialize(); + console.log('[Level1] ========== MISSION BRIEF INITIALIZATION COMPLETE =========='); debugLog('Mission brief initialized'); this._initialized = true; diff --git a/src/levels/testLevel.ts b/src/levels/testLevel.ts index bf7e922..aab36b0 100644 --- a/src/levels/testLevel.ts +++ b/src/levels/testLevel.ts @@ -18,7 +18,7 @@ export class TestLevel implements Level { private _onReadyObservable: Observable = new Observable(); private _initialized: boolean = false; private _audioEngine: AudioEngineV2; - private _boxCreationInterval: NodeJS.Timeout | null = null; + private _boxCreationInterval: number | null = null; private _totalBoxesCreated: number = 0; private _boxesPerIteration: number = 1; diff --git a/src/main.ts b/src/main.ts index 551c33c..3894ae0 100644 --- a/src/main.ts +++ b/src/main.ts @@ -35,6 +35,9 @@ import {updateUserProfile} from "./ui/screens/loginScreen"; import {Preloader} from "./ui/screens/preloader"; import {DiscordWidget} from "./ui/widgets/discordWidget"; +// Svelte App +import { mount } from 'svelte'; +import App from './components/layouts/App.svelte'; import { BrowserAgent } from '@newrelic/browser-agent/loaders/browser-agent' import { AnalyticsService } from './analytics/analyticsService'; @@ -213,23 +216,43 @@ export class Main { // If we entered XR before level creation, manually setup camera parenting // (This is needed because onInitialXRPoseSetObservable won't fire if we're already in XR) + console.log('[Main] ========== CHECKING XR STATE =========='); + console.log('[Main] DefaultScene.XR exists:', !!DefaultScene.XR); + console.log('[Main] xrSession exists:', !!xrSession); + if (DefaultScene.XR) { + console.log('[Main] XR base experience state:', DefaultScene.XR.baseExperience.state); + } + if (DefaultScene.XR && xrSession && DefaultScene.XR.baseExperience.state === 2) { // WebXRState.IN_XR = 2 + console.log('[Main] ========== XR ALREADY ACTIVE - MANUAL SETUP =========='); if (ship && ship.transformNode) { + console.log('[Main] Ship and transformNode exist - parenting camera'); debugLog('Manually parenting XR camera to ship transformNode'); DefaultScene.XR.baseExperience.camera.parent = ship.transformNode; DefaultScene.XR.baseExperience.camera.position = new Vector3(0, 1.5, 0); + console.log('[Main] Camera parented successfully'); + + console.log('[Main] ========== ABOUT TO SHOW MISSION BRIEF =========='); + console.log('[Main] level1 object:', level1); + console.log('[Main] level1._missionBrief:', (level1 as any)._missionBrief); - console.log('[Main] XR already active - showing mission brief'); // Show mission brief (since onInitialXRPoseSetObservable won't fire) await level1.showMissionBrief(); - console.log('[Main] Mission brief shown, mission brief will call startGameplay() on button click'); + + console.log('[Main] ========== MISSION BRIEF SHOW() RETURNED =========='); + console.log('[Main] Mission brief will call startGameplay() when trigger is pulled'); // NOTE: Don't start timer/recording here anymore - mission brief will do it // when the user clicks the START button } else { + console.error('[Main] !!!!! SHIP OR TRANSFORM NODE NOT FOUND !!!!!'); + console.log('[Main] ship exists:', !!ship); + console.log('[Main] ship.transformNode exists:', ship ? !!ship.transformNode : 'N/A'); debugLog('WARNING: Could not parent XR camera - ship or transformNode not found'); } + } else { + console.log('[Main] XR not active yet - will use onInitialXRPoseSetObservable instead'); } // Hide preloader @@ -239,10 +262,18 @@ export class Main { }, 500); // Remove UI - mainDiv.remove(); + console.log('[Main] ========== ABOUT TO REMOVE MAIN DIV =========='); + console.log('[Main] mainDiv exists:', !!mainDiv); + console.log('[Main] Timestamp:', Date.now()); + if (mainDiv) { + mainDiv.remove(); + console.log('[Main] mainDiv removed from DOM'); + } // Start the game (XR session already active, or flat mode) + console.log('[Main] About to call this.play()'); await this.play(); + console.log('[Main] this.play() completed'); }); // Now initialize the level (after observable is registered) @@ -747,11 +778,36 @@ async function initializeApp() { await LevelRegistry.getInstance().initialize(); console.log('[Main] LevelRegistry.initialize() completed successfully [AFTER MIGRATION]'); debugLog('[Main] LevelRegistry initialized after migration'); - router.start(); + // NOTE: Old router disabled - now using svelte-spa-router + // router.start(); + + // Mount Svelte app + console.log('[Main] Mounting Svelte app [AFTER MIGRATION]'); + const appElement = document.getElementById('app'); + if (appElement) { + mount(App, { + target: appElement + }); + console.log('[Main] Svelte app mounted successfully [AFTER MIGRATION]'); + + // Create Main instance lazily only if it doesn't exist + if (!DEBUG_CONTROLLERS && !(window as any).__mainInstance) { + debugLog('[Main] Creating Main instance (not initialized) [AFTER MIGRATION]'); + const main = new Main(); + (window as any).__mainInstance = main; + + // Initialize demo mode without engine (just for UI purposes) + const demo = new Demo(main); + } + } else { + console.error('[Main] Failed to mount Svelte app - #app element not found [AFTER MIGRATION]'); + } + resolve(); } catch (error) { console.error('[Main] Failed to initialize LevelRegistry after migration:', error); - router.start(); // Start anyway to show error state + // NOTE: Old router disabled - now using svelte-spa-router + // router.start(); // Start anyway to show error state resolve(); } }); @@ -777,17 +833,41 @@ async function initializeApp() { console.log('[Main] To clear caches: window.__levelRegistry.clearAllCaches().then(() => location.reload())'); } - console.log('[Main] About to call router.start()'); - router.start(); - console.log('[Main] router.start() completed'); + // NOTE: Old router disabled - now using svelte-spa-router + // console.log('[Main] About to call router.start()'); + // router.start(); + // console.log('[Main] router.start() completed'); } catch (error) { console.error('[Main] !!!!! EXCEPTION in LevelRegistry initialization !!!!!'); console.error('[Main] Failed to initialize LevelRegistry:', error); console.error('[Main] Error stack:', error?.stack); - router.start(); // Start anyway to show error state + // NOTE: Old router disabled - now using svelte-spa-router + // router.start(); // Start anyway to show error state } } + // Mount Svelte app + console.log('[Main] Mounting Svelte app'); + const appElement = document.getElementById('app'); + if (appElement) { + mount(App, { + target: appElement + }); + console.log('[Main] Svelte app mounted successfully'); + + // Create Main instance lazily only if it doesn't exist + if (!DEBUG_CONTROLLERS && !(window as any).__mainInstance) { + debugLog('[Main] Creating Main instance (not initialized)'); + const main = new Main(); + (window as any).__mainInstance = main; + + // Initialize demo mode without engine (just for UI purposes) + const demo = new Demo(main); + } + } else { + console.error('[Main] Failed to mount Svelte app - #app element not found'); + } + console.log('[Main] initializeApp() FINISHED at', new Date().toISOString()); } diff --git a/src/services/authService.ts b/src/services/authService.ts index cb79853..1e85ab6 100644 --- a/src/services/authService.ts +++ b/src/services/authService.ts @@ -26,14 +26,22 @@ export class AuthService { * Call this early in the application lifecycle */ public async initialize(): Promise { + console.log('[AuthService] ========== INITIALIZE CALLED =========='); const domain = import.meta.env.VITE_AUTH0_DOMAIN; const clientId = import.meta.env.VITE_AUTH0_CLIENT_ID; + console.log('[AuthService] Config:', { + domain, + clientId: clientId ? clientId.substring(0, 10) + '...' : 'missing', + redirectUri: window.location.origin + }); + if (!domain || !clientId || domain.trim() === '') { - console.warn('Auth0 not configured - authentication features will be disabled'); + console.warn('[AuthService] Auth0 not configured - authentication features will be disabled'); return; } - console.log(window.location.origin); + + console.log('[AuthService] Creating Auth0 client...'); this._client = await createAuth0Client({ domain, clientId, @@ -43,33 +51,58 @@ export class AuthService { cacheLocation: 'localstorage', // Persist tokens across page reloads useRefreshTokens: true // Enable silent token refresh }); + console.log('[AuthService] Auth0 client created successfully'); // Handle redirect callback after login - if (window.location.search.includes('code=') || - window.location.search.includes('state=')) { + const hasCallback = window.location.search.includes('code=') || + window.location.search.includes('state='); + console.log('[AuthService] Checking for Auth0 callback:', hasCallback); + console.log('[AuthService] Current URL:', window.location.href); + + if (hasCallback) { + console.log('[AuthService] ========== PROCESSING AUTH0 CALLBACK =========='); try { - await this._client.handleRedirectCallback(); + const result = await this._client.handleRedirectCallback(); + console.log('[AuthService] Callback handled successfully:', result); // Clean up the URL after handling callback window.history.replaceState({}, document.title, '/'); + console.log('[AuthService] URL cleaned, redirected to home'); } catch (error) { - console.error('Error handling redirect callback:', error); + console.error('[AuthService] !!!!! CALLBACK ERROR !!!!!', error); + console.error('[AuthService] Error details:', error?.message, error?.stack); } } // Check if user is authenticated and load user info + console.log('[AuthService] Checking authentication status...'); const isAuth = await this._client.isAuthenticated(); + console.log('[AuthService] Is authenticated:', isAuth); + if (isAuth) { + console.log('[AuthService] Loading user info...'); this._user = await this._client.getUser() ?? null; + console.log('[AuthService] User loaded:', { + name: this._user?.name, + email: this._user?.email, + sub: this._user?.sub + }); + } else { + console.log('[AuthService] User not authenticated'); } + + console.log('[AuthService] ========== INITIALIZATION COMPLETE =========='); } /** * Redirect to Auth0 login page */ public async login(): Promise { + console.log('[AuthService] ========== LOGIN CALLED =========='); if (!this._client) { + console.error('[AuthService] !!!!! CLIENT NOT INITIALIZED !!!!!'); throw new Error('Auth client not initialized. Call initialize() first.'); } + console.log('[AuthService] Redirecting to Auth0 login...'); await this._client.loginWithRedirect(); } @@ -77,10 +110,13 @@ export class AuthService { * Log out the current user and redirect to home */ public async logout(): Promise { + console.log('[AuthService] ========== LOGOUT CALLED =========='); if (!this._client) { + console.error('[AuthService] !!!!! CLIENT NOT INITIALIZED !!!!!'); throw new Error('Auth client not initialized. Call initialize() first.'); } this._user = null; + console.log('[AuthService] Logging out and redirecting to:', window.location.origin); await this._client.logout({ logoutParams: { returnTo: window.location.origin diff --git a/src/ship/ship.ts b/src/ship/ship.ts index f5418d0..abbf110 100644 --- a/src/ship/ship.ts +++ b/src/ship/ship.ts @@ -446,7 +446,7 @@ export class Ship { } // Check condition 2: Stranded (outside landing zone, no fuel, low velocity) - if (!this._isInLandingZone && fuel < 0.01 && totalVelocity < 1) { + if (!this._isInLandingZone && fuel < 0.01 && totalVelocity < 5) { debugLog('Game end condition met: Stranded (no fuel, low velocity)'); this._statusScreen.show(true, false); // Game ended, not victory this._keyboardInput?.setEnabled(false); diff --git a/src/stores/auth.ts b/src/stores/auth.ts new file mode 100644 index 0000000..7abd1b2 --- /dev/null +++ b/src/stores/auth.ts @@ -0,0 +1,54 @@ +import { writable } from 'svelte/store'; +import { AuthService } from '../services/authService'; + +export interface AuthState { + isAuthenticated: boolean; + user: any | null; + isLoading: boolean; +} + +function createAuthStore() { + const authService = AuthService.getInstance(); + + const initial: AuthState = { + isAuthenticated: false, + user: null, + isLoading: true, + }; + + const { subscribe, set, update } = writable(initial); + + console.log('[AuthStore] Store created with initial state:', initial); + + // Initialize auth state - will be properly initialized after AuthService.initialize() is called + (async () => { + console.log('[AuthStore] Checking initial auth state...'); + const isAuth = await authService.isAuthenticated(); + const user = authService.getUser(); + console.log('[AuthStore] Initial auth check:', { isAuth, user: user?.name || user?.email || null }); + set({ isAuthenticated: isAuth, user, isLoading: false }); + })(); + + return { + subscribe, + login: async () => { + console.log('[AuthStore] login() called'); + await authService.login(); + // After redirect, page will reload and auth state will be refreshed + }, + logout: async () => { + console.log('[AuthStore] logout() called'); + await authService.logout(); + // After logout redirect, page will reload + }, + refresh: async () => { + console.log('[AuthStore] refresh() called'); + const isAuth = await authService.isAuthenticated(); + const user = authService.getUser(); + console.log('[AuthStore] Refreshed auth state:', { isAuth, user: user?.name || user?.email || null }); + update(state => ({ ...state, isAuthenticated: isAuth, user })); + }, + }; +} + +export const authStore = createAuthStore(); diff --git a/src/stores/controllerMapping.ts b/src/stores/controllerMapping.ts new file mode 100644 index 0000000..22167e3 --- /dev/null +++ b/src/stores/controllerMapping.ts @@ -0,0 +1,38 @@ +import { writable, get } from 'svelte/store'; +import type { ControllerMapping } from '../ship/input/controllerMapping'; +import { ControllerMappingConfig } from '../ship/input/controllerMapping'; + +const STORAGE_KEY = 'space-game-controller-mapping'; + +function createControllerMappingStore() { + const config = ControllerMappingConfig.getInstance(); + const initial = config.getMapping(); + + const { subscribe, set, update } = writable(initial); + + return { + subscribe, + update, + set: (value: ControllerMapping) => { + set(value); + config.setMapping(value); + }, + save: () => { + const mapping = get(controllerMappingStore); + config.setMapping(mapping); + config.save(); + console.log('[ControllerMapping Store] Saved'); + }, + reset: () => { + config.resetToDefault(); + config.save(); + set(config.getMapping()); + console.log('[ControllerMapping Store] Reset to defaults'); + }, + validate: () => { + return config.validate(); + }, + }; +} + +export const controllerMappingStore = createControllerMappingStore(); diff --git a/src/stores/gameConfig.ts b/src/stores/gameConfig.ts new file mode 100644 index 0000000..a941265 --- /dev/null +++ b/src/stores/gameConfig.ts @@ -0,0 +1,71 @@ +import { writable, get } from 'svelte/store'; + +const STORAGE_KEY = 'game-config'; + +export interface GameConfigData { + physicsEnabled: boolean; + debugEnabled: boolean; + progressionEnabled: boolean; + shipPhysics: { + maxLinearVelocity: number; + maxAngularVelocity: number; + linearForceMultiplier: number; + angularForceMultiplier: number; + }; +} + +const defaultConfig: GameConfigData = { + physicsEnabled: true, + debugEnabled: false, + progressionEnabled: true, + shipPhysics: { + maxLinearVelocity: 200, + maxAngularVelocity: 1.4, + linearForceMultiplier: 800, + angularForceMultiplier: 15, + }, +}; + +function loadFromStorage(): GameConfigData { + try { + const stored = localStorage.getItem(STORAGE_KEY); + if (stored) { + const parsed = JSON.parse(stored); + return { ...defaultConfig, ...parsed }; + } + } catch (error) { + console.warn('[GameConfig Store] Failed to load from localStorage:', error); + } + return { ...defaultConfig }; +} + +function createGameConfigStore() { + const initial = loadFromStorage(); + const { subscribe, set, update } = writable(initial); + + return { + subscribe, + update, + set, + save: () => { + const config = get(gameConfigStore); + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(config)); + console.log('[GameConfig Store] Saved to localStorage'); + } catch (error) { + console.error('[GameConfig Store] Failed to save:', error); + } + }, + reset: () => { + set({ ...defaultConfig }); + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(defaultConfig)); + console.log('[GameConfig Store] Reset to defaults'); + } catch (error) { + console.error('[GameConfig Store] Failed to save defaults:', error); + } + }, + }; +} + +export const gameConfigStore = createGameConfigStore(); diff --git a/src/stores/levelRegistry.ts b/src/stores/levelRegistry.ts new file mode 100644 index 0000000..a6e0fd0 --- /dev/null +++ b/src/stores/levelRegistry.ts @@ -0,0 +1,59 @@ +import { writable, get } from 'svelte/store'; +import { LevelRegistry, type LevelDirectoryEntry } from '../levels/storage/levelRegistry'; +import type { LevelConfig } from '../levels/config/levelConfig'; + +export interface LevelRegistryState { + isInitialized: boolean; + defaultLevels: Map; + customLevels: Map; +} + +function createLevelRegistryStore() { + const registry = LevelRegistry.getInstance(); + + const initial: LevelRegistryState = { + isInitialized: false, + defaultLevels: new Map(), + customLevels: new Map(), + }; + + const { subscribe, set, update } = writable(initial); + + // Initialize registry + (async () => { + await registry.initialize(); + update(state => ({ + ...state, + isInitialized: true, + defaultLevels: registry.getDefaultLevels(), + customLevels: registry.getCustomLevels(), + })); + })(); + + return { + subscribe, + getLevel: async (levelId: string): Promise => { + return await registry.getLevel(levelId); + }, + refresh: async () => { + await registry.initialize(); + update(state => ({ + ...state, + defaultLevels: registry.getDefaultLevels(), + customLevels: registry.getCustomLevels(), + })); + }, + deleteCustomLevel: (levelId: string): boolean => { + const success = registry.deleteCustomLevel(levelId); + if (success) { + update(state => ({ + ...state, + customLevels: registry.getCustomLevels(), + })); + } + return success; + }, + }; +} + +export const levelRegistryStore = createLevelRegistryStore(); diff --git a/src/stores/navigation.ts b/src/stores/navigation.ts new file mode 100644 index 0000000..93d9277 --- /dev/null +++ b/src/stores/navigation.ts @@ -0,0 +1,29 @@ +import { writable } from 'svelte/store'; + +export interface NavigationState { + currentRoute: string; + isLoading: boolean; + loadingMessage: string; +} + +function createNavigationStore() { + const initial: NavigationState = { + currentRoute: '/', + isLoading: false, + loadingMessage: '', + }; + + const { subscribe, set, update } = writable(initial); + + return { + subscribe, + setRoute: (route: string) => { + update(state => ({ ...state, currentRoute: route })); + }, + setLoading: (isLoading: boolean, message: string = '') => { + update(state => ({ ...state, isLoading, loadingMessage: message })); + }, + }; +} + +export const navigationStore = createNavigationStore(); diff --git a/src/stores/progression.ts b/src/stores/progression.ts new file mode 100644 index 0000000..e5b7112 --- /dev/null +++ b/src/stores/progression.ts @@ -0,0 +1,127 @@ +import { writable, get } from 'svelte/store'; +import { ProgressionManager, type LevelProgress } from '../game/progression'; +import { gameConfigStore } from './gameConfig'; + +interface ProgressionState { + completedLevels: Map; + editorUnlocked: boolean; + completedCount: number; + totalLevels: number; + completionPercentage: number; +} + +function createProgressionStore() { + const progression = ProgressionManager.getInstance(); + + // Create initial state from progression manager + const initialState: ProgressionState = { + completedLevels: new Map(), + editorUnlocked: progression.isEditorUnlocked(), + completedCount: progression.getCompletedCount(), + totalLevels: progression.getTotalDefaultLevels(), + completionPercentage: progression.getCompletionPercentage(), + }; + + const { subscribe, set, update } = writable(initialState); + + return { + subscribe, + + /** + * Check if a level is unlocked and can be played + * @param levelName - The name of the level (e.g., "Rookie Training") + * @param isDefault - Whether this is a default level (not custom) + * @returns true if the level is unlocked + */ + isLevelUnlocked: (levelName: string, isDefault: boolean): boolean => { + const config = get(gameConfigStore); + + // If progression is disabled, all levels are unlocked + if (!config.progressionEnabled) { + return true; + } + + // Custom levels are always unlocked + if (!isDefault) { + return true; + } + + // Check with progression manager + return progression.isLevelUnlocked(levelName); + }, + + /** + * Check if a level has been completed + */ + isLevelComplete: (levelName: string): boolean => { + return progression.isLevelComplete(levelName); + }, + + /** + * Mark a level as completed + */ + markLevelComplete: (levelName: string, stats?: { completionTime?: number; accuracy?: number }) => { + progression.markLevelComplete(levelName, stats); + + // Update store state + update(state => ({ + ...state, + completedLevels: new Map(), // Could be populated if needed + editorUnlocked: progression.isEditorUnlocked(), + completedCount: progression.getCompletedCount(), + totalLevels: progression.getTotalDefaultLevels(), + completionPercentage: progression.getCompletionPercentage(), + })); + }, + + /** + * Get the previous level name (for lock messages) + */ + getPreviousLevelName: (levelName: string): string | null => { + const defaultLevels = [ + 'Rookie Training', + 'Rescue Mission', + 'Deep Space Patrol', + 'Enemy Territory', + 'The Gauntlet', + 'Final Challenge' + ]; + + const levelIndex = defaultLevels.indexOf(levelName); + if (levelIndex > 0) { + return defaultLevels[levelIndex - 1]; + } + return null; + }, + + /** + * Check if this is the tutorial level + */ + isTutorial: (levelName: string): boolean => { + return levelName === 'Rookie Training'; + }, + + /** + * Get next level to play + */ + getNextLevel: (): string | null => { + return progression.getNextLevel(); + }, + + /** + * Reset all progression + */ + reset: () => { + progression.reset(); + update(state => ({ + ...state, + completedLevels: new Map(), + editorUnlocked: false, + completedCount: 0, + completionPercentage: 0, + })); + }, + }; +} + +export const progressionStore = createProgressionStore(); diff --git a/src/ui/hud/missionBrief.ts b/src/ui/hud/missionBrief.ts index 9215aef..06c4c8f 100644 --- a/src/ui/hud/missionBrief.ts +++ b/src/ui/hud/missionBrief.ts @@ -26,35 +26,60 @@ export class MissionBrief { * Initialize the mission brief as a fullscreen overlay */ public initialize(): void { + console.log('[MissionBrief] ========== INITIALIZE CALLED =========='); const scene = DefaultScene.MainScene; + console.log('[MissionBrief] Scene exists:', !!scene); - console.log('[MissionBrief] Initializing as fullscreen overlay'); - const mesh = MeshBuilder.CreatePlane('brief', {size: 2}); - const ship = scene.getNodeById('Ship'); - mesh.parent = ship; - mesh.position = new Vector3(0,1,2.8); - // Create fullscreen advanced texture (not attached to mesh) - this._advancedTexture = AdvancedDynamicTexture.CreateForMesh(mesh); + try { + console.log('[MissionBrief] Initializing as fullscreen overlay'); + const mesh = MeshBuilder.CreatePlane('brief', {size: 2}); + console.log('[MissionBrief] Mesh created:', mesh.name, 'ID:', mesh.id); + const ship = scene.getNodeById('Ship'); + console.log('[MissionBrief] Ship node found:', !!ship); - console.log('[MissionBrief] Fullscreen UI created'); + if (!ship) { + console.error('[MissionBrief] ERROR: Ship node not found! Cannot parent mission brief mesh.'); + return; + } - // Create main container - centered overlay - this._container = new Rectangle("missionBriefContainer"); - this._container.width = "800px"; - this._container.height = "600px"; - this._container.thickness = 4; - this._container.color = "#00ff00"; - this._container.background = "rgba(0, 0, 0, 0.95)"; - this._container.cornerRadius = 20; - this._container.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_CENTER; - this._container.verticalAlignment = Control.VERTICAL_ALIGNMENT_CENTER; - this._advancedTexture.addControl(this._container); + mesh.parent = ship; + mesh.position = new Vector3(0,1,2.8); + console.log('[MissionBrief] Mesh parented to ship at position:', mesh.position); + console.log('[MissionBrief] Mesh absolute position:', mesh.getAbsolutePosition()); + console.log('[MissionBrief] Mesh scaling:', mesh.scaling); + console.log('[MissionBrief] Mesh isEnabled:', mesh.isEnabled()); + console.log('[MissionBrief] Mesh isVisible:', mesh.isVisible); - // Initially hidden - this._container.isVisible = false; + // Create fullscreen advanced texture (not attached to mesh) + this._advancedTexture = AdvancedDynamicTexture.CreateForMesh(mesh); + console.log('[MissionBrief] AdvancedDynamicTexture created for mesh'); + console.log('[MissionBrief] Texture dimensions:', this._advancedTexture.getSize()); - console.log('[MissionBrief] Fullscreen overlay initialized'); + console.log('[MissionBrief] Fullscreen UI created'); + + // Create main container - centered overlay + this._container = new Rectangle("missionBriefContainer"); + this._container.width = "800px"; + this._container.height = "600px"; + this._container.thickness = 4; + this._container.color = "#00ff00"; + this._container.background = "rgba(0, 0, 0, 0.95)"; + this._container.cornerRadius = 20; + this._container.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_CENTER; + this._container.verticalAlignment = Control.VERTICAL_ALIGNMENT_CENTER; + this._advancedTexture.addControl(this._container); + console.log('[MissionBrief] Container created and added to texture'); + + // Initially hidden + this._container.isVisible = false; + console.log('[MissionBrief] Container initially hidden'); + + console.log('[MissionBrief] ========== INITIALIZATION COMPLETE =========='); + } catch (error) { + console.error('[MissionBrief] !!!!! INITIALIZATION FAILED !!!!!', error); + console.error('[MissionBrief] Error stack:', error?.stack); + } } /** @@ -65,12 +90,18 @@ export class MissionBrief { * @param onStart - Callback when start button is pressed */ public show(levelConfig: LevelConfig, directoryEntry: LevelDirectoryEntry | null, triggerObservable: Observable, onStart: () => void): void { + console.log('[MissionBrief] ========== SHOW() CALLED =========='); + console.log('[MissionBrief] Container exists:', !!this._container); + console.log('[MissionBrief] AdvancedTexture exists:', !!this._advancedTexture); + if (!this._container || !this._advancedTexture) { - debugLog('[MissionBrief] Cannot show - not initialized'); + console.error('[MissionBrief] !!!!! CANNOT SHOW - NOT INITIALIZED !!!!!'); + console.error('[MissionBrief] Container:', this._container); + console.error('[MissionBrief] AdvancedTexture:', this._advancedTexture); return; } - debugLog('[MissionBrief] Showing with config:', { + console.log('[MissionBrief] Showing with config:', { difficulty: levelConfig.difficulty, description: levelConfig.metadata?.description, asteroidCount: levelConfig.asteroids?.length, @@ -183,7 +214,12 @@ export class MissionBrief { this._container.isVisible = true; this._isVisible = true; - debugLog('[MissionBrief] Mission brief displayed'); + console.log('[MissionBrief] ========== CONTAINER NOW VISIBLE =========='); + console.log('[MissionBrief] Container.isVisible:', this._container.isVisible); + console.log('[MissionBrief] _isVisible flag:', this._isVisible); + console.log('[MissionBrief] Container children count:', this._container.children.length); + console.log('[MissionBrief] AdvancedTexture control count:', this._advancedTexture.rootContainer.children.length); + console.log('[MissionBrief] ========== MISSION BRIEF DISPLAY COMPLETE =========='); } /** diff --git a/src/utils/blenderExporter.ts b/src/utils/blenderExporter.ts deleted file mode 100644 index 3a3c821..0000000 --- a/src/utils/blenderExporter.ts +++ /dev/null @@ -1,301 +0,0 @@ -import { spawn } from 'child_process'; -import { platform } from 'os'; -import { existsSync, watch } from 'fs'; -import path from 'path'; - -/** - * Configuration options for Blender export - */ -export interface BlenderExportOptions { - /** - * Custom path to Blender executable (optional) - * If not provided, will use default paths for the current platform - */ - blenderPath?: string; - - /** - * Additional glTF export parameters - * See: https://docs.blender.org/api/current/bpy.ops.export_scene.html#bpy.ops.export_scene.gltf - */ - exportParams?: { - export_format?: 'GLB' | 'GLTF_SEPARATE' | 'GLTF_EMBEDDED'; - export_draco_mesh_compression_enable?: boolean; - export_texture_dir?: string; - export_apply_modifiers?: boolean; - export_yup?: boolean; - export_animations?: boolean; - export_materials?: 'EXPORT' | 'PLACEHOLDER' | 'NONE'; - [key: string]: any; - }; - - /** - * Timeout in milliseconds (default: 60000 = 1 minute) - */ - timeout?: number; -} - -/** - * Result of a Blender export operation - */ -export interface BlenderExportResult { - success: boolean; - outputPath: string; - stdout: string; - stderr: string; - duration: number; // milliseconds -} - -/** - * Get the default Blender executable path for the current platform - */ -function getDefaultBlenderPath(): string { - const os = platform(); - - switch (os) { - case 'darwin': // macOS - return '/Applications/Blender.app/Contents/MacOS/Blender'; - case 'win32': // Windows - // Try common installation paths - const windowsPaths = [ - 'C:\\Program Files\\Blender Foundation\\Blender 4.2\\blender.exe', - 'C:\\Program Files\\Blender Foundation\\Blender 4.1\\blender.exe', - 'C:\\Program Files\\Blender Foundation\\Blender 4.0\\blender.exe', - 'C:\\Program Files\\Blender Foundation\\Blender 3.6\\blender.exe', - 'C:\\Program Files\\Blender Foundation\\Blender\\blender.exe', - ]; - for (const p of windowsPaths) { - if (existsSync(p)) return p; - } - return 'blender'; // Fall back to PATH - case 'linux': - return 'blender'; // Assume it's in PATH - default: - return 'blender'; - } -} - -/** - * Build the Python expression for glTF export - */ -function buildPythonExpr(outputPath: string, options?: BlenderExportOptions['exportParams']): string { - const params: string[] = [`filepath='${outputPath.replace(/\\/g, '/')}'`]; - - if (options) { - for (const [key, value] of Object.entries(options)) { - if (typeof value === 'boolean') { - params.push(`${key}=${value ? 'True' : 'False'}`); - } else if (typeof value === 'string') { - params.push(`${key}='${value}'`); - } else if (typeof value === 'number') { - params.push(`${key}=${value}`); - } - } - } - - return `import bpy; bpy.ops.export_scene.gltf(${params.join(', ')})`; -} - -/** - * Export a Blender file to GLB format using Blender's command-line interface - * - * @param blendFilePath - Path to the input .blend file - * @param outputPath - Path for the output .glb file - * @param options - Optional configuration for the export - * @returns Promise that resolves with export result - * - * @example - * ```typescript - * // Basic usage - * await exportBlendToGLB('./models/ship.blend', './public/ship.glb'); - * - * // With options - * await exportBlendToGLB('./models/asteroid.blend', './public/asteroid.glb', { - * exportParams: { - * export_draco_mesh_compression_enable: true, - * export_apply_modifiers: true - * } - * }); - * - * // With custom Blender path - * await exportBlendToGLB('./model.blend', './output.glb', { - * blenderPath: '/custom/path/to/blender' - * }); - * ``` - */ -export async function exportBlendToGLB( - blendFilePath: string, - outputPath: string, - options?: BlenderExportOptions -): Promise { - const startTime = Date.now(); - - // Validate input file exists - if (!existsSync(blendFilePath)) { - throw new Error(`Input blend file not found: ${blendFilePath}`); - } - - // Ensure output directory exists - const outputDir = path.dirname(outputPath); - if (!existsSync(outputDir)) { - throw new Error(`Output directory does not exist: ${outputDir}`); - } - - // Get Blender executable path - const blenderPath = options?.blenderPath || getDefaultBlenderPath(); - - // Verify Blender exists - if (blenderPath !== 'blender' && !existsSync(blenderPath)) { - throw new Error(`Blender executable not found at: ${blenderPath}`); - } - - // Build Python expression - const pythonExpr = buildPythonExpr(outputPath, options?.exportParams); - - // Build command arguments - const args = [ - '-b', // Background mode (no UI) - blendFilePath, // Input file - '--python-expr', // Execute Python expression - pythonExpr // The export command - ]; - - console.log(`[BlenderExporter] Running: ${blenderPath} ${args.join(' ')}`); - - return new Promise((resolve, reject) => { - let stdout = ''; - let stderr = ''; - - const process = spawn(blenderPath, args, { - shell: false, - windowsHide: true - }); - - // Set timeout - const timeout = options?.timeout || 60000; - const timeoutId = setTimeout(() => { - process.kill(); - reject(new Error(`Blender export timed out after ${timeout}ms`)); - }, timeout); - - process.stdout.on('data', (data) => { - stdout += data.toString(); - }); - - process.stderr.on('data', (data) => { - stderr += data.toString(); - }); - - process.on('error', (error) => { - clearTimeout(timeoutId); - reject(new Error(`Failed to spawn Blender process: ${error.message}`)); - }); - - process.on('close', (code) => { - clearTimeout(timeoutId); - const duration = Date.now() - startTime; - - if (code === 0) { - // Check if output file was created - if (existsSync(outputPath)) { - console.log(`[BlenderExporter] Successfully exported to ${outputPath} in ${duration}ms`); - resolve({ - success: true, - outputPath, - stdout, - stderr, - duration - }); - } else { - reject(new Error(`Blender exited successfully but output file was not created: ${outputPath}`)); - } - } else { - const error = new Error( - `Blender export failed with exit code ${code}\n` + - `STDERR: ${stderr}\n` + - `STDOUT: ${stdout}` - ); - reject(error); - } - }); - }); -} - -/** - * Batch export multiple Blender files to GLB - * - * @param exports - Array of [inputPath, outputPath] tuples - * @param options - Optional configuration for all exports - * @param sequential - If true, run exports one at a time (default: false for parallel) - * @returns Promise that resolves with array of results - * - * @example - * ```typescript - * const results = await batchExportBlendToGLB([ - * ['./ship1.blend', './public/ship1.glb'], - * ['./ship2.blend', './public/ship2.glb'], - * ['./asteroid.blend', './public/asteroid.glb'] - * ], { - * exportParams: { export_draco_mesh_compression_enable: true } - * }); - * ``` - */ -export async function batchExportBlendToGLB( - exports: Array<[string, string]>, - options?: BlenderExportOptions, - sequential: boolean = false -): Promise { - if (sequential) { - const results: BlenderExportResult[] = []; - for (const [input, output] of exports) { - const result = await exportBlendToGLB(input, output, options); - results.push(result); - } - return results; - } else { - return Promise.all( - exports.map(([input, output]) => exportBlendToGLB(input, output, options)) - ); - } -} - -/** - * Watch a Blender file and auto-export on changes - * (Requires fs.watch - Node.js only, not for browser) - * - * @param blendFilePath - Path to watch - * @param outputPath - Output GLB path - * @param options - Export options - * @returns Function to stop watching - */ -export function watchAndExport( - blendFilePath: string, - outputPath: string, - options?: BlenderExportOptions -): () => void { - console.log(`[BlenderExporter] Watching ${blendFilePath} for changes...`); - - let debounceTimer: NodeJS.Timeout | null = null; - - const watcher = watch(blendFilePath, (eventType: string) => { - if (eventType === 'change') { - // Debounce: wait 1 second after last change - if (debounceTimer) clearTimeout(debounceTimer); - - debounceTimer = setTimeout(async () => { - console.log(`[BlenderExporter] Detected change in ${blendFilePath}, exporting...`); - try { - await exportBlendToGLB(blendFilePath, outputPath, options); - } catch (error) { - console.error(`[BlenderExporter] Export failed:`, error); - } - }, 1000); - } - }); - - // Return cleanup function - return () => { - if (debounceTimer) clearTimeout(debounceTimer); - watcher.close(); - console.log(`[BlenderExporter] Stopped watching ${blendFilePath}`); - }; -} diff --git a/svelte.config.js b/svelte.config.js new file mode 100644 index 0000000..3b9ccce --- /dev/null +++ b/svelte.config.js @@ -0,0 +1,11 @@ +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +export default { + // Preprocess TypeScript and other syntax + preprocess: vitePreprocess(), + + compilerOptions: { + // Enable HMR in development + hmr: true, + } +}; diff --git a/themes/default/base2.blend b/themes/default/base2.blend index 31df4bd..e18f709 100644 Binary files a/themes/default/base2.blend and b/themes/default/base2.blend differ diff --git a/tsconfig.json b/tsconfig.json index be51b5b..2125372 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es6", + "target": "es2023", "outDir": "./dist", // choose our ECMA/JavaScript version (all modern browsers support ES6 so it's your best bet) "allowSyntheticDefaultImports": true, @@ -8,7 +8,7 @@ // choose our default ECMA/libraries to import "dom", // mandatory for all browser-based apps - "es6" + "es2023" // mandatory for targeting ES6 ], "useDefineForClassFields": true, @@ -33,12 +33,17 @@ // raises an error for unused parameters "noImplicitReturns": true, // raises an error for functions that return nothing - "skipLibCheck": true + "skipLibCheck": true, // skip type-checking of .d.ts files (it speeds up transpiling) + "types": ["svelte"] + // add Svelte type definitions }, "include": [ "src", "server" ], // specify location(s) of .ts files + "exclude": [ + "node_modules" + ] } diff --git a/vite.config.ts b/vite.config.ts index b638463..4c8c327 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,7 +1,9 @@ import {defineConfig} from "vite"; +import { svelte } from '@sveltejs/vite-plugin-svelte'; /** @type {import('vite').UserConfig} */ export default defineConfig({ + plugins: [svelte()], test: {}, define: {}, build: { @@ -10,7 +12,8 @@ export default defineConfig({ output: { manualChunks: { 'babylon': ['@babylonjs/core'], - 'babylon-procedural': ['@babylonjs/procedural-textures'] + 'babylon-procedural': ['@babylonjs/procedural-textures'], + 'babylon-inspector': ['@babylonjs/inspector'], } } }