Add Blender export tooling and refactor asset structure
All checks were successful
Build / build (push) Successful in 1m18s
All checks were successful
Build / build (push) Successful in 1m18s
## Blender Export Utilities - Add blenderExporter utility with ESM support (tsx) - Create CLI script for exporting .blend files to GLB - Add npm scripts: export-blend, export-blend:watch, export-blend:batch - Support watch mode, batch export, and Draco compression - Complete documentation in docs/BLENDER_EXPORT.md - Add loadAsset utility helper ## Asset Structure Reorganization - Move models to themeable structure: public/assets/themes/default/models/ - Add themes/ directory with source .blend files - Remove old model files from public/ root - Consolidate to asteroid.glb, base.glb, ship.glb ## Level Configuration Improvements - Make startBase optional in LevelConfig interface - Update LevelGenerator to not generate startBase data by default - Update LevelDeserializer to handle optional startBase - Update Level1 to handle null startBase - Fix levelEditor to remove startBase generation references - Update validation to treat startBase as optional ## Dependencies - Add tsx for ESM TypeScript execution - Add @types/node for Node.js types - Update package-lock.json This enables modding support with themeable assets and simplifies level generation by making base stations optional. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
f11005fdb6
commit
146ffccd3d
229
docs/BLENDER_EXPORT.md
Normal file
229
docs/BLENDER_EXPORT.md
Normal file
@ -0,0 +1,229 @@
|
|||||||
|
# Blender Export Utility
|
||||||
|
|
||||||
|
Automated export of Blender `.blend` files to GLB format using Blender's command-line interface.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
First, install the required dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
This will install `tsx` and `@types/node` which are needed to run the export scripts.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- **Blender**: Must be installed on your system
|
||||||
|
- macOS: Install from [blender.org](https://www.blender.org/download/) (will be at `/Applications/Blender.app`)
|
||||||
|
- Windows: Install to default location (`C:\Program Files\Blender Foundation\Blender\`)
|
||||||
|
- Linux: Install via package manager (`sudo apt install blender`)
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Basic Export
|
||||||
|
|
||||||
|
Export a single `.blend` file to `.glb`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run export-blend -- public/ship1.blend public/ship1.glb
|
||||||
|
```
|
||||||
|
|
||||||
|
### Watch Mode
|
||||||
|
|
||||||
|
Automatically re-export when the `.blend` file changes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run export-blend -- public/ship1.blend public/ship1.glb --watch
|
||||||
|
```
|
||||||
|
|
||||||
|
This is useful during development - save your Blender file and it will auto-export!
|
||||||
|
|
||||||
|
### Batch Export
|
||||||
|
|
||||||
|
Export all `.blend` files in a directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run export-blend -- public/ public/ --batch
|
||||||
|
```
|
||||||
|
|
||||||
|
This will:
|
||||||
|
- Find all `.blend` files in `public/`
|
||||||
|
- Export each to `.glb` with the same name
|
||||||
|
- Skip `.blend1` backup files
|
||||||
|
|
||||||
|
### Compression
|
||||||
|
|
||||||
|
Enable Draco mesh compression for smaller file sizes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run export-blend -- public/ship1.blend public/ship1.glb --compress
|
||||||
|
```
|
||||||
|
|
||||||
|
### Advanced Options
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Don't apply modifiers during export
|
||||||
|
npm run export-blend -- input.blend output.glb --no-modifiers
|
||||||
|
|
||||||
|
# Combine options
|
||||||
|
npm run export-blend -- input.blend output.glb --watch --compress
|
||||||
|
```
|
||||||
|
|
||||||
|
## Programmatic Usage
|
||||||
|
|
||||||
|
You can also use the export functions directly in your TypeScript code:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { exportBlendToGLB, batchExportBlendToGLB, watchAndExport } from './src/utils/blenderExporter';
|
||||||
|
|
||||||
|
// Single export
|
||||||
|
async function exportMyModel() {
|
||||||
|
const result = await exportBlendToGLB(
|
||||||
|
'./models/ship.blend',
|
||||||
|
'./public/ship.glb',
|
||||||
|
{
|
||||||
|
exportParams: {
|
||||||
|
export_draco_mesh_compression_enable: true,
|
||||||
|
export_apply_modifiers: true,
|
||||||
|
export_animations: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`Exported in ${result.duration}ms`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Batch export
|
||||||
|
async function exportAllModels() {
|
||||||
|
const results = await batchExportBlendToGLB([
|
||||||
|
['./models/ship1.blend', './public/ship1.glb'],
|
||||||
|
['./models/ship2.blend', './public/ship2.glb'],
|
||||||
|
['./models/asteroid.blend', './public/asteroid.glb']
|
||||||
|
], {
|
||||||
|
exportParams: {
|
||||||
|
export_draco_mesh_compression_enable: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Exported ${results.length} files`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch for changes
|
||||||
|
function watchMyModel() {
|
||||||
|
const stopWatching = watchAndExport(
|
||||||
|
'./models/ship.blend',
|
||||||
|
'./public/ship.glb',
|
||||||
|
{
|
||||||
|
exportParams: {
|
||||||
|
export_apply_modifiers: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Call stopWatching() when you want to stop
|
||||||
|
setTimeout(() => {
|
||||||
|
stopWatching();
|
||||||
|
}, 60000); // Stop after 1 minute
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Export Parameters
|
||||||
|
|
||||||
|
The following glTF export parameters are supported:
|
||||||
|
|
||||||
|
| Parameter | Type | Default | Description |
|
||||||
|
|-----------|------|---------|-------------|
|
||||||
|
| `export_format` | `'GLB'` \| `'GLTF_SEPARATE'` \| `'GLTF_EMBEDDED'` | `'GLB'` | Output format |
|
||||||
|
| `export_draco_mesh_compression_enable` | `boolean` | `false` | Enable Draco compression |
|
||||||
|
| `export_apply_modifiers` | `boolean` | `true` | Apply modifiers before export |
|
||||||
|
| `export_yup` | `boolean` | `true` | Use Y-up coordinate system |
|
||||||
|
| `export_animations` | `boolean` | `true` | Export animations |
|
||||||
|
| `export_materials` | `'EXPORT'` \| `'PLACEHOLDER'` \| `'NONE'` | `'EXPORT'` | Material export mode |
|
||||||
|
|
||||||
|
See [Blender's glTF export documentation](https://docs.blender.org/api/current/bpy.ops.export_scene.html#bpy.ops.export_scene.gltf) for all available parameters.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "Blender executable not found"
|
||||||
|
|
||||||
|
**Solution**: Set a custom Blender path:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
await exportBlendToGLB('input.blend', 'output.glb', {
|
||||||
|
blenderPath: '/custom/path/to/blender'
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Or for the CLI, edit the `blenderExporter.ts` file to update `getDefaultBlenderPath()`.
|
||||||
|
|
||||||
|
### "Export timed out"
|
||||||
|
|
||||||
|
**Solution**: Increase the timeout (default is 60 seconds):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
await exportBlendToGLB('large-model.blend', 'output.glb', {
|
||||||
|
timeout: 120000 // 2 minutes
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Output file not created
|
||||||
|
|
||||||
|
Check that:
|
||||||
|
1. The `.blend` file opens in Blender without errors
|
||||||
|
2. The output directory exists
|
||||||
|
3. You have write permissions to the output directory
|
||||||
|
4. Check the console for Blender error messages
|
||||||
|
|
||||||
|
## Example Workflow
|
||||||
|
|
||||||
|
Here's a typical development workflow:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Terminal 1: Run the dev server
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# Terminal 2: Watch your ship model
|
||||||
|
npm run export-blend -- public/ship2.blend public/ship2.glb --watch --compress
|
||||||
|
|
||||||
|
# Now edit ship2.blend in Blender
|
||||||
|
# Every time you save, it will auto-export to ship2.glb
|
||||||
|
# Refresh your browser to see changes
|
||||||
|
```
|
||||||
|
|
||||||
|
## Integration with Build Process
|
||||||
|
|
||||||
|
You can add the batch export to your build process:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"prebuild": "npm run export-blend:batch -- public/ public/",
|
||||||
|
"build": "tsc && vite build"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Now all `.blend` files will be exported before every build!
|
||||||
|
|
||||||
|
## Performance Tips
|
||||||
|
|
||||||
|
1. **Use compression for production**: Add `--compress` flag to reduce file sizes by ~50-70%
|
||||||
|
2. **Batch exports are sequential**: Large batches may take time
|
||||||
|
3. **Watch mode debounces**: Changes are detected with 1-second delay to avoid excessive exports
|
||||||
|
4. **Optimize your models**: Lower poly counts export faster
|
||||||
|
|
||||||
|
## Current Project Models
|
||||||
|
|
||||||
|
Here are the `.blend` files currently in the project:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Export all current models
|
||||||
|
npm run export-blend -- public/ship1.blend public/ship1.glb
|
||||||
|
npm run export-blend -- public/ship2.blend public/ship2.glb
|
||||||
|
npm run export-blend -- public/asteroid4.blend public/asteroid4.glb
|
||||||
|
npm run export-blend -- public/base.blend public/base.glb
|
||||||
|
|
||||||
|
# Or batch export all at once
|
||||||
|
npm run export-blend:batch -- public/ public/
|
||||||
|
```
|
||||||
526
package-lock.json
generated
526
package-lock.json
generated
@ -19,6 +19,8 @@
|
|||||||
"openai": "4.52.3"
|
"openai": "4.52.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.0.0",
|
||||||
|
"tsx": "^4.7.1",
|
||||||
"typescript": "^5.4.5",
|
"typescript": "^5.4.5",
|
||||||
"vite": "^5.2.13"
|
"vite": "^5.2.13"
|
||||||
}
|
}
|
||||||
@ -392,6 +394,22 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@esbuild/netbsd-arm64": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"netbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@esbuild/netbsd-x64": {
|
"node_modules/@esbuild/netbsd-x64": {
|
||||||
"version": "0.21.5",
|
"version": "0.21.5",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
|
||||||
@ -408,6 +426,22 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@esbuild/openbsd-arm64": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@esbuild/openbsd-x64": {
|
"node_modules/@esbuild/openbsd-x64": {
|
||||||
"version": "0.21.5",
|
"version": "0.21.5",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
|
||||||
@ -424,6 +458,22 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@esbuild/openharmony-arm64": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openharmony"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@esbuild/sunos-x64": {
|
"node_modules/@esbuild/sunos-x64": {
|
||||||
"version": "0.21.5",
|
"version": "0.21.5",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
|
||||||
@ -749,11 +799,11 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "18.19.39",
|
"version": "20.19.24",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.39.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.24.tgz",
|
||||||
"integrity": "sha512-nPwTRDKUctxw3di5b4TfT3I0sWDiWoPQCZjXhvdkINntwr8lcoVCKsTgnXeRubKIlfnV+eN/HYk6Jb40tbcEAQ==",
|
"integrity": "sha512-FE5u0ezmi6y9OZEzlJfg37mqqf6ZDSF2V/NLjUyGrR9uTZ7Sb9F7bLNZ03S4XVUNRWGA7Ck4c1kK+YnuWjl+DA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~5.26.4"
|
"undici-types": "~6.21.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/node-fetch": {
|
"node_modules/@types/node-fetch": {
|
||||||
@ -939,6 +989,18 @@
|
|||||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/get-tsconfig": {
|
||||||
|
"version": "4.13.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz",
|
||||||
|
"integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"resolve-pkg-maps": "^1.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/humanize-ms": {
|
"node_modules/humanize-ms": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
|
||||||
@ -1044,6 +1106,19 @@
|
|||||||
"openai": "bin/cli"
|
"openai": "bin/cli"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/openai/node_modules/@types/node": {
|
||||||
|
"version": "18.19.130",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz",
|
||||||
|
"integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~5.26.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/openai/node_modules/undici-types": {
|
||||||
|
"version": "5.26.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||||
|
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
|
||||||
|
},
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
|
||||||
@ -1078,6 +1153,15 @@
|
|||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/resolve-pkg-maps": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/rollup": {
|
"node_modules/rollup": {
|
||||||
"version": "4.18.0",
|
"version": "4.18.0",
|
||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.0.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.0.tgz",
|
||||||
@ -1127,6 +1211,434 @@
|
|||||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
|
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
|
||||||
},
|
},
|
||||||
|
"node_modules/tsx": {
|
||||||
|
"version": "4.20.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz",
|
||||||
|
"integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"esbuild": "~0.25.0",
|
||||||
|
"get-tsconfig": "^4.7.5"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"tsx": "dist/cli.mjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "~2.3.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/aix-ppc64": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"aix"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/android-arm": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/android-arm64": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/android-x64": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/darwin-arm64": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/darwin-x64": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/freebsd-arm64": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/freebsd-x64": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/linux-arm": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/linux-arm64": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/linux-ia32": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/linux-loong64": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
|
||||||
|
"cpu": [
|
||||||
|
"loong64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/linux-mips64el": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
|
||||||
|
"cpu": [
|
||||||
|
"mips64el"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/linux-ppc64": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/linux-riscv64": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/linux-s390x": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
|
||||||
|
"cpu": [
|
||||||
|
"s390x"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/linux-x64": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/netbsd-x64": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"netbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/openbsd-x64": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/sunos-x64": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"sunos"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/win32-arm64": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/win32-ia32": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/@esbuild/win32-x64": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx/node_modules/esbuild": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"bin": {
|
||||||
|
"esbuild": "bin/esbuild"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@esbuild/aix-ppc64": "0.25.12",
|
||||||
|
"@esbuild/android-arm": "0.25.12",
|
||||||
|
"@esbuild/android-arm64": "0.25.12",
|
||||||
|
"@esbuild/android-x64": "0.25.12",
|
||||||
|
"@esbuild/darwin-arm64": "0.25.12",
|
||||||
|
"@esbuild/darwin-x64": "0.25.12",
|
||||||
|
"@esbuild/freebsd-arm64": "0.25.12",
|
||||||
|
"@esbuild/freebsd-x64": "0.25.12",
|
||||||
|
"@esbuild/linux-arm": "0.25.12",
|
||||||
|
"@esbuild/linux-arm64": "0.25.12",
|
||||||
|
"@esbuild/linux-ia32": "0.25.12",
|
||||||
|
"@esbuild/linux-loong64": "0.25.12",
|
||||||
|
"@esbuild/linux-mips64el": "0.25.12",
|
||||||
|
"@esbuild/linux-ppc64": "0.25.12",
|
||||||
|
"@esbuild/linux-riscv64": "0.25.12",
|
||||||
|
"@esbuild/linux-s390x": "0.25.12",
|
||||||
|
"@esbuild/linux-x64": "0.25.12",
|
||||||
|
"@esbuild/netbsd-arm64": "0.25.12",
|
||||||
|
"@esbuild/netbsd-x64": "0.25.12",
|
||||||
|
"@esbuild/openbsd-arm64": "0.25.12",
|
||||||
|
"@esbuild/openbsd-x64": "0.25.12",
|
||||||
|
"@esbuild/openharmony-arm64": "0.25.12",
|
||||||
|
"@esbuild/sunos-x64": "0.25.12",
|
||||||
|
"@esbuild/win32-arm64": "0.25.12",
|
||||||
|
"@esbuild/win32-ia32": "0.25.12",
|
||||||
|
"@esbuild/win32-x64": "0.25.12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "5.5.3",
|
"version": "5.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz",
|
||||||
@ -1141,9 +1653,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/undici-types": {
|
"node_modules/undici-types": {
|
||||||
"version": "5.26.5",
|
"version": "6.21.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "5.3.3",
|
"version": "5.3.3",
|
||||||
|
|||||||
@ -9,7 +9,10 @@
|
|||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"havok": "cp ./node_modules/@babylonjs/havok/lib/esm/HavokPhysics.wasm ./node_modules/.vite/deps",
|
"havok": "cp ./node_modules/@babylonjs/havok/lib/esm/HavokPhysics.wasm ./node_modules/.vite/deps",
|
||||||
"speech": "tsc && node ./dist/server/voices.js"
|
"speech": "tsc && node ./dist/server/voices.js",
|
||||||
|
"export-blend": "tsx scripts/exportBlend.ts",
|
||||||
|
"export-blend:watch": "tsx scripts/exportBlend.ts --watch",
|
||||||
|
"export-blend:batch": "tsx scripts/exportBlend.ts --batch"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babylonjs/core": "8.32.0",
|
"@babylonjs/core": "8.32.0",
|
||||||
@ -23,6 +26,8 @@
|
|||||||
"openai": "4.52.3"
|
"openai": "4.52.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.0.0",
|
||||||
|
"tsx": "^4.7.1",
|
||||||
"typescript": "^5.4.5",
|
"typescript": "^5.4.5",
|
||||||
"vite": "^5.2.13"
|
"vite": "^5.2.13"
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
public/arrow.stl
BIN
public/arrow.stl
Binary file not shown.
BIN
public/assets/themes/default/models/asteroid.glb
Normal file
BIN
public/assets/themes/default/models/asteroid.glb
Normal file
Binary file not shown.
BIN
public/assets/themes/default/models/base.glb
Normal file
BIN
public/assets/themes/default/models/base.glb
Normal file
Binary file not shown.
BIN
public/assets/themes/default/models/ship.glb
Normal file
BIN
public/assets/themes/default/models/ship.glb
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
public/base.glb
BIN
public/base.glb
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
public/ship1.glb
BIN
public/ship1.glb
Binary file not shown.
BIN
public/ship2.glb
BIN
public/ship2.glb
Binary file not shown.
211
scripts/exportBlend.ts
Normal file
211
scripts/exportBlend.ts
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
#!/usr/bin/env tsx
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CLI script to export Blender files to GLB format
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* tsx scripts/exportBlend.ts <input.blend> <output.glb>
|
||||||
|
* npm run export-blend -- <input.blend> <output.glb>
|
||||||
|
*
|
||||||
|
* Examples:
|
||||||
|
* npm run export-blend -- public/ship1.blend public/ship1.glb
|
||||||
|
* npm run export-blend -- public/asteroid4.blend public/asteroid4.glb
|
||||||
|
*
|
||||||
|
* Options:
|
||||||
|
* --watch Watch the input file and auto-export on changes
|
||||||
|
* --compress Enable Draco mesh compression
|
||||||
|
* --no-modifiers Don't apply modifiers
|
||||||
|
* --batch Export all .blend files in a directory
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { exportBlendToGLB, watchAndExport, batchExportBlendToGLB } from '../src/utils/blenderExporter.js';
|
||||||
|
import { readdirSync, statSync } from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
interface CLIArgs {
|
||||||
|
input?: string;
|
||||||
|
output?: string;
|
||||||
|
watch: boolean;
|
||||||
|
compress: boolean;
|
||||||
|
noModifiers: boolean;
|
||||||
|
batch: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(): CLIArgs {
|
||||||
|
const args: CLIArgs = {
|
||||||
|
watch: false,
|
||||||
|
compress: false,
|
||||||
|
noModifiers: false,
|
||||||
|
batch: false
|
||||||
|
};
|
||||||
|
|
||||||
|
const rawArgs = process.argv.slice(2);
|
||||||
|
|
||||||
|
for (let i = 0; i < rawArgs.length; i++) {
|
||||||
|
const arg = rawArgs[i];
|
||||||
|
|
||||||
|
if (arg === '--watch') {
|
||||||
|
args.watch = true;
|
||||||
|
} else if (arg === '--compress') {
|
||||||
|
args.compress = true;
|
||||||
|
} else if (arg === '--no-modifiers') {
|
||||||
|
args.noModifiers = true;
|
||||||
|
} else if (arg === '--batch') {
|
||||||
|
args.batch = true;
|
||||||
|
} else if (!args.input) {
|
||||||
|
args.input = arg;
|
||||||
|
} else if (!args.output) {
|
||||||
|
args.output = arg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
function printUsage() {
|
||||||
|
console.log(`
|
||||||
|
Usage: npm run export-blend -- <input.blend> <output.glb> [options]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--watch Watch the input file and auto-export on changes
|
||||||
|
--compress Enable Draco mesh compression
|
||||||
|
--no-modifiers Don't apply modifiers during export
|
||||||
|
--batch Export all .blend files in input directory
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
npm run export-blend -- public/ship1.blend public/ship1.glb
|
||||||
|
npm run export-blend -- public/ship1.blend public/ship1.glb --compress
|
||||||
|
npm run export-blend -- public/ship1.blend public/ship1.glb --watch
|
||||||
|
npm run export-blend -- public/ public/ --batch
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const args = parseArgs();
|
||||||
|
|
||||||
|
if (!args.input) {
|
||||||
|
console.error('Error: Input file or directory required\n');
|
||||||
|
printUsage();
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build export options
|
||||||
|
const options = {
|
||||||
|
exportParams: {
|
||||||
|
export_format: 'GLB' as const,
|
||||||
|
export_draco_mesh_compression_enable: args.compress,
|
||||||
|
export_apply_modifiers: !args.noModifiers,
|
||||||
|
export_yup: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (args.batch) {
|
||||||
|
// Batch export mode
|
||||||
|
await batchExportMode(args.input, args.output || args.input, options);
|
||||||
|
} else if (args.watch) {
|
||||||
|
// Watch mode
|
||||||
|
if (!args.output) {
|
||||||
|
console.error('Error: Output file required for watch mode\n');
|
||||||
|
printUsage();
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
await watchMode(args.input, args.output, options);
|
||||||
|
} else {
|
||||||
|
// Single export mode
|
||||||
|
if (!args.output) {
|
||||||
|
console.error('Error: Output file required\n');
|
||||||
|
printUsage();
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
await singleExportMode(args.input, args.output, options);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Export failed:', error instanceof Error ? error.message : error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function singleExportMode(input: string, output: string, options: any) {
|
||||||
|
console.log(`Exporting ${input} to ${output}...`);
|
||||||
|
const result = await exportBlendToGLB(input, output, options);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
console.log(`✅ Successfully exported in ${result.duration}ms`);
|
||||||
|
console.log(` Output: ${result.outputPath}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function watchMode(input: string, output: string, options: any) {
|
||||||
|
console.log(`👀 Watching ${input} for changes...`);
|
||||||
|
console.log(` Will export to ${output}`);
|
||||||
|
console.log(` Press Ctrl+C to stop\n`);
|
||||||
|
|
||||||
|
// Do initial export
|
||||||
|
try {
|
||||||
|
await exportBlendToGLB(input, output, options);
|
||||||
|
console.log('✅ Initial export complete\n');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Initial export failed:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start watching
|
||||||
|
const stopWatching = watchAndExport(input, output, options);
|
||||||
|
|
||||||
|
// Handle Ctrl+C
|
||||||
|
process.on('SIGINT', () => {
|
||||||
|
console.log('\n\nStopping watch mode...');
|
||||||
|
stopWatching();
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keep process alive
|
||||||
|
await new Promise(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function batchExportMode(inputDir: string, outputDir: string, options: any) {
|
||||||
|
console.log(`📦 Batch exporting .blend files from ${inputDir}...`);
|
||||||
|
|
||||||
|
// Find all .blend files in input directory
|
||||||
|
const files = readdirSync(inputDir)
|
||||||
|
.filter(f => f.endsWith('.blend') && !f.endsWith('.blend1'))
|
||||||
|
.filter(f => {
|
||||||
|
const fullPath = path.join(inputDir, f);
|
||||||
|
return statSync(fullPath).isFile();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (files.length === 0) {
|
||||||
|
console.log('No .blend files found in directory');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Found ${files.length} .blend file(s):`);
|
||||||
|
files.forEach(f => console.log(` - ${f}`));
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
const exports: Array<[string, string]> = files.map(f => {
|
||||||
|
const inputPath = path.join(inputDir, f);
|
||||||
|
const outputPath = path.join(outputDir, f.replace('.blend', '.glb'));
|
||||||
|
return [inputPath, outputPath];
|
||||||
|
});
|
||||||
|
|
||||||
|
const results = await batchExportBlendToGLB(exports, options, true); // Sequential
|
||||||
|
|
||||||
|
// Print summary
|
||||||
|
console.log('\n📊 Export Summary:');
|
||||||
|
const successful = results.filter(r => r.success).length;
|
||||||
|
console.log(`✅ Successful: ${successful}/${results.length}`);
|
||||||
|
|
||||||
|
results.forEach((result, i) => {
|
||||||
|
const [input] = exports[i];
|
||||||
|
const filename = path.basename(input);
|
||||||
|
if (result.success) {
|
||||||
|
console.log(` ✓ ${filename} (${result.duration}ms)`);
|
||||||
|
} else {
|
||||||
|
console.log(` ✗ ${filename} - FAILED`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the script
|
||||||
|
main();
|
||||||
@ -2,19 +2,11 @@ import {DefaultScene} from "./defaultScene";
|
|||||||
import type {AudioEngineV2} from "@babylonjs/core";
|
import type {AudioEngineV2} from "@babylonjs/core";
|
||||||
import {
|
import {
|
||||||
AbstractMesh,
|
AbstractMesh,
|
||||||
Color3,
|
|
||||||
DistanceConstraint,
|
|
||||||
MeshBuilder,
|
|
||||||
Observable,
|
Observable,
|
||||||
PhysicsAggregate,
|
|
||||||
PhysicsMotionType,
|
|
||||||
PhysicsShapeType,
|
|
||||||
StandardMaterial,
|
|
||||||
Vector3
|
Vector3
|
||||||
} from "@babylonjs/core";
|
} from "@babylonjs/core";
|
||||||
import {Ship} from "./ship";
|
import {Ship} from "./ship";
|
||||||
import Level from "./level";
|
import Level from "./level";
|
||||||
import {Scoreboard} from "./scoreboard";
|
|
||||||
import setLoadingMessage from "./setLoadingMessage";
|
import setLoadingMessage from "./setLoadingMessage";
|
||||||
import {LevelConfig} from "./levelConfig";
|
import {LevelConfig} from "./levelConfig";
|
||||||
import {LevelDeserializer} from "./levelDeserializer";
|
import {LevelDeserializer} from "./levelDeserializer";
|
||||||
@ -25,7 +17,7 @@ export class Level1 implements Level {
|
|||||||
private _ship: Ship;
|
private _ship: Ship;
|
||||||
private _onReadyObservable: Observable<Level> = new Observable<Level>();
|
private _onReadyObservable: Observable<Level> = new Observable<Level>();
|
||||||
private _initialized: boolean = false;
|
private _initialized: boolean = false;
|
||||||
private _startBase: AbstractMesh;
|
private _startBase: AbstractMesh | null;
|
||||||
private _endBase: AbstractMesh;
|
private _endBase: AbstractMesh;
|
||||||
private _levelConfig: LevelConfig;
|
private _levelConfig: LevelConfig;
|
||||||
private _audioEngine: AudioEngineV2;
|
private _audioEngine: AudioEngineV2;
|
||||||
@ -46,6 +38,7 @@ export class Level1 implements Level {
|
|||||||
|
|
||||||
xr.baseExperience.onInitialXRPoseSetObservable.add(() => {
|
xr.baseExperience.onInitialXRPoseSetObservable.add(() => {
|
||||||
xr.baseExperience.camera.parent = this._ship.transformNode;
|
xr.baseExperience.camera.parent = this._ship.transformNode;
|
||||||
|
const currPose = xr.baseExperience.camera.globalPosition.y;
|
||||||
xr.baseExperience.camera.position = new Vector3(0, 0, 0);
|
xr.baseExperience.camera.position = new Vector3(0, 0, 0);
|
||||||
const observer = xr.input.onControllerAddedObservable.add((controller) => {
|
const observer = xr.input.onControllerAddedObservable.add((controller) => {
|
||||||
debugLog('🎮 onControllerAddedObservable FIRED for:', controller.inputSource.handedness);
|
debugLog('🎮 onControllerAddedObservable FIRED for:', controller.inputSource.handedness);
|
||||||
@ -59,7 +52,6 @@ export class Level1 implements Level {
|
|||||||
return this._onReadyObservable;
|
return this._onReadyObservable;
|
||||||
}
|
}
|
||||||
|
|
||||||
private scored: Set<string> = new Set<string>();
|
|
||||||
public async play() {
|
public async play() {
|
||||||
// Create background music using AudioEngineV2
|
// Create background music using AudioEngineV2
|
||||||
const background = await this._audioEngine.createSoundAsync("background", "/song1.mp3", {
|
const background = await this._audioEngine.createSoundAsync("background", "/song1.mp3", {
|
||||||
@ -88,7 +80,9 @@ export class Level1 implements Level {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public dispose() {
|
public dispose() {
|
||||||
|
if (this._startBase) {
|
||||||
this._startBase.dispose();
|
this._startBase.dispose();
|
||||||
|
}
|
||||||
this._endBase.dispose();
|
this._endBase.dispose();
|
||||||
if (this._backgroundStars) {
|
if (this._backgroundStars) {
|
||||||
this._backgroundStars.dispose();
|
this._backgroundStars.dispose();
|
||||||
@ -113,25 +107,7 @@ export class Level1 implements Level {
|
|||||||
this._ship.scoreboard.setRemainingCount(entities.asteroids.length);
|
this._ship.scoreboard.setRemainingCount(entities.asteroids.length);
|
||||||
debugLog(`Initialized scoreboard with ${entities.asteroids.length} asteroids`);
|
debugLog(`Initialized scoreboard with ${entities.asteroids.length} asteroids`);
|
||||||
|
|
||||||
// Position ship from config
|
|
||||||
const shipConfig = this._deserializer.getShipConfig();
|
|
||||||
//this._ship.position = new Vector3(shipConfig.position[0], shipConfig.position[1], shipConfig.position[2]);
|
|
||||||
|
|
||||||
// Add distance constraints to asteroids (if physics enabled)
|
|
||||||
setLoadingMessage("Configuring physics constraints...");
|
|
||||||
const asteroidMeshes = entities.asteroids;
|
|
||||||
if (this._startBase.physicsBody) {
|
|
||||||
for (let i = 0; i < asteroidMeshes.length; i++) {
|
|
||||||
const asteroidMesh = asteroidMeshes[i];
|
|
||||||
if (asteroidMesh.physicsBody) {
|
|
||||||
// Calculate distance from start base
|
|
||||||
const dist = Vector3.Distance(asteroidMesh.position, this._startBase.position);
|
|
||||||
const constraint = new DistanceConstraint(dist, DefaultScene.MainScene);
|
|
||||||
// constraint.isCollisionsEnabled = true;
|
|
||||||
this._startBase.physicsBody.addConstraint(asteroidMesh.physicsBody, constraint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create background starfield
|
// Create background starfield
|
||||||
setLoadingMessage("Creating starfield...");
|
setLoadingMessage("Creating starfield...");
|
||||||
@ -156,33 +132,4 @@ export class Level1 implements Level {
|
|||||||
// Notify that initialization is complete
|
// Notify that initialization is complete
|
||||||
this._onReadyObservable.notifyObservers(this);
|
this._onReadyObservable.notifyObservers(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private createTarget(i: number) {
|
|
||||||
const target = MeshBuilder.CreateTorus("target" + i, {diameter: 10, tessellation: 72}, DefaultScene.MainScene);
|
|
||||||
const targetLOD = MeshBuilder.CreateTorus("target" + i, {
|
|
||||||
diameter: 50,
|
|
||||||
tessellation: 10
|
|
||||||
}, DefaultScene.MainScene);
|
|
||||||
targetLOD.parent = target;
|
|
||||||
target.addLODLevel(300, targetLOD);
|
|
||||||
|
|
||||||
const material = new StandardMaterial("material", DefaultScene.MainScene);
|
|
||||||
material.diffuseColor = new Color3(1, 0, 0);
|
|
||||||
material.alpha = .9;
|
|
||||||
target.material = material;
|
|
||||||
target.position = Vector3.Random(-1000, 1000);
|
|
||||||
target.rotation = Vector3.Random(0, Math.PI * 2);
|
|
||||||
const disc = MeshBuilder.CreateDisc("disc-" + i, {radius: 2, tessellation: 72}, DefaultScene.MainScene);
|
|
||||||
const discMaterial = new StandardMaterial("material", DefaultScene.MainScene);
|
|
||||||
discMaterial.ambientColor = new Color3(.1, 1, .1);
|
|
||||||
discMaterial.alpha = .2;
|
|
||||||
target.addLODLevel(200, null);
|
|
||||||
disc.material = discMaterial;
|
|
||||||
disc.parent = target;
|
|
||||||
disc.rotation.x = -Math.PI / 2;
|
|
||||||
const agg = new PhysicsAggregate(disc, PhysicsShapeType.MESH, {mass: 0}, DefaultScene.MainScene);
|
|
||||||
agg.body.setMotionType(PhysicsMotionType.STATIC);
|
|
||||||
agg.shape.isTrigger = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -19,11 +19,12 @@ export interface ShipConfig {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Start base configuration (yellow cylinder where asteroids are constrained to)
|
* Start base configuration (yellow cylinder where asteroids are constrained to)
|
||||||
|
* All fields optional to allow levels without start bases
|
||||||
*/
|
*/
|
||||||
export interface StartBaseConfig {
|
export interface StartBaseConfig {
|
||||||
position: Vector3Array;
|
position?: Vector3Array;
|
||||||
diameter: number;
|
diameter?: number;
|
||||||
height: number;
|
height?: number;
|
||||||
color?: Vector3Array; // RGB values 0-1
|
color?: Vector3Array; // RGB values 0-1
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,7 +86,7 @@ export interface LevelConfig {
|
|||||||
};
|
};
|
||||||
|
|
||||||
ship: ShipConfig;
|
ship: ShipConfig;
|
||||||
startBase: StartBaseConfig;
|
startBase?: StartBaseConfig;
|
||||||
sun: SunConfig;
|
sun: SunConfig;
|
||||||
planets: PlanetConfig[];
|
planets: PlanetConfig[];
|
||||||
asteroids: AsteroidConfig[];
|
asteroids: AsteroidConfig[];
|
||||||
@ -127,17 +128,15 @@ export function validateLevelConfig(config: any): ValidationResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check startBase
|
// Check startBase (optional)
|
||||||
if (!config.startBase) {
|
if (config.startBase) {
|
||||||
errors.push('Missing startBase configuration');
|
if (config.startBase.position && (!Array.isArray(config.startBase.position) || config.startBase.position.length !== 3)) {
|
||||||
} else {
|
|
||||||
if (!Array.isArray(config.startBase.position) || config.startBase.position.length !== 3) {
|
|
||||||
errors.push('Invalid startBase.position - must be [x, y, z] array');
|
errors.push('Invalid startBase.position - must be [x, y, z] array');
|
||||||
}
|
}
|
||||||
if (typeof config.startBase.diameter !== 'number') {
|
if (config.startBase.diameter !== undefined && typeof config.startBase.diameter !== 'number') {
|
||||||
errors.push('Invalid startBase.diameter - must be a number');
|
errors.push('Invalid startBase.diameter - must be a number');
|
||||||
}
|
}
|
||||||
if (typeof config.startBase.height !== 'number') {
|
if (config.startBase.height !== undefined && typeof config.startBase.height !== 'number') {
|
||||||
errors.push('Invalid startBase.height - must be a number');
|
errors.push('Invalid startBase.height - must be a number');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,15 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
AbstractMesh,
|
AbstractMesh,
|
||||||
Color3, DirectionalLight,
|
|
||||||
GlowLayer,
|
|
||||||
MeshBuilder,
|
MeshBuilder,
|
||||||
Observable,
|
Observable,
|
||||||
PhysicsAggregate,
|
|
||||||
PhysicsMotionType,
|
|
||||||
PhysicsShapeType,
|
|
||||||
PointLight,
|
|
||||||
StandardMaterial,
|
|
||||||
Texture,
|
|
||||||
Vector3
|
Vector3
|
||||||
} from "@babylonjs/core";
|
} from "@babylonjs/core";
|
||||||
import { DefaultScene } from "./defaultScene";
|
import { DefaultScene } from "./defaultScene";
|
||||||
@ -18,19 +10,13 @@ import { ScoreEvent } from "./scoreboard";
|
|||||||
import {
|
import {
|
||||||
LevelConfig,
|
LevelConfig,
|
||||||
ShipConfig,
|
ShipConfig,
|
||||||
StartBaseConfig,
|
|
||||||
SunConfig,
|
|
||||||
PlanetConfig,
|
|
||||||
AsteroidConfig,
|
|
||||||
Vector3Array,
|
Vector3Array,
|
||||||
validateLevelConfig
|
validateLevelConfig
|
||||||
} from "./levelConfig";
|
} from "./levelConfig";
|
||||||
import { FireProceduralTexture } from "@babylonjs/procedural-textures";
|
|
||||||
import {createSphereLightmap} from "./sphereLightmap";
|
|
||||||
import { GameConfig } from "./gameConfig";
|
import { GameConfig } from "./gameConfig";
|
||||||
import buildStarBase from "./starBase";
|
|
||||||
import { MaterialFactory } from "./materialFactory";
|
import { MaterialFactory } from "./materialFactory";
|
||||||
import debugLog from './debug';
|
import debugLog from './debug';
|
||||||
|
import StarBase from "./starBase";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deserializes a LevelConfig JSON object and creates all entities in the scene
|
* Deserializes a LevelConfig JSON object and creates all entities in the scene
|
||||||
@ -53,7 +39,7 @@ export class LevelDeserializer {
|
|||||||
* Create all entities from the configuration
|
* Create all entities from the configuration
|
||||||
*/
|
*/
|
||||||
public async deserialize(scoreObservable: Observable<ScoreEvent>): Promise<{
|
public async deserialize(scoreObservable: Observable<ScoreEvent>): Promise<{
|
||||||
startBase: AbstractMesh;
|
startBase: AbstractMesh | null;
|
||||||
sun: AbstractMesh;
|
sun: AbstractMesh;
|
||||||
planets: AbstractMesh[];
|
planets: AbstractMesh[];
|
||||||
asteroids: AbstractMesh[];
|
asteroids: AbstractMesh[];
|
||||||
@ -66,10 +52,13 @@ export class LevelDeserializer {
|
|||||||
const planets = this.createPlanets();
|
const planets = this.createPlanets();
|
||||||
const asteroids = await this.createAsteroids(scoreObservable);
|
const asteroids = await this.createAsteroids(scoreObservable);
|
||||||
|
|
||||||
|
/*
|
||||||
const dir = new Vector3(-1,-2,-1)
|
const dir = new Vector3(-1,-2,-1)
|
||||||
|
|
||||||
const light = new DirectionalLight("dirLight", dir, DefaultScene.MainScene);
|
const light = new DirectionalLight("dirLight", dir, DefaultScene.MainScene);
|
||||||
const light2 = new DirectionalLight("dirLight2", dir.negate(), DefaultScene.MainScene);
|
const light2 = new DirectionalLight("dirLight2", dir.negate(), DefaultScene.MainScene);
|
||||||
light2.intensity = .5;
|
light2.intensity = .5;
|
||||||
|
*/
|
||||||
return {
|
return {
|
||||||
startBase,
|
startBase,
|
||||||
sun,
|
sun,
|
||||||
@ -82,13 +71,8 @@ export class LevelDeserializer {
|
|||||||
* Create the start base from config
|
* Create the start base from config
|
||||||
*/
|
*/
|
||||||
private async createStartBase(): Promise<AbstractMesh> {
|
private async createStartBase(): Promise<AbstractMesh> {
|
||||||
const config = this.config.startBase;
|
const position = this?.config?.startBase?.position?this.arrayToVector3(this?.config?.startBase?.position):null;
|
||||||
const position = this.arrayToVector3(config.position);
|
return await StarBase.buildStarBase(position);
|
||||||
|
|
||||||
// Call the buildStarBase function to load and configure the base
|
|
||||||
const baseMesh = await buildStarBase(position);
|
|
||||||
|
|
||||||
return baseMesh;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -96,19 +80,11 @@ export class LevelDeserializer {
|
|||||||
*/
|
*/
|
||||||
private createSun(): AbstractMesh {
|
private createSun(): AbstractMesh {
|
||||||
const config = this.config.sun;
|
const config = this.config.sun;
|
||||||
|
|
||||||
// Create point light
|
|
||||||
//const light = new PointLight("light", this.arrayToVector3(config.position), this.scene);
|
|
||||||
//light.intensity = config.intensity || 1000000;
|
|
||||||
|
|
||||||
// Create sun sphere
|
|
||||||
const sun = MeshBuilder.CreateSphere("sun", {
|
const sun = MeshBuilder.CreateSphere("sun", {
|
||||||
diameter: config.diameter,
|
diameter: config.diameter,
|
||||||
segments: 32
|
segments: 32
|
||||||
}, this.scene);
|
}, this.scene);
|
||||||
|
|
||||||
sun.position = this.arrayToVector3(config.position);
|
sun.position = this.arrayToVector3(config.position);
|
||||||
|
|
||||||
// Create material using GameConfig texture level
|
// Create material using GameConfig texture level
|
||||||
const gameConfig = GameConfig.getInstance();
|
const gameConfig = GameConfig.getInstance();
|
||||||
const material = MaterialFactory.createSunMaterial(
|
const material = MaterialFactory.createSunMaterial(
|
||||||
@ -181,21 +157,11 @@ export class LevelDeserializer {
|
|||||||
i,
|
i,
|
||||||
this.arrayToVector3(asteroidConfig.position),
|
this.arrayToVector3(asteroidConfig.position),
|
||||||
this.arrayToVector3(asteroidConfig.scaling),
|
this.arrayToVector3(asteroidConfig.scaling),
|
||||||
|
this.arrayToVector3(asteroidConfig.linearVelocity),
|
||||||
|
this.arrayToVector3(asteroidConfig.angularVelocity),
|
||||||
scoreObservable
|
scoreObservable
|
||||||
);
|
);
|
||||||
|
|
||||||
// Set velocities from config
|
|
||||||
if (rock.physicsBody) {
|
|
||||||
rock.physicsBody.setLinearVelocity(this.arrayToVector3(asteroidConfig.linearVelocity));
|
|
||||||
|
|
||||||
if (asteroidConfig.angularVelocity) {
|
|
||||||
rock.physicsBody.setAngularVelocity(this.arrayToVector3(asteroidConfig.angularVelocity));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: We don't set mass here as RockFactory already sets it to 10000
|
|
||||||
// If needed, could add: rock.physicsBody.setMassProperties({ mass: asteroidConfig.mass || 10000 });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the actual mesh from the Rock object
|
// Get the actual mesh from the Rock object
|
||||||
// The Rock class wraps the mesh, need to access it via position getter
|
// The Rock class wraps the mesh, need to access it via position getter
|
||||||
const mesh = this.scene.getMeshByName(asteroidConfig.id);
|
const mesh = this.scene.getMeshByName(asteroidConfig.id);
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { LevelGenerator } from "./levelGenerator";
|
import { LevelGenerator } from "./levelGenerator";
|
||||||
import { LevelConfig, DifficultyConfig, Vector3Array, validateLevelConfig } from "./levelConfig";
|
import { LevelConfig, DifficultyConfig, validateLevelConfig } from "./levelConfig";
|
||||||
import debugLog from './debug';
|
import debugLog from './debug';
|
||||||
|
|
||||||
const STORAGE_KEY = 'space-game-levels';
|
const STORAGE_KEY = 'space-game-levels';
|
||||||
@ -373,14 +373,7 @@ class LevelEditor {
|
|||||||
parseFloat((document.getElementById('shipZ') as HTMLInputElement).value)
|
parseFloat((document.getElementById('shipZ') as HTMLInputElement).value)
|
||||||
];
|
];
|
||||||
|
|
||||||
// Override start base
|
// Note: startBase is no longer generated by default
|
||||||
generator.startBasePosition = [
|
|
||||||
parseFloat((document.getElementById('baseX') as HTMLInputElement).value),
|
|
||||||
parseFloat((document.getElementById('baseY') as HTMLInputElement).value),
|
|
||||||
parseFloat((document.getElementById('baseZ') as HTMLInputElement).value)
|
|
||||||
];
|
|
||||||
generator.startBaseDiameter = parseFloat((document.getElementById('baseDiameter') as HTMLInputElement).value);
|
|
||||||
generator.startBaseHeight = parseFloat((document.getElementById('baseHeight') as HTMLInputElement).value);
|
|
||||||
|
|
||||||
// Override sun
|
// Override sun
|
||||||
generator.sunPosition = [
|
generator.sunPosition = [
|
||||||
|
|||||||
@ -19,10 +19,6 @@ export class LevelGenerator {
|
|||||||
|
|
||||||
// Configurable properties (can be overridden by subclasses or set before generate())
|
// Configurable properties (can be overridden by subclasses or set before generate())
|
||||||
public shipPosition: Vector3Array = [0, 1, 0];
|
public shipPosition: Vector3Array = [0, 1, 0];
|
||||||
public startBasePosition: Vector3Array = [0, 0, 0];
|
|
||||||
public startBaseDiameter = 10;
|
|
||||||
public startBaseHeight = 1;
|
|
||||||
public startBaseColor: Vector3Array = [1, 1, 0]; // Yellow
|
|
||||||
|
|
||||||
public sunPosition: Vector3Array = [0, 0, 400];
|
public sunPosition: Vector3Array = [0, 0, 400];
|
||||||
public sunDiameter = 50;
|
public sunDiameter = 50;
|
||||||
@ -52,7 +48,6 @@ export class LevelGenerator {
|
|||||||
*/
|
*/
|
||||||
public generate(): LevelConfig {
|
public generate(): LevelConfig {
|
||||||
const ship = this.generateShip();
|
const ship = this.generateShip();
|
||||||
const startBase = this.generateStartBase();
|
|
||||||
const sun = this.generateSun();
|
const sun = this.generateSun();
|
||||||
const planets = this.generatePlanets();
|
const planets = this.generatePlanets();
|
||||||
const asteroids = this.generateAsteroids();
|
const asteroids = this.generateAsteroids();
|
||||||
@ -66,7 +61,7 @@ export class LevelGenerator {
|
|||||||
description: `Procedurally generated ${this._difficulty} level`
|
description: `Procedurally generated ${this._difficulty} level`
|
||||||
},
|
},
|
||||||
ship,
|
ship,
|
||||||
startBase,
|
// startBase is now optional and not generated
|
||||||
sun,
|
sun,
|
||||||
planets,
|
planets,
|
||||||
asteroids,
|
asteroids,
|
||||||
@ -83,15 +78,6 @@ export class LevelGenerator {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private generateStartBase(): StartBaseConfig {
|
|
||||||
return {
|
|
||||||
position: [...this.startBasePosition],
|
|
||||||
diameter: this.startBaseDiameter,
|
|
||||||
height: this.startBaseHeight,
|
|
||||||
color: [...this.startBaseColor]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private generateSun(): SunConfig {
|
private generateSun(): SunConfig {
|
||||||
return {
|
return {
|
||||||
position: [...this.sunPosition],
|
position: [...this.sunPosition],
|
||||||
|
|||||||
@ -2,16 +2,14 @@ import {
|
|||||||
AudioEngineV2,
|
AudioEngineV2,
|
||||||
Color3,
|
Color3,
|
||||||
CreateAudioEngineAsync,
|
CreateAudioEngineAsync,
|
||||||
DirectionalLight,
|
|
||||||
Engine,
|
Engine,
|
||||||
HavokPlugin, HemisphericLight,
|
HavokPlugin,
|
||||||
ParticleHelper,
|
ParticleHelper,
|
||||||
Scene,
|
Scene,
|
||||||
ScenePerformancePriority,
|
|
||||||
Vector3,
|
Vector3,
|
||||||
WebGPUEngine,
|
WebGPUEngine,
|
||||||
WebXRDefaultExperience,
|
WebXRDefaultExperience,
|
||||||
WebXRFeatureName, WebXRFeaturesManager
|
WebXRFeaturesManager
|
||||||
} from "@babylonjs/core";
|
} from "@babylonjs/core";
|
||||||
import '@babylonjs/loaders';
|
import '@babylonjs/loaders';
|
||||||
import HavokPhysics from "@babylonjs/havok";
|
import HavokPhysics from "@babylonjs/havok";
|
||||||
@ -216,7 +214,7 @@ export class Main {
|
|||||||
DefaultScene.DemoScene = new Scene(this._engine);
|
DefaultScene.DemoScene = new Scene(this._engine);
|
||||||
DefaultScene.MainScene = new Scene(this._engine);
|
DefaultScene.MainScene = new Scene(this._engine);
|
||||||
|
|
||||||
DefaultScene.MainScene.ambientColor = new Color3(0,0,0);
|
DefaultScene.MainScene.ambientColor = new Color3(.2,.2,.2);
|
||||||
DefaultScene.MainScene.clearColor = new Color3(0, 0, 0).toColor4();
|
DefaultScene.MainScene.clearColor = new Color3(0, 0, 0).toColor4();
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,21 +1,21 @@
|
|||||||
import {
|
import {
|
||||||
AbstractMesh,
|
AbstractMesh,
|
||||||
|
DistanceConstraint,
|
||||||
InstancedMesh,
|
InstancedMesh,
|
||||||
Mesh,
|
Mesh,
|
||||||
Observable,
|
Observable,
|
||||||
PBRMaterial,
|
PhysicsAggregate,
|
||||||
PhysicsAggregate, PhysicsBody,
|
PhysicsBody,
|
||||||
PhysicsMotionType,
|
PhysicsMotionType,
|
||||||
PhysicsShapeType, PhysicsViewer,
|
PhysicsShapeType, TransformNode,
|
||||||
SceneLoader,
|
|
||||||
Vector3
|
Vector3
|
||||||
} from "@babylonjs/core";
|
} from "@babylonjs/core";
|
||||||
import {DefaultScene} from "./defaultScene";
|
import {DefaultScene} from "./defaultScene";
|
||||||
import {ScoreEvent} from "./scoreboard";
|
import {ScoreEvent} from "./scoreboard";
|
||||||
import { GameConfig } from "./gameConfig";
|
import {GameConfig} from "./gameConfig";
|
||||||
import { MaterialFactory } from "./materialFactory";
|
import {ExplosionManager} from "./explosionManager";
|
||||||
import { ExplosionManager } from "./explosionManager";
|
|
||||||
import debugLog from './debug';
|
import debugLog from './debug';
|
||||||
|
import loadAsset from "./utils/loadAsset";
|
||||||
|
|
||||||
export class Rock {
|
export class Rock {
|
||||||
private _rockMesh: AbstractMesh;
|
private _rockMesh: AbstractMesh;
|
||||||
@ -31,60 +31,39 @@ export class Rock {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class RockFactory {
|
export class RockFactory {
|
||||||
private static _rockMesh: AbstractMesh;
|
private static _asteroidMesh: AbstractMesh;
|
||||||
private static _rockMaterial: PBRMaterial;
|
|
||||||
private static _originalMaterial: PBRMaterial = null;
|
|
||||||
private static _explosionManager: ExplosionManager;
|
private static _explosionManager: ExplosionManager;
|
||||||
private static _viewer: PhysicsViewer = null;
|
private static _orbitCenter: PhysicsAggregate;
|
||||||
|
|
||||||
public static async init() {
|
public static async init() {
|
||||||
// Initialize explosion manager
|
// Initialize explosion manager
|
||||||
|
const node = new TransformNode('orbitCenter', DefaultScene.MainScene);
|
||||||
|
node.position = Vector3.Zero();
|
||||||
|
this._orbitCenter = new PhysicsAggregate(node, PhysicsShapeType.SPHERE, {radius: .1, mass: 1000}, DefaultScene.MainScene );
|
||||||
|
|
||||||
this._explosionManager = new ExplosionManager(DefaultScene.MainScene, {
|
this._explosionManager = new ExplosionManager(DefaultScene.MainScene, {
|
||||||
duration: 500,
|
duration: 800,
|
||||||
explosionForce: 15.0,
|
explosionForce: 20.0,
|
||||||
frameRate: 60
|
frameRate: 60
|
||||||
});
|
});
|
||||||
await this._explosionManager.initialize();
|
await this._explosionManager.initialize();
|
||||||
|
|
||||||
if (!this._rockMesh) {
|
if (!this._asteroidMesh) {
|
||||||
await this.loadMesh();
|
await this.loadMesh();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private static async loadMesh() {
|
private static async loadMesh() {
|
||||||
debugLog('loading mesh');
|
debugLog('loading mesh');
|
||||||
const importMesh = await SceneLoader.ImportMeshAsync(null, "./", "asteroid4.glb", DefaultScene.MainScene);
|
this._asteroidMesh = (await loadAsset("asteroid.glb")).meshes.get('Asteroid');
|
||||||
this._rockMesh = importMesh.meshes[1].clone("asteroid", null, false);
|
this._asteroidMesh.setParent(null);
|
||||||
this._rockMesh.setParent(null);
|
this._asteroidMesh.setEnabled(false);
|
||||||
this._rockMesh.setEnabled(false);
|
debugLog(this._asteroidMesh);
|
||||||
|
|
||||||
//importMesh.meshes[1].dispose();
|
|
||||||
debugLog(importMesh.meshes);
|
|
||||||
if (!this._rockMaterial) {
|
|
||||||
// Clone the original material from GLB to preserve all textures
|
|
||||||
this._originalMaterial = this._rockMesh.material.clone("asteroid-original") as PBRMaterial;
|
|
||||||
this._rockMaterial = this._rockMesh.material.clone("asteroid-original") as PBRMaterial;
|
|
||||||
debugLog('Cloned original material from GLB:', this._originalMaterial);
|
|
||||||
|
|
||||||
// Create material using GameConfig texture level
|
|
||||||
/*const config = GameConfig.getInstance();
|
|
||||||
this._rockMaterial = MaterialFactory.createAsteroidMaterial(
|
|
||||||
'asteroid-material',
|
|
||||||
config.asteroidTextureLevel,
|
|
||||||
DefaultScene.MainScene,
|
|
||||||
this._originalMaterial
|
|
||||||
) as PBRMaterial;*/
|
|
||||||
this._rockMaterial.freeze();
|
|
||||||
|
|
||||||
this._rockMesh.material = this._rockMaterial;
|
|
||||||
importMesh.meshes[1].dispose(false, true);
|
|
||||||
importMesh.meshes[0].dispose();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async createRock(i: number, position: Vector3, size: Vector3,
|
public static async createRock(i: number, position: Vector3, size: Vector3,
|
||||||
score: Observable<ScoreEvent>): Promise<Rock> {
|
linearVelocitry: Vector3, angularVelocity: Vector3, score: Observable<ScoreEvent>): Promise<Rock> {
|
||||||
|
|
||||||
const rock = new InstancedMesh("asteroid-" +i, this._rockMesh as Mesh);
|
const rock = new InstancedMesh("asteroid-" +i, this._asteroidMesh as Mesh);
|
||||||
debugLog(rock.id);
|
debugLog(rock.id);
|
||||||
rock.scaling = size;
|
rock.scaling = size;
|
||||||
rock.position = position;
|
rock.position = position;
|
||||||
@ -105,23 +84,16 @@ export class RockFactory {
|
|||||||
// Don't pass radius - let Babylon compute from scaled mesh bounds
|
// Don't pass radius - let Babylon compute from scaled mesh bounds
|
||||||
}, DefaultScene.MainScene);
|
}, DefaultScene.MainScene);
|
||||||
const body = agg.body;
|
const body = agg.body;
|
||||||
|
const constraint = new DistanceConstraint(Vector3.Distance(position, this._orbitCenter.body.transformNode.position), DefaultScene.MainScene);
|
||||||
|
body.addConstraint(this._orbitCenter.body, constraint);
|
||||||
|
|
||||||
if (!this._viewer) {
|
|
||||||
// this._viewer = new PhysicsViewer(DefaultScene.MainScene);
|
|
||||||
}
|
|
||||||
|
|
||||||
// this._viewer.showBody(body);
|
|
||||||
body.setLinearDamping(0)
|
body.setLinearDamping(0)
|
||||||
body.setMotionType(PhysicsMotionType.DYNAMIC);
|
body.setMotionType(PhysicsMotionType.DYNAMIC);
|
||||||
body.setCollisionCallbackEnabled(true);
|
body.setCollisionCallbackEnabled(true);
|
||||||
|
body.setLinearVelocity(linearVelocitry);
|
||||||
|
body.setAngularVelocity(angularVelocity);
|
||||||
body.getCollisionObservable().add((eventData) => {
|
body.getCollisionObservable().add((eventData) => {
|
||||||
if (eventData.type == 'COLLISION_STARTED') {
|
if (eventData.type == 'COLLISION_STARTED') {
|
||||||
/*debugLog('[RockFactory] Collision detected:', {
|
|
||||||
collidedWith: eventData.collidedAgainst.transformNode.id,
|
|
||||||
asteroidName: eventData.collider.transformNode.name
|
|
||||||
});*/
|
|
||||||
|
|
||||||
if ( eventData.collidedAgainst.transformNode.id == 'ammo') {
|
if ( eventData.collidedAgainst.transformNode.id == 'ammo') {
|
||||||
debugLog('[RockFactory] ASTEROID HIT! Triggering explosion...');
|
debugLog('[RockFactory] ASTEROID HIT! Triggering explosion...');
|
||||||
score.notifyObservers({score: 1, remaining: -1, message: "Asteroid Destroyed"});
|
score.notifyObservers({score: 1, remaining: -1, message: "Asteroid Destroyed"});
|
||||||
@ -149,17 +121,8 @@ export class RockFactory {
|
|||||||
eventData.collidedAgainst.dispose();
|
eventData.collidedAgainst.dispose();
|
||||||
debugLog('[RockFactory] Disposal complete');
|
debugLog('[RockFactory] Disposal complete');
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
/*debugLog('[RockFactory] Collision ended between:', {
|
|
||||||
collider: eventData.collider.transformNode.id,
|
|
||||||
collidedWith: eventData.collidedAgainst.transformNode.id
|
|
||||||
});*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
//body.setAngularVelocity(new Vector3(Math.random(), Math.random(), Math.random()));
|
|
||||||
// body.setLinearVelocity(Vector3.Random(-10, 10));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Rock(rock);
|
return new Rock(rock);
|
||||||
|
|||||||
12
src/ship.ts
12
src/ship.ts
@ -7,7 +7,7 @@ import {
|
|||||||
Observable,
|
Observable,
|
||||||
PhysicsAggregate,
|
PhysicsAggregate,
|
||||||
PhysicsMotionType,
|
PhysicsMotionType,
|
||||||
PhysicsShapeType, PointLight,
|
PhysicsShapeType,
|
||||||
SceneLoader,
|
SceneLoader,
|
||||||
StandardMaterial,
|
StandardMaterial,
|
||||||
TransformNode,
|
TransformNode,
|
||||||
@ -374,6 +374,16 @@ export class Ship {
|
|||||||
this._shooting = false;
|
this._shooting = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (controllerEvent.component.type == 'button') {
|
||||||
|
if (controllerEvent.component.id == 'a-button') {
|
||||||
|
DefaultScene.XR.baseExperience.camera.position.y = DefaultScene.XR.baseExperience.camera.position.y - .1;
|
||||||
|
}
|
||||||
|
if (controllerEvent.component.id == 'b-button') {
|
||||||
|
DefaultScene.XR.baseExperience.camera.position.y = DefaultScene.XR.baseExperience.camera.position.y + .1;
|
||||||
|
}
|
||||||
|
console.log(controllerEvent);
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,50 +1,64 @@
|
|||||||
import {
|
import {
|
||||||
AbstractMesh, LoadAssetContainerAsync, Mesh,
|
AbstractMesh,
|
||||||
|
HavokPlugin,
|
||||||
PhysicsAggregate,
|
PhysicsAggregate,
|
||||||
PhysicsMotionType,
|
PhysicsMotionType,
|
||||||
PhysicsShapeType, Scene,
|
PhysicsShapeType,
|
||||||
Vector3
|
Vector3
|
||||||
} from "@babylonjs/core";
|
} from "@babylonjs/core";
|
||||||
import {DefaultScene} from "./defaultScene";
|
import {DefaultScene} from "./defaultScene";
|
||||||
import {GameConfig} from "./gameConfig";
|
import {GameConfig} from "./gameConfig";
|
||||||
import debugLog from "./debug";
|
import debugLog from "./debug";
|
||||||
|
import loadAsset from "./utils/loadAsset";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create and load the star base mesh
|
* Create and load the star base mesh
|
||||||
* @param position - Position for the star base
|
* @param position - Position for the star base
|
||||||
* @returns Promise resolving to the loaded star base mesh
|
* @returns Promise resolving to the loaded star base mesh
|
||||||
*/
|
*/
|
||||||
export default async function buildStarBase(position: Vector3): Promise<AbstractMesh> {
|
export default class StarBase {
|
||||||
const scene = DefaultScene.MainScene;
|
public static async buildStarBase(position: Vector3): Promise<AbstractMesh> {
|
||||||
|
|
||||||
// Load the base model
|
|
||||||
const importMesh = await LoadAssetContainerAsync('/base.glb', DefaultScene.MainScene,
|
|
||||||
{
|
|
||||||
pluginOptions: {
|
|
||||||
gltf: {
|
|
||||||
enabled: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
importMesh.addAllToScene();
|
|
||||||
const starBase = Mesh.MergeMeshes(importMesh.rootNodes[0].getChildMeshes(false), true, false, null, false, true);
|
|
||||||
starBase.id = 'starBase';
|
|
||||||
starBase.name = 'starBase';
|
|
||||||
DefaultScene.MainScene.addMesh(starBase);
|
|
||||||
debugLog('imported base mesh', importMesh.meshes[0]);
|
|
||||||
|
|
||||||
starBase.position = position;
|
|
||||||
const config = GameConfig.getInstance();
|
const config = GameConfig.getInstance();
|
||||||
|
const scene = DefaultScene.MainScene;
|
||||||
|
const importMeshes = await loadAsset('base.glb');
|
||||||
|
const baseMesh = importMeshes.meshes.get('Base');
|
||||||
|
const landingMesh = importMeshes.meshes.get('BaseLandingZone');
|
||||||
|
clearParent(importMeshes.meshes, position);
|
||||||
|
|
||||||
|
|
||||||
if (config.physicsEnabled) {
|
if (config.physicsEnabled) {
|
||||||
const agg = new PhysicsAggregate(starBase, PhysicsShapeType.MESH, {
|
const agg2 = new PhysicsAggregate(baseMesh, PhysicsShapeType.MESH, {
|
||||||
mass: 10000
|
mass: 10000
|
||||||
}, scene);
|
}, scene);
|
||||||
agg.body.setMotionType(PhysicsMotionType.ANIMATED);
|
agg2.body.setMotionType(PhysicsMotionType.ANIMATED);
|
||||||
agg.body.setCollisionCallbackEnabled(true);
|
|
||||||
agg.body.getCollisionObservable().add((collidedBody) => {
|
agg2.body.getCollisionObservable().add((collidedBody) => {
|
||||||
debugLog('collidedBody', collidedBody);
|
debugLog('collidedBody', collidedBody);
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const landingAgg = new PhysicsAggregate(landingMesh, PhysicsShapeType.MESH);
|
||||||
|
landingAgg.body.setMotionType(PhysicsMotionType.ANIMATED);
|
||||||
|
landingAgg.body.getCollisionObservable().add((collidedCollidedBody) => {
|
||||||
|
console.log(collidedCollidedBody);
|
||||||
|
});
|
||||||
|
landingAgg.shape.isTrigger = true;
|
||||||
|
(DefaultScene.MainScene.getPhysicsEngine().getPhysicsPlugin() as HavokPlugin).onTriggerCollisionObservable.add((eventdata, eventState) => {
|
||||||
|
console.log(eventState);
|
||||||
|
console.log(eventdata);
|
||||||
|
})
|
||||||
|
landingAgg.body.setCollisionCallbackEnabled(true);
|
||||||
|
}
|
||||||
|
//importMesh.rootNodes[0].dispose();
|
||||||
|
return baseMesh;
|
||||||
}
|
}
|
||||||
importMesh.rootNodes[0].dispose();
|
}
|
||||||
return starBase;
|
function clearParent (meshes: Map<string, AbstractMesh>, position?: Vector3) {
|
||||||
|
meshes.forEach((mesh) => {
|
||||||
|
mesh.setParent(null);
|
||||||
|
if (position) {
|
||||||
|
mesh.position = position;
|
||||||
|
}
|
||||||
|
|
||||||
|
DefaultScene.MainScene.addMesh(mesh);
|
||||||
|
})
|
||||||
}
|
}
|
||||||
301
src/utils/blenderExporter.ts
Normal file
301
src/utils/blenderExporter.ts
Normal file
@ -0,0 +1,301 @@
|
|||||||
|
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<BlenderExportResult> {
|
||||||
|
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<BlenderExportResult[]> {
|
||||||
|
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}`);
|
||||||
|
};
|
||||||
|
}
|
||||||
15
src/utils/loadAsset.ts
Normal file
15
src/utils/loadAsset.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import {DefaultScene} from "../defaultScene";
|
||||||
|
import {AbstractMesh, AssetContainer, LoadAssetContainerAsync} from "@babylonjs/core";
|
||||||
|
|
||||||
|
export type LoadedAsset = {
|
||||||
|
container: AssetContainer,
|
||||||
|
meshes: Map<string, AbstractMesh>,
|
||||||
|
}
|
||||||
|
export default async function loadAsset(file: string, theme: string = "default"): Promise<LoadedAsset> {
|
||||||
|
const container = await LoadAssetContainerAsync(`assets/themes/${theme}/models/${file}`, DefaultScene.MainScene);
|
||||||
|
const map: Map<string, AbstractMesh> = new Map();
|
||||||
|
for (const mesh of container.rootNodes[0].getChildMeshes(false)) {
|
||||||
|
map.set(mesh.id, mesh);
|
||||||
|
}
|
||||||
|
return {container: container, meshes: map};
|
||||||
|
}
|
||||||
BIN
themes/default/asteroid.blend
Normal file
BIN
themes/default/asteroid.blend
Normal file
Binary file not shown.
BIN
themes/default/asteroid.jpg
Normal file
BIN
themes/default/asteroid.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 MiB |
BIN
themes/default/base.blend
Normal file
BIN
themes/default/base.blend
Normal file
Binary file not shown.
BIN
themes/default/ship.blend
Normal file
BIN
themes/default/ship.blend
Normal file
Binary file not shown.
BIN
themes/default/shiplights.jpg
Normal file
BIN
themes/default/shiplights.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 465 KiB |
Loading…
Reference in New Issue
Block a user