diff --git a/.cursor/rules/hyperfy.mdc b/.cursor/rules/hyperfy.mdc new file mode 100644 index 00000000..666bca1c --- /dev/null +++ b/.cursor/rules/hyperfy.mdc @@ -0,0 +1,912 @@ +--- +description: # Developer Style and Syntax Ruleset +globs: *.js +alwaysApply: true +--- +## 1. Project Structure +- **Modular Organization**: Code is organized into clear directories (`src/core`, `src/client`, `src/server`) +- **Separation of Concerns**: Clear separation between client, server, and core shared functionality +- **System-based Architecture**: Uses a component/system architecture pattern similar to ECS (Entity Component System) +- **Asset Management**: Organized asset management with hashing and caching for immutability +- **Core/Client/Server Split**: Clear separation between shared code (core), client-specific, and server-specific functionality +- **Utility Separation**: Utilities are split based on environment (utils.js, utils-client.js, utils-server.js) +- **Bootstrap Process**: Clear bootstrapping process for both client and server +- **Multi-layered Architecture**: Hierarchical organization with World → Systems → Entities → Nodes +- **Component Registration**: Components are registered with the world at initialization time + +## 2. JavaScript Style +- **Modern JavaScript**: Uses ES6+ features extensively + - Arrow functions for callbacks and short methods + - Async/await for asynchronous operations + - Object destructuring and spread operators + - Class-based object-oriented programming with inheritance + - Import/export modules +- **Functional Patterns**: Uses functional programming patterns where appropriate (map, filter, etc.) +- **Immutable Data Patterns**: Tendency to create new objects rather than modify existing ones +- **Method Binding**: Uses class property arrow functions for methods that need `this` binding +- **Default Parameters**: Consistently uses default parameters for optional values +- **Promise Handling**: Clean promise handling with proper error management +- **ESM Modules**: Uses ES modules with named exports/imports +- **Singleton Patterns**: Uses singleton pattern for resources that should be instantiated once +- **Getter/Setter Patterns**: Extensive use of getters and setters for controlled property access +- **Method Chaining**: Methods often return `this` to enable chaining +- **Consistent Initialization**: Consistent patterns for object initialization and default values + +## 3. Naming Conventions +- **camelCase**: Used for variables, functions, methods, and instances +- **PascalCase**: Used for class names and constructor functions +- **Descriptive Names**: Clear, descriptive names that indicate purpose +- **Method Naming Patterns**: Lifecycle methods follow clear naming patterns (pre/post prefixes) +- **Constants**: Uses UPPER_SNAKE_CASE for constants +- **Private Properties/Methods**: No strict pattern for private members, but sometimes prefixed with underscore +- **Event Handlers**: Prefix with 'on' for event handlers (e.g., `onKeyDown`, `onPointerMove`) +- **Consistent Abbreviations**: Uses consistent abbreviations (e.g., 'lmb' for left mouse button) +- **Boolean Variables**: Boolean variables often start with 'is', 'has', or 'should' +- **Vector/Quaternion/Matrix Variables**: Uses clear naming conventions for 3D math (v1, q1, m1) +- **Temporary Variables**: Consistently uses underscore prefix for temporary variables (_v1, _q1) +- **Interface Naming**: Clear naming for interfaces and implementation classes + +## 4. Code Organization +- **Class Structure**: Well-organized classes with clear inheritance patterns +- **Lifecycle Methods**: Consistent use of lifecycle methods (init, start, update, etc.) +- **Event-driven Architecture**: Uses EventEmitter pattern for communication between components +- **Modular Design**: Functions and classes are modular and focused on a single responsibility +- **Proxy Pattern**: Uses JavaScript Proxy objects for advanced property access +- **Reusable Components**: Emphasis on creating reusable, composable components +- **Vector Reuse**: Creates and reuses THREE.js vectors to avoid garbage collection +- **Lazy Initialization**: Resources are initialized only when needed +- **Memoization**: Cached results for expensive operations +- **Default Parameter Objects**: Extensive use of default parameter objects +- **Component Hierarchy**: Clear parent-child relationships between components +- **Property Validation**: Validates property values before setting them +- **Consistent Method Organization**: Similar classes have similar method organization + +## 5. Documentation +- **JSDoc Comments**: Uses JSDoc-style comments for explaining functionality +- **Inline Comments**: Concise explanatory comments for complex logic +- **Clear Method Documentation**: Comments explaining the purpose of lifecycle methods +- **Function Documentation**: Clearly documents function parameters and returns +- **Code Section Markers**: Uses comment blocks to separate logical sections +- **TODO Comments**: Uses TODO comments to mark areas for future improvement +- **Implementation Notes**: Comments explaining why certain implementation decisions were made +- **Multi-line Documentation**: Substantial documentation for complex functions or classes +- **Header Documentation**: Clear file header documentation explaining file purpose +- **Constant Documentation**: Documents the purpose and valid values for constants +- **Reference Documentation**: Links to external resources for complex algorithms +- **Performance Notes**: Comments about performance considerations + +## 6. Error Handling +- **Graceful Error Handling**: Try/catch blocks for error handling +- **Error Logging**: Consistent error logging +- **Defensive Programming**: Checks for null/undefined values before operations +- **Error Prevention**: Uses defensive programming to avoid common issues +- **Fallbacks**: Implements fallback behavior when errors occur +- **Error Wrapping**: Often wraps async operations in try/catch blocks +- **Error Reporting**: Configurable error reporting (development vs. production) +- **Promise Error Handling**: Properly handles promise rejections +- **Input Validation**: Validates inputs to methods to prevent errors +- **Type Checking**: Uses lodash's isString, isNumber, etc. for type checking +- **Enumeration Validation**: Validates that values match expected enumerations + +## 7. Application Architecture +- **Game Loop Pattern**: Uses a fixed timestep game loop with accumulator for physics +- **Systems and Components**: Organized as systems that operate on entities/components +- **Event-Based Communication**: Uses events for cross-system communication +- **State Management**: Clear patterns for managing application state +- **Simulation Steps**: Well-defined simulation steps (fixed update, update, late update) +- **Time Management**: Sophisticated handling of time, delta time, and interpolation +- **Dependency Injection**: Systems are passed dependencies via constructor +- **Command Pattern**: Uses command pattern for undo/redo operations +- **Observer Pattern**: Extensive use of observer pattern for events +- **Entity-Component Structure**: Clear separation between entities and their components +- **Hierarchical Scene Graph**: Node-based scene graph with parent-child relationships +- **Client-Server Split**: Clear separation between client and server functionality +- **Systems Registration**: Systems are registered with the world in a consistent order + +## 8. Networking + +### Protocol and Communication +- **WebSockets**: Used for real-time communication + - WebSocket connections with authentication tokens + - Binary message format for efficiency + - Socket wrapper class for abstraction +- **Binary Protocol**: Efficient binary messaging + - msgpackr for binary serialization + - Packet ID mapping for compact messages + - Method name to ID conversion + +### Connection Management +- **Socket Handling**: Socket lifecycle management + - Connection establishment with authentication + - Message handling and dispatch + - Proper disconnection handling +- **Heartbeat System**: Connection health monitoring + - Ping/pong mechanism for connection status + - Alive status tracking + - Socket termination for inactive connections + +### Data Synchronization +- **Entity Updates**: Entity state synchronization + - Position and rotation updates + - Added/modified/removed entity events + - Blueprint synchronization +- **Snapshot System**: State synchronization + - Initial state snapshots + - Time synchronization + - Chat history and entity state + +## 9. Utility Functions +- **Small, Focused Utilities**: Utility functions are small, focused, and reusable +- **Exported Functions**: Utilities are exported as named functions +- **Default Parameters**: Uses default parameters when appropriate +- **Utility File Organization**: Separates utilities by context (client, server, shared) +- **Pure Functions**: Preference for pure functions that don't have side effects +- **Common Patterns**: Common operations are abstracted into reusable utility functions +- **Cryptographic Functions**: Consistent patterns for hashing and security operations +- **Environment-specific Implementations**: Same utility functions implemented differently for client/server +- **JWT Pattern**: Consistent JWT creation and verification +- **Vector Math Utilities**: Specialized utilities for 3D vector operations +- **Role Management Utilities**: Utilities for handling user roles and permissions +- **ID Generation**: Consistent pattern for generating unique IDs + +## 10. React (Client-Side) +- **Functional Components**: Uses functional React components with hooks +- **Hooks Pattern**: Uses useState, useEffect, useRef, and useMemo hooks +- **CSS-in-JS**: Uses a CSS-in-JS solution (@firebolt-dev/css) +- **Component Composition**: Builds complex UIs through component composition +- **React Refs**: Uses refs for DOM manipulation and imperative actions +- **Effect Cleanup**: Properly manages effect cleanup in useEffect +- **Component Memoization**: Uses useMemo and useCallback for performance +- **Imperative Handles**: Uses imperative handles for cross-component communication +- **Custom Hooks**: Creates custom hooks for reusable logic +- **Lucide Icons**: Uses Lucide for consistent icon design +- **Responsive Design**: Adapts UI based on screen size +- **Conditional Rendering**: Clean patterns for conditional component rendering +- **Event Handling**: Consistent patterns for handling user events +- **UI State Management**: Manages UI state through React state and context + +## 11. Server-Side +- **Fastify**: Uses Fastify for HTTP server +- **Middleware Pattern**: Clear middleware registration pattern +- **Environment Variables**: Uses environment variables for configuration +- **Graceful Shutdown**: Implements graceful shutdown handling +- **Database Migrations**: Uses a migration system for database schema changes +- **API Routes**: Clearly defined API routes with appropriate HTTP methods +- **Static File Serving**: Efficient serving of static files with appropriate headers +- **Session Management**: Handles user sessions securely +- **Multipart Uploads**: Supports file uploads with size limits and validation +- **dotenv-flow**: Uses dotenv-flow for environment variable loading +- **Source Map Support**: Enables source map support for better debugging +- **ESM Compatibility**: Ensures ESM compatibility with global __dirname +- **Health Checks**: Implements health check endpoints +- **Status Endpoints**: Provides status information endpoints +- **Authentication Middleware**: Clean authentication middleware implementation + +## 12. Formatting and Syntax +- **Semicolons**: Consistent use of semicolons at the end of statements +- **Braces**: Opening braces on the same line as statements +- **2-Space Indentation**: Uses 2 spaces for indentation +- **Arrow Function Style**: Concise arrow functions for callbacks +- **Template Literals**: Uses template literals for string interpolation +- **Short-Circuit Evaluation**: Uses `&&` and `||` for conditional rendering and default values +- **Object Shorthand**: Uses object property shorthand when variable names match +- **Destructuring Assignment**: Extensively uses destructuring for objects and arrays +- **Function Parameter Formatting**: Parameters on separate lines for long parameter lists +- **Consistent Spacing**: Consistent spacing around operators and after commas +- **Import Organization**: Organizes imports by type and importance +- **Code Block Organization**: Logical organization of code blocks within methods +- **Default Value Organization**: Organizes default values in a consistent way + +## 13. Performance Considerations +- **Fixed Timestep**: Implements fixed timestep physics loop +- **Accumulator Pattern**: Uses accumulator for physics simulation +- **Caching**: Implements aggressive caching for static assets +- **Binary Protocol**: Uses efficient binary protocol for network communication +- **Object Pooling**: Re-uses objects to minimize garbage collection +- **Spatial Partitioning**: Implements spatial partitioning for efficient collision detection +- **Web Worker Usage**: Uses web workers for computationally intensive tasks +- **Throttling/Debouncing**: Applies throttling and debouncing to expensive operations +- **Resource Reuse**: Reuses resources like renderers across instances +- **Memory Management**: Careful management of memory in WebGL contexts +- **Matrix Property Caching**: Caches matrix calculations for performance +- **Dirty Flagging**: Uses dirty flags to track when recalculations are needed +- **Entity Activation Control**: Only activates entities when needed +- **Render Loop Optimization**: Optimizes render loop for performance + +## 14. 3D Graphics Patterns +- **THREE.js Integration**: Deep integration with THREE.js for 3D rendering + - Custom extensions to THREE.js core classes + - Bridging between THREE.js transforms and PhysX transforms + - Vector/quaternion conversion utilities + - Custom shader material integration + - Seamless physics-graphics synchronization +- **Object Pooling**: Reuses 3D objects and vectors to minimize garbage collection + - Persistent vector and quaternion objects for calculations + - Object recycling for particle systems + - Mesh instance reuse + - Texture atlas implementation for batching +- **Render Loop**: Efficient render loop with request animation frame + - Synchronized with physics updates + - Throttling for performance optimization + - Adaptive quality settings + - Frame dropping for consistent simulation +- **Camera Management**: Sophisticated camera management with field of view adjustments + - Adaptive FOV based on screen aspect ratio + - Camera collision and environment awareness + - Smooth camera transitions and easing + - Support for multiple camera modes (first-person, third-person, etc.) +- **Material Handling**: Consistent patterns for material creation and management + - Material pooling and reuse + - PBR (Physically Based Rendering) support + - Material property animation + - Custom shader integration via three-custom-shader-material +- **Lighting Setup**: Standard approaches to lighting setup + - HDR environment maps for image-based lighting + - Cascaded shadow maps implementation + - Dynamic lighting with performance considerations + - Light probes for global illumination approximation +- **Physics Integration**: Clean integration between rendering and physics + - Matrix-based synchronization between PhysX and THREE.js + - Interpolation for smooth rendering between physics steps + - Debug visualization of physics objects + - Performance optimization with visibility culling +- **Scene Graph**: Well-organized scene graph with clear parent-child relationships + - Node-based architecture + - Transformation inheritance + - Scene traversal optimization + - Dirty flag system for matrix updates +- **HDR Lighting**: Uses HDR environment maps for realistic lighting + - RGBE texture format support + - Tone mapping for display adaptation + - Environment map prefiltering + - IBL (Image Based Lighting) implementation +- **Adaptive FOV**: Adjusts field of view based on aspect ratio + - Vertical FOV preservation + - Widescreen compensation + - VR-specific FOV handling +- **Custom Shaders**: Uses custom shader materials for advanced effects + - Material extension pattern + - GLSL snippet integration + - ShaderLib extension + - Custom uniform management +- **Billboard System**: Implements billboarding for UI elements + - Camera-facing billboards + - Y-axis aligned billboards + - Billboard matrix calculation optimization + - Quad-based billboarding for particles +- **LOD System**: Level of detail system for performance optimization + - Distance-based LOD switching + - Screen-space error metrics + - Smooth LOD transitions + - Asset management for multiple detail levels +- **Pivot System**: Flexible pivot point system for positioning elements + - Custom pivot definitions (center, edges, corners) + - Runtime pivot adjustment + - Pivot transformation utilities + - Anchor point system for UI +- **Rendering Optimizations**: + - View frustum culling + - Occlusion culling + - Instanced rendering for similar objects + - Batching for static objects + - Texture atlasing for reducing draw calls + - Material sorting for minimizing state changes + - WebGL extension detection and fallbacks + - Shader complexity adaptation based on device capabilities + +## 15. Database Patterns +- **Knex Query Builder**: Uses Knex.js as SQL query builder +- **Migration System**: Implements a version-based migration system +- **Schema Validation**: Validates data before database operations +- **Transaction Support**: Uses transactions for multi-step operations +- **Connection Management**: Properly manages database connections +- **Data Serialization**: Consistent patterns for serializing/deserializing data to/from the database +- **Schema Evolution**: Carefully handles schema evolution without breaking changes +- **SQLite for Development**: Uses SQLite for local development database +- **Version-based Migrations**: Keeps track of schema version in a config table +- **Data Normalization**: Properly normalizes data for database storage +- **Migration Versioning**: Each migration is clearly versioned and never modified +- **Migration Documentation**: Each migration is well-documented with its purpose +- **Entity Persistence**: Clean patterns for persisting entities to the database + +## 16. Testing Patterns +- **Minimal Error Classes**: Clear error class hierarchy with descriptive error messages +- **Defensive Programming**: Validates inputs before operations +- **Error Recovery**: Attempts to recover from errors when possible +- **Fallback Values**: Provides fallback values for error scenarios +- **Centralized Error Handling**: Centralizes error handling in appropriate places +- **Safe Defaults**: Provides safe default values in failure cases +- **Validation Functions**: Dedicated functions for validating specific data types and formats + +## 17. Security Considerations + +### Script Sandboxing +- **SES Integration**: Secure ECMAScript implementation + - Lockdown configuration for security boundaries + - Error handling configuration options + - Script execution within compartments +- **Compartment System**: Secure script execution + - Limited global object access + - Controlled API surface area + - Predefined safe functions and objects + +### Access Controls +- **Reference Protection**: Node reference security + - Secure reference accessor pattern + - Controlled access to internal references + - Prevention of unauthorized access +- **JWT Authentication**: Authentication system + - Token-based authentication + - Role-based access control + - Token validation and verification + +## 18. Physics System Design +- **PhysX Integration**: Clean integration with PhysX physics engine + - Custom fork of physx-js-webidl with WebAssembly bindings + - Separate loading mechanisms for client and server environments + - Extension of THREE.js objects with PhysX functionality + - Version tracking with `PHYSX.PHYSICS_VERSION` for compatibility + - PxFoundation initialization with custom allocator and error callback +- **Contact Events**: Sophisticated handling of contact events + - Custom callback implementation for PxSimulationEventCallback + - Filtering of contact events based on shape properties + - Clean event propagation to affected entities + - Separate handling for touch found and touch lost events + - Contact point data extraction and normalization +- **Collision Filtering**: Uses layer masks for collision filtering + - PxFilterData with group and mask values for precise collision control + - Custom query filtering with PxQueryFilterCallback implementation + - Shape-specific collision configuration + - Separate filtering for simulation shapes and scene query shapes +- **Physics Debugging**: Tools for debugging physics issues + - Visual debugging toggles for development builds + - Spatial visualization of physics objects + - Contact point visualization + - Bounds visualization for collision shapes +- **Raycasting API**: Clean API for raycasting and overlap tests + - PxRaycastResult and PxSweepResult implementations + - Reusable vectors to minimize garbage collection + - Consistent query filter data and hit flags + - Support for multiple hit modes (closest, any, multiple) + - Separation of simulation and scene query systems +- **Interpolation**: Smooth interpolation between physics steps + - Uses accumulator pattern for deterministic physics + - Alpha interpolation for visual smoothness between steps + - Explicit control over interpolation for kinematic bodies + - Compensation for network latency in visual representation +- **WASM Integration**: Clean integration with WebAssembly for physics + - Custom build configuration with PxWasmBindings + - Optimized WASM binaries for both server and client + - Proper memory management across JavaScript/WASM boundary + - Export naming configuration for global access (`EXPORT_NAME=PhysX`) +- **Physics Material Management**: Consistent patterns for physics materials + - Material properties configuration for friction and restitution + - Material sharing across similar objects + - Dynamic material property adjustment +- **Collision Callbacks**: Well-structured collision callback system + - Trigger callbacks (onTriggerEnter, onTriggerLeave) + - Contact callbacks (onContactStart, onContactEnd) + - Proxy objects for safe access to callback data + - Event propagation to responsible entities +- **Custom Contact Events**: Sophisticated custom contact event creation + - Filtering based on object types and properties + - Transformation of low-level physics events into high-level game events + - Support for custom contact modification +- **Rigid Body Types**: Clear distinction between static, kinematic, and dynamic bodies + - Type-specific initialization logic + - Appropriate mass and inertia setup based on type + - Flag configuration for kinematic bodies + - Performance optimizations based on body type + - Early pair elimination for kinematic-kinematic interactions +- **Physics Property Access**: Clean API for accessing and setting physics properties + - Getters and setters for common physics properties + - Safe property access through proxy objects + - Validation of physics property values +- **Physics Force Application**: Consistent patterns for applying forces and torques + - Support for different force modes (force, impulse, velocity change) + - Centralized force application via systems + - Helper methods for common force operations +- **Collision Shape Management**: Flexible collision shape attachment system + - Support for box, sphere, convex mesh, and triangle mesh geometries + - Shape flags for query vs. simulation shapes + - Dynamic addition and removal of shapes from rigid bodies + - Mesh scaling and transformation + - Convex hull generation for complex shapes +- **Scene Configuration**: Sophisticated physics scene setup + - Configurable gravity, broadphase, and solver types + - Scene flags for CCD and active actor tracking + - Custom CPU dispatcher configuration + - TGS solver type for improved stability + - GPU-accelerated broadphase where supported +- **Scene Query System**: Optimized scene query implementation + - Separation of collision broadphase and scene query system + - Efficient raycasting and overlap checks + - Specialized structure for scene queries to improve performance + - Pruning strategy selection for different use cases + - Dynamic tree rebuild rate optimization +- **Character Controller**: Custom character controller implementation + - Uses scene queries and penetration depth computation + - Sweep-based movement with step resolution + - Depenetration logic for resolving overlaps + - Collision filtering specific to characters + - Support for slopes and steps +- **Performance Optimizations**: + - Fixed timestep physics to avoid stability issues + - "Well of Despair" avoidance through careful timestep management + - Scene query optimization with tight bounds for convex meshes + - Dynamic tree rebuild rate configuration + - Pruning structure selection based on scene characteristics + - Double-buffering approach for scene queries during simulation + - Separate broadphase for simulation and scene queries + +## 19. External Library Integration +- **Clean Module Loading**: Clean patterns for loading external modules +- **Library Initialization**: Consistent patterns for library initialization +- **Promise-based Loading**: Uses promises for asynchronous resource loading +- **Global Access**: Provides global access to important libraries +- **Lazy Loading**: Loads libraries only when needed +- **Version Management**: Keeps track of library versions +- **Library Extension**: Extends libraries with custom functionality +- **Consistent API Wrapping**: Wraps external libraries in consistent APIs +- **Error Handling for External Libraries**: Properly handles errors from external libraries + +## 20. Entity System Design +- **Base Entity Class**: Clean base entity class with core functionality +- **Specialized Entity Types**: Specialized entity classes for specific purposes +- **Entity Serialization**: Consistent patterns for entity serialization +- **Entity Creation**: Clear factory patterns for entity creation +- **Entity Management**: Centralized entity management +- **Entity Events**: Well-structured entity event system +- **Entity Proxies**: Creates proxies for safer entity access +- **Local/Remote Distinction**: Clear distinction between local and remote entities +- **Player-specific Entities**: Special handling for player entities +- **Entity Positioning**: Consistent systems for entity positioning and movement + +## 21. Node System Design +- **Base Node Class**: Flexible base node class with transformation capabilities +- **Specialized Node Types**: Specialized node classes for specific purposes (UI, Audio, Mesh, etc.) +- **Node Transformation**: Sophisticated transformation system with matrices +- **Node Hierarchy**: Clear parent-child relationships between nodes +- **Node Activation**: Nodes are activated and deactivated in a controlled manner +- **Node Property Access**: Consistent getter/setter patterns for node properties +- **Node Proxies**: Creates proxies for safer node access +- **Node Copy System**: Consistent patterns for copying nodes +- **Node Event System**: Well-structured node event system +- **Node Serialization**: Consistent patterns for node serialization + +## 22. Audio System Design +- **Positional Audio**: Sophisticated positional audio system + - Integration with THREE.js spatial audio + - Multiple audio distance models (linear, inverse, exponential) +- **Audio Groups**: Organized audio groups for different types of sounds + - Dedicated music and SFX groups + - Volume control at both group and individual sound levels +- **Distance Models**: Multiple distance models for different audio behaviors + - Configurable reference distance and maximum distance + - Appropriate rolloff factors for different sound types +- **Audio Parameter Control**: Fine-grained control over audio parameters + - Volume, loop settings, spatial positioning + - Cone settings for directional audio +- **Audio Lifecycle Management**: Clean lifecycle management for audio resources + - Proper disposal of audio sources and nodes + - Dynamic creation and connection of audio graph +- **Spatial Audio**: 3D spatial audio integrated with the scene + - Automatic positioning based on node transformation + - WebAudio API panners for spatial effects +- **Audio Resource Management**: Efficient management of audio resources + - Caching of audio buffers + - Lazy loading of audio resources +- **Audio State Control**: Play, pause, stop functionality with appropriate state handling + - Method chaining for audio control operations + - Event handling for audio state changes +- **Volume Control**: Consistent volume control patterns + - Gain node management + - Cross-fading capabilities + +## 23. UI System Design + +### Component Architecture +- **Declarative UI**: Component-based UI architecture + - Node-based UI components + - Property-driven configuration + - Hierarchical structure + +### Layout Engine +- **Yoga Integration**: Flexbox-like layout system using Yoga + - Flexbox direction, justification, and alignment + - Gap and padding support + - Measurement functions for dynamic content + - Edge settings for borders and margins + +### Styling System +- **Styling Properties**: Comprehensive styling options + - Background colors and transparency + - Border width, color, and radius + - Text styling with font families and sizes + - Image rendering options + +### Rendering +- **Canvas-Based Rendering**: UI drawn using HTML Canvas + - CanvasTexture for THREE.js integration + - Context2D drawing operations + - Texture optimizations with anisotropy + +### Interaction +- **Hit Testing**: Spatial hit testing for UI elements + - Raycasting from 3D to 2D coordinates + - Hierarchical hit testing through UI tree + - Event coordinates translated to canvas space + +### Positioning +- **3D Integration**: UI planes positioned in 3D space + - PlaneGeometry with configurable dimensions + - Pivot point options for alignment + - Billboard options for camera-facing UI + - Scale calculation based on screen metrics + +## 24. File Structure and Organization + +### Overview +The project follows a clear three-part division: +- `src/core/` - Core functionality shared between client and server +- `src/client/` - Client-specific code +- `src/server/` - Server-specific code + +### File Structure Tree + +``` +src/ +├── core/ # Shared functionality between client and server +│ ├── assets/ # Core assets (copied to world assets directory at startup) +│ ├── entities/ # Entity definitions +│ │ ├── App.js # Application entity - manages complex application logic +│ │ ├── Entity.js # Base entity class - foundation for all entities +│ │ ├── Player.js # Base player entity - common player functionality +│ │ ├── PlayerLocal.js # Client-side player implementation - handles local player controls +│ │ └── PlayerRemote.js # Remote player representation - handles networked players +│ │ +│ ├── extras/ # Additional utilities and helpers +│ │ ├── ControlPriorities.js # Control system priorities +│ │ ├── Layers.js # Layer definitions for rendering and physics +│ │ ├── bindRotations.js # Utility for binding rotation controls +│ │ ├── buttons.js # Input button definitions and mappings +│ │ ├── createNode.js # Factory for creating nodes +│ │ ├── createPlayerProxy.js # Creates proxy for safer player access +│ │ ├── extendThreePhysX.js # Extends THREE.js with PhysX functionality +│ │ ├── fillRoundRect.js # Utility for drawing rounded rectangles +│ │ ├── general.js # General purpose constants and utilities +│ │ ├── playerEmotes.js # Player emote definitions and handlers +│ │ ├── simpleCamLerp.js # Camera interpolation utility +│ │ ├── three.js # THREE.js re-exports and extensions +│ │ └── yoga.js # Layout engine constants and types +│ │ +│ ├── libs/ # Third-party library integrations +│ │ ├── csm/ # Cascaded Shadow Maps implementation +│ │ ├── stats-gl/ # WebGL performance monitoring +│ │ ├── three-custom-shader-material/ # Custom shader material implementation +│ │ └── three-vrm/ # VRM avatar support for THREE.js +│ │ +│ ├── nodes/ # Node definitions (building blocks for entities) +│ │ ├── Action.js # Action node for handling interactive actions +│ │ ├── Anchor.js # Anchor node for spatial anchoring +│ │ ├── Audio.js # Audio node for 3D spatial audio +│ │ ├── Avatar.js # Avatar node for character representation +│ │ ├── Collider.js # Collider node for physics collisions +│ │ ├── Controller.js # Controller node for input handling +│ │ ├── Group.js # Group node for organizing hierarchies +│ │ ├── Joint.js # Joint node for physics constraints +│ │ ├── LOD.js # Level of Detail node for performance optimization +│ │ ├── Mesh.js # Mesh node for 3D geometry +│ │ ├── Nametag.js # Nametag node for displaying player names +│ │ ├── Node.js # Base node class - foundation for all nodes +│ │ ├── RigidBody.js # RigidBody node for physics simulation +│ │ ├── Sky.js # Sky node for environment visualization +│ │ ├── Snap.js # Snap node for object snapping +│ │ ├── UI.js # Base UI node for user interface elements +│ │ ├── UIImage.js # UI image node for displaying images +│ │ ├── UIText.js # UI text node for displaying text +│ │ ├── UIView.js # UI view container node +│ │ └── index.js # Node exports and registration +│ │ +│ ├── systems/ # System definitions (game logic components) +│ │ ├── Anchors.js # System for managing spatial anchors +│ │ ├── Blueprints.js # System for managing object blueprints +│ │ ├── Chat.js # System for player chat functionality +│ │ ├── Client.js # Base client system +│ │ ├── ClientActions.js # System for client-side actions +│ │ ├── ClientAudio.js # System for client-side audio +│ │ ├── ClientBuilder.js # System for client-side world building +│ │ ├── ClientControls.js # System for handling user input +│ │ ├── ClientEnvironment.js # System for client-side environment +│ │ ├── ClientGraphics.js # System for rendering and graphics +│ │ ├── ClientLoader.js # System for loading assets +│ │ ├── ClientNetwork.js # System for client-side networking +│ │ ├── ClientPrefs.js # System for client preferences +│ │ ├── ClientStats.js # System for performance monitoring +│ │ ├── ClientTarget.js # System for targeting and selection +│ │ ├── Entities.js # System for entity management +│ │ ├── Events.js # System for event handling +│ │ ├── LODs.js # System for level of detail management +│ │ ├── Nametags.js # System for nametag rendering +│ │ ├── Physics.js # System for physics simulation +│ │ ├── Scripts.js # System for scripting/code execution +│ │ ├── Server.js # Base server system +│ │ ├── ServerLoader.js # System for server-side asset loading +│ │ ├── ServerNetwork.js # System for server-side networking +│ │ ├── Snaps.js # System for object snapping +│ │ ├── Stage.js # System for managing the world stage +│ │ ├── System.js # Base system class - foundation for all systems +│ │ └── XR.js # System for XR (VR/AR) support +│ │ +│ ├── Socket.js # WebSocket wrapper with packet handling +│ ├── World.js # Core world class - main simulation container +│ ├── createClientWorld.js # Factory for creating client-side world +│ ├── createServerWorld.js # Factory for creating server-side world +│ ├── lockdown.js # Security lockdown configuration +│ ├── packets.js # Network packet definitions and handling +│ ├── storage.js # Data storage and persistence +│ ├── utils-client.js # Client-specific utilities +│ ├── utils-server.js # Server-specific utilities +│ └── utils.js # Shared utilities +│ +├── client/ # Client-specific code +│ ├── components/ # React UI components +│ │ ├── AppsPane.js # UI panel for application management +│ │ ├── AvatarPane.js # UI panel for avatar customization +│ │ ├── ChatBox.js # UI component for chat interface +│ │ ├── CodePane.js # UI panel for code editing +│ │ ├── ContextWheel.js # Radial context menu component +│ │ ├── GUI.js # Main GUI component containing all UI elements +│ │ ├── Inputs.js # Input handling components +│ │ ├── InspectPane.js # UI panel for object inspection +│ │ ├── MouseLeftIcon.js # Left mouse button icon +│ │ ├── MouseRightIcon.js # Right mouse button icon +│ │ ├── MouseWheelIcon.js # Mouse wheel icon +│ │ ├── SettingsPane.js # UI panel for settings +│ │ ├── cls.js # CSS class utility +│ │ ├── useElemSize.js # Hook for tracking element size +│ │ ├── usePane.js # Hook for managing UI panels +│ │ └── useUpdate.js # Hook for component updates +│ │ +│ ├── public/ # Static public assets +│ ├── AvatarPreview.js # Avatar preview renderer +│ ├── index.js # Client entry point +│ ├── loadPhysX.js # PhysX loader for client +│ └── utils.js # Client-specific utilities +│ +└── server/ # Server-specific code + ├── physx/ # PhysX bindings for server + │ ├── loadPhysX.js # PhysX loader for server + │ ├── physx-js-webidl.js # PhysX JavaScript bindings + │ └── physx-js-webidl.wasm # PhysX WebAssembly module + │ + ├── bootstrap.js # Server bootstrap process + ├── db.js # Database management and migrations + └── index.js # Server entry point +``` + +### Key File Descriptions + +#### Core Files +- **World.js**: The central simulation container that manages systems, game loop, and timing. +- **System.js**: Base class for all systems with lifecycle methods (init, start, update, etc.). +- **Node.js**: Base class for scene nodes with transformation and hierarchy functionality. +- **Entity.js**: Base class for entities that exist in the world. +- **Socket.js**: WebSocket wrapper for network communication. +- **packets.js**: Defines network protocol and packet structure. + +#### Client Files +- **index.js**: Client entry point that initializes React and the client world. +- **AvatarPreview.js**: Handles rendering and customization of player avatars. +- **GUI.js**: Main UI component that manages all interface elements. +- **loadPhysX.js**: Loads the PhysX physics engine on the client. + +#### Server Files +- **index.js**: Server entry point that initializes Fastify and the server world. +- **db.js**: Manages database connection and migrations. +- **bootstrap.js**: Handles server startup process and environment setup. + +#### System Files +- **Physics.js**: Handles physics simulation with PhysX integration. +- **ClientControls.js**: Manages user input and control systems. +- **ClientNetwork.js**: Handles client-side networking. +- **ServerNetwork.js**: Handles server-side networking. +- **ClientGraphics.js**: Manages rendering and visual effects. + +#### Node Files +- **Audio.js**: Handles 3D spatial audio. +- **RigidBody.js**: Handles physics bodies and interactions. +- **UI.js**: Base class for UI elements rendered in the 3D world. +- **Mesh.js**: Handles 3D geometry and materials. +- **Avatar.js**: Manages character avatars and animations. + +## 25. Game Loop and Time Management + +### Game Loop Architecture +- **Fixed Timestep**: Uses a fixed timestep model with accumulator for physics simulation + - Predictable and deterministic physics behavior + - Consistent simulation regardless of frame rate + - Standard practice for physics engines like PhysX + - Prevents "tunneling" issues with fast-moving objects +- **Frame Management**: Tracks frame count and time for consistent updates + - Global frame counter for synchronization + - Time dilation support for effects like slow-motion + - Smooth handling of frame rate fluctuations +- **Delta Time Handling**: Sophisticated handling of delta time with clamping for stability + - Maximum delta time cap to prevent "spiral of death" + - Minimum delta time threshold to handle unusual timing cases + - Compensation for browser throttling during background tabs +- **Time Accumulation**: Accumulates time to determine when physics steps should occur + - Classic accumulator pattern for fixed timestep simulation + - Leftover time preservation for accurate simulation + - Prevents small time steps that would hurt performance +- **Multi-phase Updates**: Clear separation between different update phases: + - `preTick`: Performance monitoring begins, frame setup + - `preFixedUpdate`: Preparation before physics steps + - `fixedUpdate`: Updates that need to run at fixed intervals + - `postFixedUpdate`: Actions after physics simulation + - `preUpdate`: Preparation for variable-rate updates + - `update`: Main game logic updates + - `postUpdate`: Post-processing of updates + - `lateUpdate`: Updates that need to run after main updates + - `postLateUpdate`: Final cleanup of update cycle + - `commit`: Commit changes to rendering or output + - `postTick`: Performance monitoring ends, frame cleanup + +### Interpolation +- **Alpha Calculation**: Calculates interpolation alpha based on accumulator and fixed timestep + - Smooth blending between physics states + - Normalized value between 0 and 1 for linear interpolation + - Used for visual representation of physical objects +- **Physics Interpolation**: Smoothly interpolates physics between fixed steps + - Position and rotation interpolation for rigid bodies + - Prevents visual stuttering despite fixed physics rate + - Special handling for teleportation and discontinuous movement +- **Visual Smoothing**: Uses interpolation to provide smooth visuals despite fixed physics rate + - Camera movement smoothing + - Character animation blending + - State transitions for visual effects + +### Timing Constants +- **Fixed Delta Time**: Set to 1/50 (0.02s) for consistent physics simulation + - 50Hz physics update rate + - Compromise between accuracy and performance + - Aligns with monitor refresh rates (typically 60Hz) +- **Maximum Delta Time**: Capped at 1/30 (0.033s) to prevent spiral of death + - Avoids the "physics well of despair" described in PhysX documentation + - Prevents cascading performance issues from time spikes + - Maintains stability during performance drops +- **Network Rate**: Set to 1/8 (0.125s) for efficient network updates + - 8Hz network update frequency + - Bandwidth optimization while maintaining responsive feel + - Interpolation compensates for lower update rate + +### System Management +- **System Registration**: Systems are registered with the world in a specific order + - Establishes initialization and update execution order + - Ensures dependencies are satisfied + - Controls information flow between systems +- **System Lifecycle**: Each system implements the same update methods as the world + - Consistent lifecycle methods across all systems + - Predictable execution flow + - Clean separation of update phases +- **Priority-based Updates**: Systems are updated in a defined order to handle dependencies + - Critical systems (physics, network) updated first + - Dependent systems follow in logical sequence + - Input systems before processing, rendering after state updates + +### Performance Considerations +- **Accumulator Capping**: Prevents large time steps that could cause instability + - Avoids physics anomalies from long frames + - Maintains simulation stability during performance spikes + - Graceful degradation during performance issues +- **Delta Clamping**: Ensures delta time stays within reasonable bounds + - Minimum threshold prevents zero or negative delta time + - Maximum threshold prevents excessive stepping + - Consistent behavior across varying frame rates +- **Performance Monitoring**: Hooks for stats/performance monitors in preTick and postTick + - Granular timing data collection + - System-specific performance tracking + - Memory usage monitoring + - GPU performance tracking via stats-gl + +### Game Loop Optimization Strategies +- **Adaptive Time Steps**: Can slightly vary time step within bounds to recover from spikes +- **Work Distribution**: Spreads computation across frames to avoid performance spikes +- **Frame Dropping**: Intelligent frame dropping under heavy load while maintaining simulation integrity +- **Multi-threading**: Leverages the PxDefaultCpuDispatcher for parallel physics computation +- **Separating Sim Frequency**: Ability to decouple physics simulation from rendering frequency +- **Performance Recovery**: Implements strategies to recover from temporary performance issues +- **"Well of Despair" Avoidance**: Careful management of sub-stepping to avoid performance traps +- **Scene Complexity Management**: Dynamically adjusts simulation complexity based on performance + +### Scene Query Optimization +- **Double-buffering**: Implements double-buffering approach for scene queries during simulation +- **Separate Structures**: Uses different data structures for broadphase collision and scene queries +- **Update Frequency Control**: Can update scene query structures at different rates than simulation +- **Bounds Optimization**: Uses tighter bounds for scene queries than for broadphase collision +- **Dynamic Tree Rebuild**: Configurable dynamic tree rebuild rate for scene query performance +- **Objects Per Node**: Tweakable parameter for objects per node in scene query structures +- **Pruner Selection**: Appropriate pruner selection for different scene characteristics + +## 26. Asset Management System + +### Asset Loading and Caching +- **Resource Loading**: Basic resource loading system + - Type-specific loaders (textures, models, audio) + - Caching for reuse +- **Asset Caching**: Basic caching strategy + - Memory cache for loaded assets + - URL.createObjectURL for local files +- **Results Management**: Tracking of loaded assets + - Results map for tracking loaded assets + - Promises map for in-flight requests + +### Asset Optimization +- **Texture Management**: Basic texture handling + - Canvas textures for UI elements + - Anisotropy settings for better quality + - CanvasTexture with configurable filters +- **Model Optimization**: Basic model handling + - Node-based representation of loaded models + - Statistics tracking for loaded models + - File size tracking + +### File Formats +- **Image Formats**: Support for standard web image formats + - Standard web formats via TextureLoader +- **3D Model Formats**: Support for industry-standard 3D formats + - glTF as primary format via GLTFLoader + - VRM for avatar models +- **HDR Format**: Support for high dynamic range images + - RGBE format loading + +## 27. THREE.js and PhysX Integration + +### Core Integration Architecture +- **Extension System**: THREE.js objects extended with PhysX functionality + - Vector3 extensions for PhysX conversions + - Quaternion and Matrix4 extensions for transforms + - Utility methods for conversions between coordinate systems + +### Physics Simulation Integration +- **Physics System**: Core integration between THREE.js scene and PhysX + - Scene creation with configurable parameters + - Simulation and fetching of results + - Active actor tracking + - Interpolation for smooth rendering + +### Geometry Bridge +- **Geometry Conversion**: Convert THREE.js geometries to PhysX meshes + - Primitive shapes (box, sphere) + - Convex mesh generation + - Triangle mesh generation + - Scale handling for mesh geometries + +### Colliders +- **Collider System**: PhysX shape creation and management + - Multiple collider types (box, sphere, geometry) + - Material properties (friction, restitution) + - Layer-based filtering + - Trigger volumes + +### Joints +- **Joint System**: Constraint system for physics objects + - Distance joint implementation + - Joint frames and transformations + - Constraint flags and breaking forces + - Limit configuration + +### Collision Detection +- **Raycast and Sweep**: Spatial queries for collision detection + - Raycasting implementation + - Sweep tests with geometry + - Filter data for layer-based queries + - Hit result processing + +### Contact Events +- **Contact Handling**: System for responding to collisions + - Simulation event callbacks + - Contact point collection + - Actor pair handling + - Filtering of contact events \ No newline at end of file diff --git a/.cursor/rules/hyperfyapps.mdc b/.cursor/rules/hyperfyapps.mdc new file mode 100644 index 00000000..a8da554a --- /dev/null +++ b/.cursor/rules/hyperfyapps.mdc @@ -0,0 +1,1991 @@ +--- +description: Syntax for Hyperfy Apps +globs: +alwaysApply: false +--- +# 🌐 Hyp App API Reference + +``` +╔══════════════════════════════════════╗ +║ HYP APP API REFERENCE ║ +╚══════════════════════════════════════╝ +``` + +## Introduction + +The Hyp App API is the modern way to create and manage applications in Hyperfy worlds. This API replaces the legacy method of manually creating apps in the world folder, providing a more streamlined and powerful development experience. + +## Core Concepts + +### App Structure + +Global App Format + +For simpler apps or scripts, you can directly use the `app` global without the wrapping object: + +```javascript +// Direct use of the app global +app.configure(() => { + return [ + // Configuration options... + ] +}) + +// Create objects +const myObject = app.create('mesh') +app.add(myObject) + +// Set up event handlers +app.on('update', (dt) => { + // Update logic +}) +``` + +Choose the format that best suits your app's complexity and organization needs. + +### SES Environment Restrictions + +Hyperfy uses SES (Secure ECMAScript) for security, which imposes specific syntax restrictions: + +- **Never use ES6 `export` or `import` syntax** - SES will throw errors +- **Always wrap apps in parentheses** when using the object return format +- **Use only approved global APIs and objects** +- **No direct `eval()` or `new Function()` usage** +- **Avoid using browser-specific APIs** that aren't explicitly provided + +## Creating Apps in World + +### The Hyp App Method + +Creating apps with the Hyp App system is simple: + +```javascript +// Create a new app directly in the world +({ + init() { + // Create a cube + const geometry = new THREE.BoxGeometry(1, 1, 1) + const material = new THREE.MeshBasicMaterial({ color: 0xff00ff }) + this.cube = new THREE.Mesh(geometry, material) + + // Add it to the scene + this.app.add(this.cube) + + // Position it + this.cube.position.set(0, 1, 0) + }, + + update() { + // Rotate the cube + if (this.cube) { + this.cube.rotation.y += 0.01 + } + }, + + cleanup() { + // Dispose of resources + if (this.cube) { + this.cube.geometry.dispose() + this.cube.material.dispose() + } + } +}) +``` + +### Configuration + +Apps can be configured using the `app.configure()` method, which defines UI controls for customizing your app. The API supports multiple configuration types and complex conditional fields: + +```javascript +app.configure(() => { + return [ + // Basic text input + { + key: 'title', + type: 'text', + label: 'Title', + initial: 'Default Title' + }, + + // File upload for assets + { + key: 'audioFile', + type: 'file', + kind: 'audio', + label: 'Background Music' + }, + + // Section header for grouping + { + type: 'section', + key: 'appearance', + label: 'Appearance Settings', + }, + + // Multiple choice option + { + type: 'switch', + key: 'theme', + label: 'Theme', + initial: 'neon', + options: [ + { value: 'neon', label: '霓虹' }, + { value: 'dark', label: '暗黒' }, + { value: 'light', label: '明るい' } + ] + }, + + // Conditional fields that appear based on other selections + { + key: 'neonColor', + type: 'color', + label: 'Neon Color', + value: '#00ffaa', + when: [{ key: 'theme', op: 'eq', value: 'neon' }] + } + ] +}) +``` + +#### Conditional Configuration Fields + +You can create dynamic configurations with fields that only appear when certain conditions are met: + +```javascript +app.configure(() => { + return [ + // Selection switch + { + key: 'emote', + type: 'switch', + label: 'Custom', + options: [ + { label: '1', value: '1' }, + { label: '2', value: '2' }, + { label: '3', value: '3' }, + { label: '4', value: '4' }, + ], + }, + + // Dynamically generated fields using spread syntax + ...customEmoteFields('1'), + ...customEmoteFields('2'), + ...customEmoteFields('3'), + ...customEmoteFields('4'), + ] + + // Helper function to generate conditional fields + function customEmoteFields(n) { + return [ + { + key: `emote${n}Name`, + type: 'text', + label: 'Name', + when: [{ key: 'emote', op: 'eq', value: n }], + }, + { + key: `emote${n}`, + type: 'file', + label: 'Emote', + kind: 'emote', + when: [{ key: 'emote', op: 'eq', value: n }], + }, + ] + } +}) +``` + +#### Configuration Field Types + +The configuration system supports a variety of field types: + +- **text**: Simple text input +- **textarea**: Multi-line text input +- **number**: Numeric input with min/max/step options +- **switch**: Multiple choice selection +- **color**: Color picker +- **file**: File upload with various kinds (audio, texture, model, emote, hdr) +- **section**: Header for organizing fields + +#### Accessing Configuration Values + +Configuration values are accessible through the `app.config` object: + +```javascript +// Access configuration with optional chaining for nullable fields +const audioUrl = app.config.audioFile?.url +const themeName = app.config.theme +const title = app.config.title || 'Default Title' +``` + +## Working Examples + +### Audio Player Example + +This example demonstrates how to create a simple audio player with play, pause, and stop controls. + +```javascript +// Audio Player App +app.configure(() => { + return [ + { + key: 'audio', + type: 'file', + kind: 'audio', + label: 'Audio' + } + ] +}) + +const audio = app.create('audio') +audio.src = config.audio?.url + +const body = app.get('Body') +body.add(audio) + +const ui = app.create('ui') +ui.backgroundColor = 'rgba(0, 15, 30, 0.8)' +ui.position.y = 3 +body.add(ui) + +const btn1 = app.create('uitext') +btn1.value = '[ PLAY ]' +btn1.color = '#00ffaa' +btn1.onPointerDown = () => audio.play() +ui.add(btn1) + +const btn2 = app.create('uitext') +btn2.value = '[ PAUSE ]' +btn2.color = '#00ffaa' +btn2.onPointerDown = () => audio.pause() +ui.add(btn2) + +const btn3 = app.create('uitext') +btn3.value = '[ STOP ]' +btn3.color = '#00ffaa' +btn3.onPointerDown = () => audio.stop() +ui.add(btn3) + +const time = app.create('uitext') +time.value = '' +time.color = '#00ffaa' +ui.add(time) + +app.on('update', () => { + time.value = audio.currentTime.toFixed(2) +}) +``` + +### Basic Sky Example + +A simple example showing how to create and configure a sky: + +```javascript +app.configure(() => { + return [ + { + key: 'sky', + label: 'Sky', + type: 'file', + kind: 'texture', + }, + { + key: 'hdr', + label: 'HDR', + type: 'file', + kind: 'hdr', + }, + ] +}) + +const sky = app.create('sky') +sky.bg = app.config.sky?.url +sky.hdr = app.config.hdr?.url +app.add(sky) +``` + +### Dynamic Sky System + +A more advanced sky system with time-of-day switching: + +```javascript +app.configure(() => { + return [ + { + type: 'section', + key: 'title', + label: 'Sky', + }, + { + type: 'switch', + key: 'switch', + label: 'TOD', + value: 1, + options: [ + { value: 1, label: '☀️' }, + { value: 2, label: '🌅' }, + { value: 3, label: '🌙' }, + { value: 4, label: '🌌' } + ] + }, + { + key: 'sky1', + label: 'Dusk Sky', + type: 'file', + kind: 'texture', + value: null + }, + { + key: 'hdr1', + label: 'Dusk HDR', + type: 'file', + kind: 'hdr', + value: null + } + // Additional sky options... + ] +}) + +const sky = app.create('sky') +app.add(sky) + +function updateSky() { + const mode = app.config.switch + + if (mode === 4) { + // Aurora + sky.bg = app.config.sky3?.url + sky.hdr = app.config.hdr3?.url + } else if (mode === 3) { + // Night + sky.bg = app.config.sky2?.url + sky.hdr = app.config.hdr2?.url + } else if (mode === 2) { + // Dusk + sky.bg = app.config.sky1?.url + sky.hdr = app.config.hdr1?.url + } else { + // Day mode (empty sky/hdr means engine default) + sky.bg = null + sky.hdr = null + } +} + +// Initial setup and event listeners +updateSky() +app.on('config', updateSky) +``` + +### Client-Server Communication + +A simple example showing client-server communication: + +```javascript +// Client-side code +if (world.isClient) { + const action = app.create('action') + action.label = '[ M0VE CUBE ]' + action.position.set(0, 1, 0) + + action.onTrigger = () => { + app.send("cube:move") + } + + app.on("cube:position", (data) => { + app.position.fromArray(data) + }) + + app.add(action) +} + +// Server-side code +if (world.isServer) { + app.on("cube:move", () => { + app.position.y += 1 + app.send("cube:position", app.position.toArray()) + }) +} +``` + +### Advanced Avatar System + +This example demonstrates a more complex system for avatar management with emotes and interactions: + +```javascript +// Constants +const BUBBLE_TIME = 5 +const EMOTE_TIME = 2 +const LOOK_TIME = 5 +const UP = new Vector3(0, 1, 0) + +// Reusable vectors and quaternions +const v1 = new Vector3() +const v2 = new Vector3() +const q1 = new Quaternion() + +// Initialize +const config = app.config +const vrm = app.get('avatar') + +// Server-side code +if (world.isServer) { + // Send initial state + const state = { ready: true } + app.state = state + app.send('state', state) + + // Create controller + const ctrl = app.create('controller') + ctrl.position.copy(app.position) + world.add(ctrl) + ctrl.quaternion.copy(app.quaternion) + ctrl.add(vrm) + + // Handle emotes from configuration + const emoteUrls = {} + if (config.emote1Name && config.emote1?.url) { + emoteUrls[config.emote1Name] = config.emote1.url + } + // Additional emotes... + + // World event listeners + world.on('chat', msg => { + if (msg.fromId === app.instanceId) return + // Process chat events + }) +} + +// Client-side code +if (world.isClient) { + const idleEmoteUrl = config.emote0?.url + world.attach(vrm) + + // Initialize based on state + let state = app.state + if (state.ready) { + init() + } else { + world.remove(vrm) + app.on('state', _state => { + state = _state + init() + }) + } + + // Create UI bubble for chat + const bubble = app.create('ui') + bubble.width = 300 + bubble.height = 512 + bubble.size = 0.005 + bubble.pivot = 'bottom-center' + bubble.billboard = 'full' + bubble.justifyContent = 'flex-end' + bubble.alignItems = 'center' + bubble.position.y = 2 + bubble.active = false + + // Add bubble content + const bubbleBox = app.create('uiview') + bubbleBox.backgroundColor = 'rgba(0, 0, 0, 0.95)' + bubbleBox.borderRadius = 20 + bubbleBox.padding = 20 + bubble.add(bubbleBox) + + const bubbleText = app.create('uitext') + bubbleText.color = '#00ffaa' + bubbleText.lineHeight = 1.4 + bubbleText.fontSize = 16 + bubbleText.value = '...' + bubbleBox.add(bubbleText) + vrm.add(bubble) + + // Initialize the avatar + function init() { + world.add(vrm) + vrm.setEmote(idleEmoteUrl) + } + + // Event handlers for various avatar actions + app.on('say', value => { + // Handle speech + }) + + app.on('emote', url => { + // Handle emote animations + }) + + app.on('update', delta => { + // Update timers and animations + }) +} +``` + +### Conditional Configuration Fields + +Example of creating dynamic configuration options with conditional visibility: + +```javascript +app.configure(() => { + return [ + { + key: 'emotes', + type: 'section', + label: 'Emotes', + }, + { + key: 'emote', + type: 'switch', + label: 'Custom', + options: [ + { label: '1', value: '1' }, + { label: '2', value: '2' }, + { label: '3', value: '3' }, + { label: '4', value: '4' }, + ], + }, + ...customEmoteFields('1'), + ...customEmoteFields('2'), + ...customEmoteFields('3'), + ...customEmoteFields('4'), + ] + + // Helper function to generate conditional fields + function customEmoteFields(n) { + return [ + { + key: `emote${n}Name`, + type: 'text', + label: 'Name', + when: [{ key: 'emote', op: 'eq', value: n }], + }, + { + key: `emote${n}`, + type: 'file', + label: 'Emote', + kind: 'emote', + when: [{ key: 'emote', op: 'eq', value: n }], + }, + ] + } +}) +``` + +## UI Components + +The Hyp App API provides powerful UI components with extensive styling options: + +```javascript +// Basic UI container +const ui = app.create('ui') +ui.backgroundColor = 'rgba(0, 15, 30, 0.8)' +ui.width = 300 +ui.height = 200 +ui.position.y = 2 +ui.billboard = 'full' // Always face user +ui.pivot = 'bottom-center' // Pivot point +ui.justifyContent = 'center' // Flex layout +ui.alignItems = 'center' // Flex layout +ui.flexDirection = 'column' // Flex layout +ui.gap = 10 // Space between children +ui.padding = 15 // Inner spacing +ui.borderRadius = 20 // Rounded corners +ui.active = true // Visibility toggle +ui.size = 0.005 // Scale factor +app.add(ui) + +// Text +const text = app.create('uitext') +text.value = 'H3LL0 W0RLD' +text.color = '#00ffaa' +text.fontSize = 24 +text.fontWeight = 700 +text.lineHeight = 1.4 +text.marginTop = 10 +text.padding = 8 +ui.add(text) + +// Panel (container for other UI elements) +const panel = app.create('uiview') +panel.backgroundColor = 'rgba(0, 15, 30, 0.8)' +panel.borderRadius = 20 +panel.padding = 15 +panel.width = 280 +panel.flexDirection = 'row' +ui.add(panel) +``` + +### Interactive UI Elements + +UI elements can respond to user interaction: + +```javascript +const button = app.create('uitext') +button.value = '[ ACTIVATE ]' +button.color = '#00ffaa' +button.fontSize = 18 +button.backgroundColor = 'rgba(0, 0, 0, 0.5)' +button.padding = 10 +button.borderRadius = 5 + +// Interaction events +button.onPointerDown = () => { + // Called when pressed + button.color = '#ffffff' +} + +button.onPointerUp = () => { + // Called when released + button.color = '#00ffaa' + performAction() +} + +button.onPointerOver = () => { + // Called when hovering + button.backgroundColor = 'rgba(0, 30, 60, 0.5)' +} + +button.onPointerOut = () => { + // Called when no longer hovering + button.backgroundColor = 'rgba(0, 0, 0, 0.5)' +} +``` + +## Event Handling + +The Hyp App API provides several event types for various purposes: + +```javascript +// App lifecycle events +app.on('update', (dt) => { + // Called every frame with delta time in seconds + // Use for animations, continuous behaviors +}) + +app.on('fixedUpdate', (dt) => { + // Called at fixed intervals (physics timing) + // Use for physics and consistent-rate behaviors +}) + +app.on('config', () => { + // Called when configuration changes + // Use to react to user-modified settings +}) + +// Custom events for app communication +app.on('customEvent', (data) => { + // Handle custom event with data +}) + +// World events +world.on('enter', (player) => { + // Called when a player enters the world +}) + +world.on('leave', (player) => { + // Called when a player leaves the world +}) + +world.on('chat', (message) => { + // Called when a chat message is sent +}) + +// Clean up event listeners +app.off('eventName', handlerFunction) +``` + +### Timer-Based Events + +You can create timer-based behaviors using the update event: + +```javascript +// Create a timer system +const timers = { + animation: { + duration: 5, + current: 0, + active: true + } +} + +app.on('update', (dt) => { + // Update timers + Object.values(timers).forEach(timer => { + if (timer.active) { + timer.current += dt + + if (timer.current >= timer.duration) { + // Timer completed + timer.current = 0 + // Perform action or reset + } + } + }) +}) +``` + +## Client-Server Architecture + +Hyp apps can leverage both client and server-side code for multiplayer experiences: + +```javascript +// Client-specific code +if (world.isClient) { + const action = app.create('action') + action.label = '[ M0VE CUBE ]' + action.position.set(0, 1, 0) + + action.onTrigger = () => { + // Send message to server with optional data + app.send("cube:move", { player: app.instanceId }) + } + + // Listen for messages from server + app.on("cube:position", (data) => { + // Update local representation based on authoritative server data + app.position.fromArray(data.position) + updateUI(data.playerName) + }) + + app.add(action) +} + +// Server-specific code +if (world.isServer) { + // Maintain authoritative state + let height = 1 + + // Listen for client requests + app.on("cube:move", (data, sender) => { + // Get information about the sender + const player = world.getPlayer(sender) + const playerName = player ? player.name : 'Unknown' + + // Update authoritative state + height += 1 + app.position.y = height + + // Broadcast updated state to all clients + app.send("cube:position", { + position: app.position.toArray(), + playerName: playerName + }) + + // Log server-side for monitoring + console.log(`Cube moved by ${playerName}`) + }) +} +``` + +### Client-Server Data Flow + +The typical data flow in client-server apps follows this pattern: + +1. Client triggers an action (button press, interaction) +2. Client sends a message to the server with `app.send()` +3. Server receives the message and updates authoritative state +4. Server broadcasts updated state to all clients +5. Clients receive the update and apply changes to their local representation +6. Clients update UI to reflect the new state + +### State Synchronization + +For complex apps, use app state to synchronize initial state: + +```javascript +// Server: Initialize and send state +if (world.isServer) { + const state = { + position: [0, 1, 0], + color: '#00ffaa', + ready: true + } + app.state = state + app.send('state', state) +} + +// Client: Initialize from state +if (world.isClient) { + let state = app.state + + if (state.ready) { + // Initialize from existing state + initializeFromState(state) + } else { + // Wait for state + app.on('state', (_state) => { + state = _state + initializeFromState(state) + }) + } +} +``` + +## Networking + +Hyperfy apps can communicate with external services using the fetch API: + +```javascript +// Make a GET request +async function fetchData() { + try { + const response = await fetch('https://api.example.com/data') + const data = await response.json() + return data + } catch (error) { + console.error('Failed to fetch data:', error) + return null + } +} + +// Make a POST request with authentication +async function sendData(payload) { + try { + const response = await fetch('https://api.example.com/update', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer YOUR_TOKEN' + }, + body: JSON.stringify(payload) + }) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + const result = await response.json() + return result + } catch (error) { + console.error('Error sending data:', error) + return null + } +} +``` + +## Best Practices + +### Performance Optimization + +```javascript +// Object pooling for frequently used objects +this.pool = Array(10).fill(null).map(() => { + const geometry = new THREE.BoxGeometry(1, 1, 1) + const material = new THREE.MeshBasicMaterial() + return new THREE.Mesh(geometry, material) +}) + +// Reuse objects instead of creating new ones +const tempVector = new Vector3() + +function updatePosition(target) { + // Use the temp vector instead of creating a new one + tempVector.copy(target.position).add(velocity) + object.position.copy(tempVector) +} +``` + +### Memory Management + +Always dispose of resources in the cleanup method: + +```javascript +cleanup() { + // Dispose geometries + Object.values(this.geometries).forEach(g => g.dispose()) + + // Dispose materials + Object.values(this.materials).forEach(m => m.dispose()) + + // Remove from scene + if (this.mesh && this.mesh.parent) { + this.mesh.parent.remove(this.mesh) + } +} +``` + +### Client-Server Code Organization + +Keep your client and server code well-organized: + +```javascript +// Shared variables and constants +const CONSTANTS = { + MAX_HEIGHT: 10, + MIN_HEIGHT: 0, + MOVE_SPEED: 0.5 +} + +// Server-specific code in one block +if (world.isServer) { + // Server-only variables + const serverState = { + // Server state here + } + + // Server initialization + function initServer() { + // Initialize server-specific components + } + + // Server event handlers + function setupServerEvents() { + app.on('event1', handleEvent1) + app.on('event2', handleEvent2) + } + + // Initialize server + initServer() + setupServerEvents() +} + +// Client-specific code in one block +if (world.isClient) { + // Client-only variables + const clientState = { + // Client state here + } + + // Client initialization + function initClient() { + // Initialize client-specific components + } + + // Client event handlers + function setupClientEvents() { + app.on('event3', handleEvent3) + app.on('event4', handleEvent4) + } + + // Initialize client + initClient() + setupClientEvents() +} +``` + +### Safe Access Patterns + +Use optional chaining and nullish coalescing for safer code: + +```javascript +// Optional chaining for potentially undefined properties +const url = config.texture?.url + +// Nullish coalescing for default values +const name = config.name ?? 'Default Name' + +// Safe function calls +player?.getPosition()?.toArray() || [0, 0, 0] + +// Type checking before operations +if (typeof value === 'number' && !isNaN(value)) { + // Perform operation with number +} +``` + +## Migration from Legacy Methods + +The Hyp App API represents the modern approach to creating Hyperfy applications. If you have code built using the older approaches, this section will help you migrate. + +### Legacy App Structure vs. Modern Approach + +#### Legacy Method: +```javascript +// Manually creating a file in world/apps directory +export default function App(app) { + // App code here + const cube = app.three.getMesh('cube') + + app.three.onUpdate(() => { + cube.rotation.y += 0.01 + }) +} +``` + +#### Modern Method: +```javascript +// Using the Hyp App API with object return format +({ + init() { + // Initialize app + this.cube = this.app.get('cube') + }, + + update() { + // Update every frame + if (this.cube) { + this.cube.rotation.y += 0.01 + } + }, + + cleanup() { + // Clean up resources + } +}) +``` + +### Migration Steps + +1. **Move App Files** - Relocate your app code from the world/apps directory to use the Hyp App system. + +2. **Update Structure** - Convert your app structure to use either the object return format (recommended) or the global app format. + +3. **Replace Legacy APIs** - Update legacy API calls with their modern equivalents: + + | Legacy API | Modern API | + |------------|------------| + | `app.three.getMesh()` | `app.get()` | + | `app.three.onUpdate()` | `app.on('update')` | + | `app.three.add()` | `app.add()` | + | `app.three.createObj()` | `app.create()` | + | `app.three.physics.createRigidBody()` | `app.create('rigidbody')` | + +4. **Update Event Handling** - Use the modern event system with `app.on()` and `app.off()` instead of legacy callbacks. + +5. **Replace JSON Configuration** - Move from static JSON configuration to dynamic configuration with `app.configure()`. + +6. **Implement Cleanup** - Add proper resource cleanup in the `cleanup()` lifecycle method. + +7. **Test Thoroughly** - Ensure your migrated app functions as expected in the new environment. + +8. **Consider New Features** - Take advantage of new Hyp App API features like conditional configuration, improved UI components, and enhanced networking. + +### Common Migration Issues + +- **Missing Resources**: Ensure all assets are properly imported and accessible +- **Event Listener Memory Leaks**: Verify all event listeners are removed in cleanup +- **Legacy Method Calls**: Check for any remaining legacy API calls +- **State Management**: Update how you manage and synchronize app state +- **Error Handling**: Add proper error handling for improved robustness + +By following these steps, you can successfully migrate your legacy Hyperfy apps to the modern Hyp App API system. + +## API Reference + +``` +╔══════════════════════════════════════╗ +║ API REFERENCE ║ +╚══════════════════════════════════════╝ +``` + +This section provides detailed documentation for all API components available in the Hyperfy scripting system. + +### Global Objects + +- **`app`**: The main app instance for managing your application +- **`world`**: World state and interactions with the environment and players +- **`config`**: App configuration values (shorthand for app.config) +- **`THREE`**: Three.js library for 3D operations and math +- **`Vector3`**, **`Quaternion`**, **`Euler`**, **`Matrix4`**: Math utilities for spatial operations + +### App API + +The global `app` variable is always available within the app scripting runtime. + +#### Properties + +- **`.instanceId`**: String - The unique ID of the current app instance (shared across clients and server) +- **`.version`**: String - The version of the app instance, incremented whenever the app is modified +- **`.state`**: Object - Plain JavaScript object for storing state, synchronized between server and clients +- **`.config`**: Object - Contains all configuration values defined in `app.configure()` + +#### Methods + +- **`.on(name, callback)`**: Subscribes to custom networked app events and engine update events. Note: Only subscribe to update events when needed - the engine is optimized to skip over apps that don't receive update events. +- **`.off(name, callback)`**: Unsubscribes from custom events and update events. Important: Be sure to unsubscribe from update events when no longer needed. +- **`.send(name, data, skipNetworkId)`**: Sends an event across the network. If called from a client, sends to the server. If called from the server, sends to all clients (with optional skipNetworkId to exclude a specific client). +- **`.get(nodeId)`**: Node - Finds and returns any node with the matching ID from the model. For Blender models, this is the object "name". Note: The Blender GLTF exporter sometimes renames objects (removing spaces, etc.) - best practice is to name objects in UpperCamelCase. +- **`.create(nodeName, options)`**: Node - Creates and returns a node of the specified type. The optional options object can contain initialization parameters like `id`. +- **`.configure(fields)`**: Configures custom UI for your app with configuration options +- **`.add(object)`**: Adds an object to the app scene hierarchy + +### World API + +The global `world` variable is always available within the app scripting runtime. + +#### Properties + +- **`.networkId`**: String - A unique ID for the current server or client +- **`.isServer`**: Boolean - Whether the script is currently executing on the server +- **`.isClient`**: Boolean - Whether the script is currently executing on the client + +#### Methods + +- **`.add(node)`**: Adds a node into world-space, outside of the app's local hierarchy +- **`.remove(node)`**: Removes a node from world-space +- **`.attach(node)`**: Adds a node into world-space, maintaining its current world transform +- **`.on(event, callback)`**: Subscribes to world events like `enter` and `leave` +- **`.off(event, callback)`**: Unsubscribes from world events +- **`.raycast(origin, direction, maxDistance?, layerMask?)`**: Performs physics raycasting. If `maxDistance` is not specified, max distance is infinite. If `layerMask` is not specified, it will hit anything. +- **`.createLayerMask(...groups)`**: Creates a bitmask for raycast layer filtering. Currently the only groups available are `environment` and `player`. +- **`.getPlayer(playerId)`**: Player - Returns a player object (local player if no ID provided) +- **`.chat(message)`**: Sends a chat message to all players +- **`.getTimestamp()`**: Returns the current server timestamp + +```javascript +// Example: Using raycast to detect objects in the world +const origin = new Vector3(0, 1, 0) +const direction = new Vector3(0, 0, -1) +const maxDistance = 10 +const layerMask = world.createLayerMask('environment') + +const hit = world.raycast(origin, direction, maxDistance, layerMask) +if (hit) { + console.log('Hit object:', hit.object.id, 'at distance:', hit.distance) +} + +// Example: Listening for player join/leave events +world.on('enter', (player) => { + console.log(`Player ${player.name} entered the world`) +}) + +world.on('leave', (player) => { + console.log(`Player ${player.name} left the world`) +}) +``` + +### Player API + +Represents a player in the world. + +#### Properties + +- **`.networkId`**: String - A completely unique ID that is given to every player each time they connect +- **`.entityId`**: String - The entity's ID +- **`.id`**: String - The player ID (same each time the player enters the world) +- **`.name`**: String - The player's name +- **`.position`**: Vector3 - The player's position in the world +- **`.quaternion`**: Quaternion - The player's rotation in the world +- **`.rotation`**: Euler - The player's rotation as Euler angles + +#### Methods + +- **`.teleport(position, rotationY)`**: Teleports the player instantly to a new position. The `rotationY` value is in radians and if omitted, the player will continue facing their current direction. +- **`.getBoneTransform(boneName)`**: Matrix4 - Returns a matrix of the bone transform in world space (see Avatar API for details on available bones) + +```javascript +// Example: Teleporting a player +const player = world.getPlayer() +player.teleport(new Vector3(0, 10, 0), Math.PI) // Teleport and face opposite direction + +// Example: Getting bone transform +const handMatrix = player.getBoneTransform('rightHand') +if (handMatrix) { + // Attach an object to the player's hand + object.position.setFromMatrixPosition(handMatrix) + object.quaternion.setFromRotationMatrix(handMatrix) +} +``` + +### UI Components + +#### UI + +Creates a UI panel in the 3D world. + +##### Properties + +- **`.width`**: Number - The width of the UI canvas in pixels. Defaults to `100`. +- **`.height`**: Number - The height of the UI canvas in pixels. Defaults to `100`. +- **`.size`**: Number - Conversion factor from pixels to meters. This allows you to build UI while thinking in pixels instead of meters, and makes it easier to resize things later. Defaults to `0.01`. +- **`.lit`**: Boolean - Whether the canvas is affected by lighting. Defaults to `false`. +- **`.doubleside`**: Boolean - Whether the canvas is doublesided. Defaults to `false`. +- **`.billboard`**: String - Makes the UI face the camera. Can be `null`, `full` or `y-axis`. Defaults to `null`. +- **`.pivot`**: String - Determines where the "center" of the UI is. Options are: `top-left`, `top-center`, `top-right`, `center-left`, `center`, `center-right`, `bottom-left`, `bottom-center`, `bottom-right`. Defaults to `center`. +- **`.backgroundColor`**: String - The background color of the UI. Can be hex (eg `#000000`) or rgba (eg `rgba(0, 0, 0, 0.5)`). Defaults to `null`. +- **`.borderWidth`**: Number - The width of the border in pixels. +- **`.borderColor`**: String - The color of the border. +- **`.borderRadius`**: Number - The radius of the border in pixels. +- **`.padding`**: Number - The inner padding of the UI in pixels. Defaults to `0`. +- **`.flexDirection`**: String - The flex direction. Can be `column`, `column-reverse`, `row` or `row-reverse`. Defaults to `column`. +- **`.justifyContent`**: String - Options: `flex-start`, `flex-end`, `center`. Defaults to `flex-start`. +- **`.alignItems`**: String - Options: `stretch`, `flex-start`, `flex-end`, `center`, `baseline`. Defaults to `stretch`. +- **`.alignContent`**: String - Options: `flex-start`, `flex-end`, `stretch`, `center`, `space-between`, `space-around`, `space-evenly`. Defaults to `flex-start`. +- **`.flexWrap`**: String - Options: `no-wrap`, `wrap`. Defaults to `no-wrap`. +- **`.gap`**: Number - Gap between child elements. Defaults to `0`. + +```javascript +// Example: Creating a UI panel with various properties +const ui = app.create('ui') +ui.width = 300 +ui.height = 200 +ui.backgroundColor = 'rgba(0, 15, 30, 0.8)' +ui.borderRadius = 20 +ui.padding = 15 +ui.billboard = 'full' +ui.pivot = 'center' +ui.justifyContent = 'center' +ui.alignItems = 'center' +ui.gap = 10 +app.add(ui) +``` + +#### UIView + +A container for other UI elements. + +##### Properties + +- **`.display`**: String - Either `none` or `flex`. Defaults to `flex`. +- **`.width`**: Number - The width of the view in pixels. Defaults to `100`. +- **`.height`**: Number - The height of the view in pixels. Defaults to `100`. +- **`.backgroundColor`**: String - The background color of the view. Can be hex (eg `#000000`) or rgba (eg `rgba(0, 0, 0, 0.5)`). Defaults to `null`. +- **`.borderWidth`**: Number - The width of the border in pixels. +- **`.borderColor`**: String - The color of the border. +- **`.borderRadius`**: Number - The radius of the border in pixels. +- **`.margin`**: Number - The outer margin of the view in pixels. Defaults to `0`. +- **`.padding`**: Number - The inner padding of the view in pixels. Defaults to `0`. +- **`.flexDirection`**: String - The flex direction. Can be `column`, `column-reverse`, `row` or `row-reverse`. Defaults to `column`. +- **`.justifyContent`**: String - Options: `flex-start`, `flex-end`, `center`. Defaults to `flex-start`. +- **`.alignItems`**: String - Options: `stretch`, `flex-start`, `flex-end`, `center`, `baseline`. Defaults to `stretch`. +- **`.alignContent`**: String - Options: `flex-start`, `flex-end`, `stretch`, `center`, `space-between`, `space-around`, `space-evenly`. Defaults to `flex-start`. +- **`.flexBasis`**: Number - Defaults to `null`. +- **`.flexGrow`**: Number - Defaults to `null`. +- **`.flexShrink`**: Number - Defaults to `null`. +- **`.flexWrap`**: String - Options: `no-wrap`, `wrap`. Defaults to `no-wrap`. +- **`.gap`**: Number - Defaults to `0`. + +```javascript +// Example: Creating a panel with various properties +const panel = app.create('uiview') +panel.width = 280 +panel.height = 150 +panel.backgroundColor = 'rgba(0, 15, 30, 0.8)' +panel.borderRadius = 10 +panel.padding = 10 +panel.margin = 5 +panel.flexDirection = 'column' +panel.justifyContent = 'center' +panel.alignItems = 'center' +panel.gap = 8 +ui.add(panel) +``` + +#### UIText + +Displays text within a UI element. + +##### Properties + +- **`.display`**: String - Either `none` or `flex`. Defaults to `flex`. +- **`.value`**: String - The text to display. +- **`.fontSize`**: Number - Font size in pixels. Defaults to `16`. +- **`.color`**: String - The text color. Defaults to `#000000`. +- **`.fontWeight`**: String/Number - Defaults to `normal`, can also be a number like `100` or string like `bold`. +- **`.lineHeight`**: Number - Line height as a multiplier. Defaults to `1.2`. +- **`.textAlign`**: String - Text alignment: `left`, `center`, `right`. Defaults to `left`. +- **`.fontFamily`**: String - Font family. Defaults to `Rubik`. +- **`.padding`**: Number - Inner padding in pixels. Defaults to `0`. +- **`.margin`**: Number - Outer margin in pixels. Defaults to `0`. +- **`.backgroundColor`**: String - Background color behind the text. Can be hex (eg `#000000`) or rgba (eg `rgba(0, 0, 0, 0.5)`). Defaults to `null`. +- **`.borderRadius`**: Number - Border radius in pixels. + +##### Events + +- **`.onPointerDown`**: Function - Called when text is pressed +- **`.onPointerUp`**: Function - Called when text is released +- **`.onPointerOver`**: Function - Called when pointer enters +- **`.onPointerOut`**: Function - Called when pointer leaves + +```javascript +// Example: Creating a button-like text element +const button = app.create('uitext') +button.value = '[ ACCESS GRANTED ]' +button.color = '#00ffaa' +button.fontSize = 18 +button.fontWeight = 'bold' +button.backgroundColor = 'rgba(0, 15, 30, 0.8)' +button.padding = 12 +button.borderRadius = 6 +button.textAlign = 'center' + +// Adding interaction events +button.onPointerOver = () => { + button.backgroundColor = 'rgba(0, 30, 60, 0.8)' +} + +button.onPointerOut = () => { + button.backgroundColor = 'rgba(0, 15, 30, 0.8)' +} + +button.onPointerDown = () => { + button.color = '#ffffff' +} + +button.onPointerUp = () => { + button.color = '#00ffaa' + // Perform action +} + +ui.add(button) +``` + +### Avatar API + +Represents an avatar in the world. + +#### Properties + +- **`.src`**: String - URL to a .vrm file, either an asset URL (from props) or an absolute URL +- **`.emote`**: String - URL to a .glb file with an emote animation, either from props or an absolute URL +- **`.head`**: Node - Reference to the avatar's head node +- **`.leftHand`**: Node - Reference to the avatar's left hand +- **`.rightHand`**: Node - Reference to the avatar's right hand +- **`.position`**: Vector3 - The avatar's position +- **`.quaternion`**: Quaternion - The avatar's rotation +- **`.scale`**: Vector3 - The avatar's scale + +#### Methods + +- **`.setEmote(url)`**: Sets the current emote animation +- **`.getBoneTransform(boneName)`**: Matrix4 - Returns bone transform in world space +- **`.getHeight()`**: Number - Returns the height of the avatar in meters (may be null if avatar hasn't loaded yet) + +```javascript +// Creating an avatar with src and emote +const avatar = app.create('avatar', { + src: config.avatar?.url, + emote: config.emote?.url +}) +app.add(avatar) + +// Getting bone transform for attaching objects +const matrix = avatar.getBoneTransform('rightHand') +if (matrix) { + weapon.position.setFromMatrixPosition(matrix) + weapon.quaternion.setFromRotationMatrix(matrix) +} +``` + +Note: VRM avatars have required and optional bones. The VRM spec defines these bones as required: +``` +hips, spine, chest, neck, head, leftShoulder, leftUpperArm, leftLowerArm, leftHand, rightShoulder, rightUpperArm, rightLowerArm, rightHand, leftUpperLeg, leftLowerLeg, leftFoot, leftToes, rightUpperLeg, rightLowerLeg, rightFoot, rightToes +``` + +### Nametag API + +Creates a floating nametag above objects, typically used for avatars. + +#### Properties + +- **`.label`**: String - The text to display +- **`.color`**: String - Text color +- **`.backgroundColor`**: String - Background color +- **`.position`**: Vector3 - Position offset from parent +- **`.scale`**: Vector3 - Scale of the nametag +- **`.active`**: Boolean - Whether the nametag is visible + +#### Methods + +- **`.setLabel(text)`**: Updates the text displayed +- **`.setColor(color)`**: Updates the text color + +### Audio API + +Sound management for your app. + +#### Properties + +- **`.src`**: String - URL for the audio file, either absolute or an asset URL from an embedded file. Currently only mp3 files are supported. +- **`.volume`**: Number - Volume level from 0 to 1 (default: 1) +- **`.loop`**: Boolean - Whether the audio should loop (default: false) +- **`.autoplay`**: Boolean - Whether the audio should play automatically +- **`.group`**: String - The type of audio: 'music' for ambient sounds/live event music or 'sfx' for short sound effects. Users can adjust volume for these groups independently. (default: 'music') +- **`.spatial`**: Boolean - Whether sound should be played spatially and heard by people nearby (default: true) +- **`.distanceModel`**: String - When spatial is enabled, the distance model to use: 'linear', 'inverse', or 'exponential' (default: 'inverse') +- **`.refDistance`**: Number - When spatial is enabled, the reference distance (default: 1) +- **`.maxDistance`**: Number - When spatial is enabled, the maximum distance (default: 40) +- **`.rolloffFactor`**: Number - When spatial is enabled, the rolloff factor (default: 3) +- **`.coneInnerAngle`**: Number - When spatial is enabled, the cone inner angle (default: 360) +- **`.coneOuterAngle`**: Number - When spatial is enabled, the cone outer angle (default: 360) +- **`.coneOuterGain`**: Number - When spatial is enabled, the cone outer gain (default: 0) +- **`.currentTime`**: Number (read-only) - Current time position in seconds + +#### Methods + +- **`.play()`**: Starts playback. Note: If no click gesture has happened within the world, playback won't begin until it has. +- **`.pause()`**: Pauses playback, retaining the current time +- **`.stop()`**: Stops playback and resets position to beginning + +```javascript +// Example of creating an audio node with spatial settings +const ambience = app.create('audio') +ambience.src = config.ambientSound?.url +ambience.loop = true +ambience.volume = 0.8 +ambience.group = 'music' +ambience.spatial = true +ambience.maxDistance = 50 +ambience.play() + +// Add to specific location in the world +const soundSource = app.create('group') +soundSource.position.set(10, 2, 15) +soundSource.add(ambience) +app.add(soundSource) +``` + +### Action API + +Creates an interactive action button in the world. + +#### Properties + +- **`.label`**: String - Text displayed on the action (defaults to "Interact") +- **`.distance`**: Number - The distance in meters that the action should be displayed (defaults to 3) +- **`.duration`**: Number - How long the player must hold down the interact button to trigger it, in seconds (defaults to 0.5) +- **`.position`**: Vector3 - Position in 3D space +- **`.rotation`**: Euler - Rotation in 3D space +- **`.scale`**: Vector3 - Scale in 3D space +- **`.active`**: Boolean - Whether the action is visible and interactive + +#### Methods + +- **`.onStart`**: Function - Called when the interact button is first pressed +- **`.onTrigger`**: Function - Called when the interact button has been held down for the full duration +- **`.onCancel`**: Function - Called if the interact button is released before the full duration + +```javascript +// Example of creating an action with all properties +const action = app.create('action') +action.label = '[ PR3SS M3 ]' +action.distance = 5 +action.duration = 1.0 +action.position.set(0, 1.5, 0) + +// Set up event handlers +action.onStart = () => { + console.log('Interaction started') +} + +action.onTrigger = () => { + console.log('Action triggered!') + performAction() +} + +action.onCancel = () => { + console.log('Interaction canceled') +} + +app.add(action) +``` + +### Node API + +The base class for all scene objects. + +#### Properties + +- **`.id`**: String - The ID of the node. Auto-generated when creating nodes via script. For GLTF models, it uses the same object name as in Blender. +- **`.position`**: Vector3 - Position in local space +- **`.rotation`**: Euler - Rotation in local space +- **`.quaternion`**: Quaternion - Rotation as quaternion (updating this automatically updates rotation) +- **`.scale`**: Vector3 - Scale in local space +- **`.matrixWorld`**: Matrix4 - The world matrix of this node in global space +- **`.parent`**: Node - Parent node +- **`.visible`**: Boolean - Whether the node is visible +- **`.children`**: Array[Node] - Child nodes + +> **Note:** Blender GLTF exporter sometimes renames objects (removing spaces, etc.) - best practice is to name objects in UpperCamelCase with no other characters. + +#### Methods + +- **`.add(childNode)`**: Adds a child node +- **`.remove(childNode)`**: Removes a child node +- **`.getWorldPosition(target)`**: Gets position in world space +- **`.getWorldQuaternion(target)`**: Gets rotation in world space +- **`.lookAt(vector)`**: Rotates to face a point in space +- **`.traverse(callback)`**: Traverses this and all descendents calling `callback` with the node + +### RigidBody API + +Physical body component for physics simulations. + +#### Properties + +- **`.type`**: String - Body type: `static`, `dynamic`, or `kinematic` +- **`.mass`**: Number - Mass of the body (affects physics) +- **`.linearDamping`**: Number - Reduces linear velocity over time +- **`.angularDamping`**: Number - Reduces angular velocity over time +- **`.detectCollisions`**: Boolean - Whether collision detection is enabled +- **`.onContactStart`**: Function (Experimental) - Called when a child collider generates contacts with another rigidbody +- **`.onContactEnd`**: Function (Experimental) - Called when a child collider ends contacts with another rigidbody +- **`.onTriggerEnter`**: Function (Experimental) - Called when a child trigger collider is entered +- **`.onTriggerLeave`**: Function (Experimental) - Called when a child trigger collider is left + +#### Methods + +- **`.applyForce(force, worldPoint)`**: Applies a force at the specified point +- **`.applyImpulse(impulse, worldPoint)`**: Applies an instantaneous impulse +- **`.setLinearVelocity(velocity)`**: Sets the linear velocity directly +- **`.setAngularVelocity(velocity)`**: Sets the angular velocity directly + +> **Note:** For performance reasons, if you plan to move the rigidbody with code without being dynamic, use `kinematic` as the type. + +### Collider API + +Collision detection shape for physics interactions. + +#### Properties + +- **`.type`**: String - Collider shape: `box`, `sphere`, or `geometry`. Defaults to `box`. +- **`.convex`**: Boolean - When using a geometry type, determines whether the geometry should be considered "convex". If disabled, the mesh will act as a trimesh. Defaults to `false`. Convex meshes are more performant and allow two convex dynamic rigidbodies to collide. +- **`.trigger`**: Boolean - Whether the collider is a trigger. Defaults to `false`. A trigger will not collide with anything, but will trigger the `onTriggerEnter` and `onTriggerLeave` functions on the parent rigidbody. + +#### Methods + +- **`.setFromNode(node)`**: Automatically sizes the collider to match a node +- **`.setSize(width, height, depth)`**: When type is `box`, sets the size of the box. Defaults to `1, 1, 1`. + +```javascript +// Example of creating different collider types +const boxCollider = app.create('collider') +boxCollider.type = 'box' +boxCollider.setSize(2, 1, 3) // width, height, depth + +const sphereCollider = app.create('collider') +sphereCollider.type = 'sphere' +sphereCollider.radius = 1.5 // Set sphere radius + +const triggerCollider = app.create('collider') +triggerCollider.trigger = true // Make it a trigger collider +``` + +> **Note:** Setting/modifying the geometry for a collider is only configurable within a GLTF (e.g., via Blender). Triggers are forced to act like convex shapes due to physics engine limitations. + +### Mesh API + +Represents a mesh to be rendered. Internally the mesh is automatically instanced for performance. + +#### Properties + +- **`.castShadow`**: Boolean - Whether the mesh casts shadows. Defaults to `true`. +- **`.receiveShadow`**: Boolean - Whether the mesh receives shadows. Defaults to `true`. + +```javascript +// Example: Creating and configuring a mesh +const mesh = app.create('mesh') +mesh.castShadow = true +mesh.receiveShadow = false +app.add(mesh) +``` + +> **Note:** Setting/modifying the geometry or materials are not currently supported, and can only be configured within a GLTF (e.g., via Blender). + +### Material API + +Surface appearance properties. + +#### Properties + +- **`.color`**: Color - Base color +- **`.emissive`**: Color - Self-illumination color +- **`.emissiveIntensity`**: Number - The emissive intensity of the material. Values greater than `1` will activate HDR Bloom, as long as the emissive color is not black. +- **`.metalness`**: Number - How metallic the surface appears (0-1) +- **`.roughness`**: Number - Surface roughness (0-1) +- **`.map`**: Texture - Color texture map +- **`.textureX`**: Number - The offset of the texture on the `x` axis. Useful for UV scrolling. +- **`.textureY`**: Number - The offset of the texture on the `y` axis. Useful for UV scrolling. + +```javascript +// Example: Creating a glowing neon material +const material = app.create('material') +material.color.set('#00ffaa') +material.emissive.set('#00ffaa') +material.emissiveIntensity = 2.5 +material.metalness = 0.7 +material.roughness = 0.2 + +// Example: Creating scrolling texture effect +const scrollingMaterial = app.create('material') +scrollingMaterial.map = app.config.texture?.url +app.on('update', (dt) => { + scrollingMaterial.textureX += dt * 0.05 +}) +``` + +### Sky API + +Controls the sky appearance in the world. + +#### Properties + +- **`.bg`**: String - URL to the sky texture or background +- **`.hdr`**: String - URL to the HDR (High Dynamic Range) environment map +- **`.rotation`**: Number - Rotation of the sky in radians +- **`.intensity`**: Number - Intensity of the sky lighting (default: 1.0) + +#### Methods + +- **`.setTexture(url)`**: Sets the sky texture directly +- **`.setHDR(url)`**: Sets the HDR environment map directly + +### Anchor API + +Position reference for UI and other elements. Anchors can also be used to attach players to them, for example for seating or vehicles. + +#### Properties + +- **`.position`**: Vector3 - Position in local space +- **`.rotation`**: Euler - Rotation in local space +- **`.quaternion`**: Quaternion - Rotation as quaternion +- **`.scale`**: Vector3 - Scale in local space +- **`.visible`**: Boolean - Whether the anchor is visible + +#### Methods + +- **`.add(childNode)`**: Adds a child node to the anchor +- **`.remove(childNode)`**: Removes a child node from the anchor + +```javascript +// Example: Creating a seat using an anchor +// Be sure to give it a unique ID to ensure that every client has the same ID +const seat = app.create('anchor', { id: 'seat1' }) +seat.position.set(0, 0.5, 0) +vehicle.add(seat) + +// Later, to attach a player to this seat: +const control = app.create('controller') +control.setEffect({ anchor: seat }) +``` + +### Group API + +A regular node with no special behavior, useful for grouping objects together under one parent. + +#### Properties + +- **`.position`**: Vector3 - Position in local space +- **`.rotation`**: Euler - Rotation in local space +- **`.quaternion`**: Quaternion - Rotation as quaternion +- **`.scale`**: Vector3 - Scale in local space +- **`.visible`**: Boolean - Whether the group and its children are visible + +#### Methods + +- **`.add(childNode)`**: Adds a child node to the group +- **`.remove(childNode)`**: Removes a child node from the group + +```javascript +// Example: Organizing objects in a group +const group = app.create('group') +group.position.set(0, 1, 0) +app.add(group) + +// Add multiple children to the group +const object1 = app.create('mesh') +const object2 = app.create('mesh') +const light = app.create('light') + +group.add(object1) +group.add(object2) +group.add(light) + +// Now we can manipulate all objects together +group.rotation.y = Math.PI / 4 +group.position.y += 1 +``` + +### LOD API + +Level of Detail for optimization. + +#### Properties + +- **`.levels`**: Array - Array of level objects +- **`.autoUpdate`**: Boolean - Whether LOD updates automatically based on distance + +#### Methods + +- **`.addLevel(object, distance)`**: Adds a detail level with an object and distance threshold +- **`.getObjectForDistance(distance)`**: Returns the appropriate object for the given distance +- **`.insert(node, maxDistance)`**: Adds `node` as a child of this LOD and also registers it to be activated/deactivated based on the `maxDistance` value. + +```javascript +// Example: Creating an LOD system for different model detail levels +const lod = app.create('lod') +app.add(lod) + +// High detail model - visible up to 20 units away +const highDetailModel = app.get('ModelHighDetail') +lod.insert(highDetailModel, 20) + +// Medium detail model - visible from 20 to 50 units away +const mediumDetailModel = app.get('ModelMediumDetail') +lod.insert(mediumDetailModel, 50) + +// Low detail model - visible from 50+ units away +const lowDetailModel = app.get('ModelLowDetail') +lod.insert(lowDetailModel, Infinity) +``` + +### Controller API + +Used for controlling entities in the world, particularly avatars and other entities that need movement. + +#### Properties + +- **`.position`**: Vector3 - Position in world space +- **`.quaternion`**: Quaternion - Rotation in world space +- **`.velocity`**: Vector3 - Current velocity +- **`.angularVelocity`**: Vector3 - Current rotational velocity + +#### Methods + +- **`.move(direction)`**: Moves the controller in the specified direction +- **`.teleport(position)`**: Instantly moves the controller to a new position +- **`.lookAt(target)`**: Rotates the controller to face a target +- **`.rotate(angle)`**: Rotates the controller by the specified angle + +### Numeric Utilities + +The `num` helper provides useful numeric utilities, particularly for random number generation since `Math.random()` is not allowed in the app script runtime. + +#### Usage + +```javascript +// Generate random integer between 0 and 10 +num(0, 10) + +// Generate random float between 100 and 1000 with 2 decimal places +num(100, 1000, 2) +``` + +#### Methods + +- **`num(min, max, dp=0)`**: Generates random numbers between min and max with optional decimal places + - `min`: Minimum value (inclusive) + - `max`: Maximum value (inclusive) + - `dp`: Decimal places (default: 0 for integers) +- **`.lerp(a, b, t)`**: Linear interpolation between a and b +- **`.clamp(value, min, max)`**: Clamps a value between min and max +- **`.degToRad(degrees)`**: Converts degrees to radians +- **`.radToDeg(radians)`**: Converts radians to degrees + +### Props API + +Apps can expose a list of custom UI fields allowing non-technical people to configure or change the way your apps work. + +#### Configuration + +Props are defined using the `app.configure()` method with an array of field definitions: + +```javascript +app.configure([ + { + key: 'name', + type: 'text', + label: 'Name', + initial: 'Default Name' + }, + // Additional fields... +]) + +// Or with a function +app.configure(() => { + return [ + { + key: 'name', + type: 'text', + label: 'Name', + }, + // Additional fields... + ] +}) +``` + +#### Accessing Props + +Props values can be accessed through the global `props` object or the `app.config` object: + +```javascript +const name = props.name || 'Default Name' +// or +const name = app.config.name || 'Default Name' +``` + +#### Field Types + +Props support a rich variety of field types: + +##### Text + +```javascript +{ + type: 'text', + key: 'fieldName', // the key on `props` to set this value + label: 'Display Name', // the label for the text input + placeholder: 'Enter text...', // an optional placeholder displayed inside the input + initial: 'Default value', // the initial value to set if not configured +} +``` + +##### Textarea + +```javascript +{ + type: 'textarea', + key: 'description', + label: 'Description', + placeholder: 'Enter details...', + initial: '', +} +``` + +##### Number + +```javascript +{ + type: 'number', + key: 'quantity', + label: 'Quantity', + dp: 2, // the number of decimal places allowed (default = 0) + min: 0, // the minimum value allowed (default = -Infinity) + max: 100, // the maximum value allowed (default = Infinity) + step: 0.5, // the amount incremented/decremented when pressing up/down arrows (default = 1) + initial: 1, // the initial value to set if not configured (default = 0) +} +``` + +##### Range + +```javascript +{ + type: 'range', + key: 'opacity', + label: 'Opacity', + min: 0, // the minimum value allowed (default = 0) + max: 1, // the maximum value allowed (default = 1) + step: 0.05, // the step amount when sliding (default = 0.05) + initial: 0.8, // the initial value to set if not configured (default = 0) +} +``` + +##### Switch + +```javascript +{ + type: 'switch', + key: 'theme', + label: 'Theme', + options: [ + { value: 'light', label: 'Light Mode' }, + { value: 'dark', label: 'Dark Mode' }, + { value: 'neon', label: 'Neon Mode' } + ], + initial: 'dark', // the initial value to set if not configured +} +``` + +##### Dropdown + +```javascript +{ + type: 'dropdown', + key: 'language', + label: 'Language', + options: [ + { value: 'en', label: 'English' }, + { value: 'es', label: 'Spanish' }, + { value: 'jp', label: 'Japanese' } + ], + initial: 'en', // the initial value to set if not configured +} +``` + +##### File + +```javascript +{ + type: 'file', + key: 'audioFile', + label: 'Audio File', + kind: 'audio', // must be one of: avatar, emote, model, texture, hdr, audio +} +``` + +File values are returned as an object: +```javascript +{ + type: 'audio', // the type of file (avatar, emote, model, texture, hdr, audio) + name: 'music.mp3', // the original file's name + url: 'https://...' // the url to the file +} +``` + +Usage example: +```javascript +const audio = app.create('audio', { + src: props.audioFile?.url +}) +audio.play() +``` + +##### Section + +```javascript +{ + type: 'section', + key: 'appearance', + label: 'Appearance Settings', +} +``` + +##### Color + +```javascript +{ + type: 'color', + key: 'backgroundColor', + label: 'Background Color', + value: '#00ffaa', +} +``` + +#### Conditional Fields + +Fields can be conditionally displayed based on other field values: + +```javascript +{ + key: 'customColor', + type: 'color', + label: 'Custom Color', + value: '#ff00ff', + when: [{ key: 'theme', op: 'eq', value: 'custom' }] +} +``` + +This field will only appear when the 'theme' field is set to 'custom'. + +--- + +## API Documentation Cross-Reference + +For more detailed information on each API component, refer to the following files: + +| API Component | Documentation File | +|---------------|-------------------| +| App | [App.md](mdc:api/App.md) | +| World | [World.md](mdc:api/World.md) | +| Node | [Node.md](mdc:api/Node.md) | +| UI | [UI.md](mdc:api/UI.md) | +| UIView | [UIView.md](mdc:api/UIView.md) | +| UIText | [UIText.md](mdc:api/UIText.md) | +| Player | [Player.md](mdc:api/Player.md) | +| Avatar | [Avatar.md](mdc:api/Avatar.md) | +| Audio | [Audio.md](mdc:api/Audio.md) | +| RigidBody | [RigidBody.md](mdc:api/RigidBody.md) | +| Collider | [Collider.md](mdc:api/Collider.md) | +| Action | [Action.md](mdc:api/Action.md) | +| Mesh | [Mesh.md](mdc:api/Mesh.md) | +| Material | [Material.md](mdc:api/Material.md) | +| Group | [Group.md](mdc:api/Group.md) | +| Anchor | [Anchor.md](mdc:api/Anchor.md) | +| LOD | [LOD.md](mdc:api/LOD.md) | +| Props | [Props.md](mdc:api/Props.md) | +| Numeric Utils | [num.md](mdc:api/num.md) | +| Sky | [Sky.md](mdc:../legacy/Hyperfy_DuhzDocs/core/nodes/Sky.md) | +| Controller | [Controller.md](mdc:../legacy/Hyperfy_DuhzDocs/core/nodes/Controller.md) | +| Nametag | [Nametag.md](mdc:../legacy/Hyperfy_DuhzDocs/core/nodes/Nametag.md) | + +*See also: [Developer Guide](mdc:../guides/for-developers.md) | [Examples](mdc:../examples/index.md)* \ No newline at end of file diff --git a/.cursor/rules/scripting-rule.mdc b/.cursor/rules/scripting-rule.mdc new file mode 100644 index 00000000..6da7859d --- /dev/null +++ b/.cursor/rules/scripting-rule.mdc @@ -0,0 +1,198 @@ +--- +description: rules for creating scripts for the hyperfy engine +globs: +alwaysApply: false +--- +# Overview + +Hyperfy is a web-based multiplayer collaborative world engine for building 3D games and experiences accessible in the browser. +Admins of a world can drag and drop glb models into the world in realtime and move them around etc. +When dropping a glb into the world, it becomes an "app". +Each app is comprised of a glb model, and optional script, and any number of additional config/assets that can be provided through props. +Every single thing in a world is an app. +Each script attached to an app is written in javascript. +Apps all run inside an isolated runtime environment with different globals to the ones web developers generally find when making websites. +Scripts execute on both the server and the clients. +When the server boots up, the app scripts execute first on the server, and then when clients connect the scripts run after they receive the initial snapshot. +When scripts are edited on the client by a builder, the app script changes execute instantly on the client first, and then the server is notified to reboot with the new script. +Each app script has access to all of the nodes that make up the glb model and they can move, clone or remove any part of it using code. +If in blender for example, your mesh is named "Sword" then in a script you can get a handle of this mesh node using `app.get('Sword')`. + +## The `world` global + +The app runtime exposes a `world` global variable, providing access to info about the world itself or performing world related functions + +`world.isServer` +- a boolean that is true if the code currently executing is on the server + +`world.isClient` +- a boolean that is true if the code currently executing is on a client + +`world.add(node)` +- adds a node into world-space + +`world.remove(node)` +- removes a node from world space + +`world.attach(node)` +- attaches a node into world-space, maintaining its current world transform + +`world.on(event, callback)` +- listens to world events emitted by other apps, allows inter-app communication +- applies only to its own context, eg if running on the server it will only listen to events emitted by other apps on the server + +`world.off(event, callback)` +- cancel a world event listener + +`world.getTime()` +- returns the number of seconds since the server booted up. +- this is also usable on the client which synchronizes time with the server automatically. + +`world.chat(msg, broadcast)` +- posts a message in the local chat, with the option to broadcast to all other clients and the server +- TODO: msg object needs details here + +`world.getPlayer(playerId)` +- returns a handle to a player +- if playerId is not specified, it returns the local player if running on a client + +`world.getPlayers()` +- returns an array of all players currently in the world + +`world.createLayerMask(...layers)` +- create a collision layer mask for raycasts etc +- TODO: provide more details + +`world.raycast(origin, direction, maxDistance, layerMask)` +- performs a physics raycast and returns the first hit, if any. +- hit object = { tag: String, playerId: String } + +`world.get(key)` +- returns value from key-value storage +- only works on the server + +`world.set(key, value)` +- sets a value in key-value storage +- only works on the server + +## The `app` global + +The app runtime exposes an `app` global variable that provides access to the root app node itself and also a few additional methods for the app itself + +`app.instanceId` +- a unique id for an instance of this app + +`app.state` +- the state object for this app. +- when clients connect to the world, the current value of `app.state` is sent to the client and available when the client script runs. +- that is all it does, it does not automatically sync one or both ways when changed, but it can be used to keep state up to date as server events are received. + +`app.props` +- TODO: need to describe props + +`app.on(event, callback)` +- listens to app events from itself in other contexts. +- on the client, this will be called when the server calls `app.send(event, data)` +- on the server, this will be called when a client calls `app.send(event, data)` +- to subscribe to the update event, we can do `app.on('update' (delta) => {})` + +`app.off(event, callback)` +- removes a previously bound listener + +`app.send(event, data)` +- sends event to the same app running on its opposite context +- if called on the client, it will be sent to the server +- if called on the server, it will be sent to all clients + +`app.sendTo(playerId, event, data)` +- same as `app.send` but targets a specific player client +- can only be used on the server + +`app.emit(event, data)` +- emits an event from this app to the world, for other apps that might be listening +- should generally be namedspaced (eg `mything:myevent`) to avoid inadvertent collisions +- if called on a client, only apps on that client will be able to receive the event +- if called on the server, only apps on that server will be able to receive the event + +`app.get(name)` +- returns a specifc node somewhere in the hierarchy +- nodes can be meshes, rigidbodies, colliders, and many more + +`app.create(name, data)` +- creates a node such as an action, ui, etc +- nodes can then be added to the world (`world.add(node)`) or the local app space (`app.add(node)`) or another node (`otherNode.add(node)`) +- TODO: provide more info + +`app.control(options)` +- creates a control handle for reading and writing inputs, camera transforms, etc +- TODO: provide more info + +`app.configure(fields)` +- exposes prop inputs in the app inspector that allow non-developers to configure your app +- the values of the props are read in-script from the `props` global object +- TODO: explain each field type, when logic, keys etc + +## The `node` base class + +Every single node in the world (mesh, collider, etc) inherits the node base class and its methods and properties. + +`node.id` +- a string id of the node +- this cannot be changed once set, eg a glb will have these that match blender object names, and you can set this once when creating a node with `app.create('mesh', { id: 'myId' })` if needed +- if not provided when creating a node, it will be given a unique one automatically + +`node.name` +- the node type, eg `mesh` or `action` or `rigidbody` etc + +`node.position` +- a `Vector3` representing the local position of the node + +`node.quaternion` +- a `Quaternion` representing the the local quaternion of the node +- changing this automatically syncs with `node.rotation` + +`node.rotation` +- a `Euler` representing the the local rotation of the node +- changing this automatically syncs with `node.quaternion` + +`node.scale` +- a `Vector3` representing local scale of the node + +`node.matrixWorld` +- a `Matrix4` for world transform of the node +- this is automatically updated each frame if the local transform of this or any parents change + +`node.active` +- whether the node is active or not +- when not active, it as if its not even in the world, including all of its children + +`node.parent` +- the parent node if any + +`node.children` +- an array of all child nodes + +`node.add(childNode)` +- adds a node as a child of this one + +`node.remove(childNode)` +- removes a node from a child of this one + +`node.clone(recursive)` +- clones the node so that it can be re-used +- must then be added to the world, app or another node to be active in the world + +`node.onPointerEnter` +- can be set to a function to be notified when the player reticle is pointing at this node/mesh etc + +`node.onPointerLeave` +- can be set to a function to be notified when the player reticle leaves pointing at this node/mesh etc + +`node.onPointerDown` +- can be set to a function to be notified when the player reticle clicks on this node/mesh etc + +`node.onPointerUp` +- can be set to a function to be notified when the player reticle released click on this node/mesh etc + +`node.cursor` +- changes the cursor in pointer mode when hovering over this node, useful for ui etc eg `pointer` \ No newline at end of file diff --git a/.cursor/rules/uirules.mdc b/.cursor/rules/uirules.mdc new file mode 100644 index 00000000..e874b2fa --- /dev/null +++ b/.cursor/rules/uirules.mdc @@ -0,0 +1,81 @@ +--- +description: +globs: +alwaysApply: false +--- +Reminders when using UI Nodes, UIView Nodes, UIText nodes, UIImage nodes +#### 1. Responsive Layouts with Auto or Percentage Width/Height + +- Explanation: Width and height cannot be set to auto or as percentages. They must be explicitly defined as pixel values. +- Example: Trying to set width: 'auto' or width: '50%' will not work. +#### 2. Relative Positioning + +- Explanation: The .position and .offset properties are fixed values (ratios or pixels) and do not support dynamic relative positioning based on other UI elements. +- Example: You cannot position a UI element relative to another UI element using position: relative. +#### 4. Image Styling + +- Explanation: The UIImage component does not support: + - Setting multiple background images. + - Using gradients or patterns as backgrounds. + - Applying complex image effects like shadows or filters directly on the image. +- Example: You cannot create a UIImage with a gradient background using background: linear-gradient(...). +#### 5. Polygon or Complex Shapes for UI Elements + +- Explanation: There is no way to create complex shapes (e.g., polygons, stars, or custom clip paths) for UI elements. Only basic shapes (e.g., rectangles with rounded corners) are supported. +- Example: You cannot create a UI element that is shaped like a pentagon. +#### 6. 3D Transformations + +- Explanation: The UI components do not support advanced 3D transformations (e.g., 3D rotations, 3D scaling). +- Example: You cannot apply transform: rotateX(45deg) to a UI element. +#### 7. Advanced CSS Animations + +- Explanation: The UI components do not support CSS animations, transitions, or keyframes. +- Example: You cannot animate the position or size of a UI element using CSS-like animations. +#### 8. Layered Backgrounds + +- Explanation: You cannot have multiple layered backgrounds (e.g., a background image with a semi-transparent overlay) for UI elements. +- Example: You cannot set backgroundImage and backgroundColor together for a single UI element. +#### 9. Mix Blend Modes + +- Explanation: The UI components do not support CSS mix-blend-mode properties, which would allow blending of UI elements with the scene or other UI elements. +- Example: You cannot set mixBlendMode: 'multiply' for a UI element. +#### 10. Z-Index Control + +- Explanation: The z-index property is not supported for UI elements, making it difficult to manage overlapping UI elements in complex layouts. +- Example: You cannot ensure that one UI element always appears above another by setting z-index: 100. +#### 11. Pointer Event Configuration + +- Explanation: While .pointerEvents is a boolean (true/false), specific pointer events like OnPointerDown, OnPointerEnter, OnPointerLeave, and OnPointerUp can be used to respond to cursor and click events. +- Clarification: You can handle specific pointer events for UIView and UIText elements. +- Example: You can use OnPointerDown to respond to click events on a UIView. +#### 12. Nested UI Components with Limitations + +- Explanation: While you can nest UIView components within each other, some properties (e.g., flexWrap, alignContent) may not work as expected in deeply nested layouts. +- Example: Complex nested flex layouts with wrap behavior may not behave as expected. +#### 13. Touch Events + +- Explanation: Advanced touch events (e.g., multi-touch gestures, swipe detection) are not supported or well-documented. +- Example: You cannot detect swipe gestures on a UI element. +#### 14. Performance Considerations + +- Explanation: Performance considerations are more relevant for world space UI elements. Screen space UI, being more lightweight, is less likely to face performance issues even with complex layouts. +- Clarification: Performance impacts vary based on the UI space (world or screen). +- Example: World space UI with many nested elements may negatively impact performance. +#### Workaround for Responsive UI with Fixed Sizes +If you want to create a responsive UI, calculate pixel values dynamically using app.control() for screen dimensions, as app.window is not accessible. +```// Workaround: Calculate width and height dynamically +const control = app.control(); +const screenWidth = control.screenWidth(); +const screenHeight = control.screenHeight(); + +const ui = app.create('ui', { + space: 'screen', + position: [0, 1, 0], + width: screenWidth * 0.3, // 30% of screen width (calculated in pixels) + height: screenHeight * 0.2, // 20% of screen height (calculated in pixels) + backgroundColor: 'rgba(0, 0, 0, 0.7)', + pivot: 'top-left' +}); +``` + + diff --git a/src/core/createClientWorld.js b/src/core/createClientWorld.js index 9b0f240f..d1bcd603 100644 --- a/src/core/createClientWorld.js +++ b/src/core/createClientWorld.js @@ -1,7 +1,6 @@ import { World } from './World' import { Client } from './systems/Client' -import { ClientLiveKit } from './systems/ClientLiveKit' import { ClientPointer } from './systems/ClientPointer' import { ClientPrefs } from './systems/ClientPrefs' import { ClientControls } from './systems/ClientControls' @@ -25,7 +24,6 @@ import { XR } from './systems/XR' export function createClientWorld() { const world = new World() world.register('client', Client) - world.register('livekit', ClientLiveKit) world.register('pointer', ClientPointer) world.register('prefs', ClientPrefs) world.register('controls', ClientControls) diff --git a/src/core/systems/ClientControls.js b/src/core/systems/ClientControls.js index 424e96ab..ea79d75c 100644 --- a/src/core/systems/ClientControls.js +++ b/src/core/systems/ClientControls.js @@ -538,6 +538,14 @@ export class ClientControls extends System { async lockPointer() { this.pointer.shouldLock = true + + // Check if we're in a sandboxed iframe without pointer lock permission + if (this.isInSandboxedFrame()) { + console.warn('Pointer lock not available in sandboxed frame - using alternative controls') + this.onPointerLockStart() // Simulate pointer lock for consistent behavior + return false + } + try { await this.viewport.requestPointerLock() return true @@ -547,6 +555,27 @@ export class ClientControls extends System { } } + isInSandboxedFrame() { + try { + // Check if we're in an iframe + if (window.self === window.top) { + return false + } + + // Check if pointer lock is available + if (!document.documentElement.requestPointerLock) { + return true + } + + // Try to access parent window (will throw in sandboxed frame) + const testAccess = window.parent.location.href + return false + } catch (err) { + // If we can't access parent or get security error, we're likely sandboxed + return true + } + } + unlockPointer() { this.pointer.shouldLock = false if (!this.pointer.locked) return diff --git a/src/core/systems/ClientLiveKit.js b/src/core/systems/ClientLiveKit.js deleted file mode 100644 index b5991cbe..00000000 --- a/src/core/systems/ClientLiveKit.js +++ /dev/null @@ -1,418 +0,0 @@ -import { Participant, ParticipantEvent, Room, RoomEvent, ScreenSharePresets, Track } from 'livekit-client' -import * as THREE from '../extras/three' - -import { System } from './System' -import { isBoolean } from 'lodash-es' -import { TrackSource } from 'livekit-server-sdk' - -const v1 = new THREE.Vector3() -const v2 = new THREE.Vector3() -const q1 = new THREE.Quaternion() - -export class ClientLiveKit extends System { - constructor(world) { - super(world) - this.room = null - this.status = { - available: false, - connected: false, - mic: false, - screenshare: null, - } - this.voices = new Map() // playerId -> PlayerVoice - this.screens = [] - this.screenNodes = new Set() // Video - } - - async deserialize(opts) { - if (!opts) return - this.status.available = true - // console.log(opts) - this.room = new Room({ - webAudioMix: { - audioContext: this.world.audio.ctx, - }, - publishDefaults: { - screenShareEncoding: ScreenSharePresets.h1080fps30.encoding, - screenShareSimulcastLayers: [ScreenSharePresets.h1080fps30], - }, - }) - this.room.on(RoomEvent.TrackMuted, this.onTrackMuted) - this.room.on(RoomEvent.TrackUnmuted, this.onTrackUnmuted) - this.room.on(RoomEvent.LocalTrackPublished, this.onLocalTrackPublished) - this.room.on(RoomEvent.LocalTrackUnpublished, this.onLocalTrackUnpublished) - this.room.on(RoomEvent.TrackSubscribed, this.onTrackSubscribed) - this.room.on(RoomEvent.TrackUnsubscribed, this.onTrackUnsubscribed) - this.room.localParticipant.on(ParticipantEvent.IsSpeakingChanged, speaking => { - this.world.entities.player.setSpeaking(speaking) - }) - this.world.audio.ready(async () => { - await this.room.connect(opts.wsUrl, opts.token) - this.status.connected = true - this.emit('status', this.status) - }) - } - - lateUpdate(delta) { - this.voices.forEach(voice => { - voice.lateUpdate(delta) - }) - } - - setMicrophoneEnabled(value) { - if (!this.room) return console.error('[livekit] setMicrophoneEnabled failed (not connected)') - value = isBoolean(value) ? value : !this.room.localParticipant.isMicrophoneEnabled - if (this.status.mic === value) return - this.room.localParticipant.setMicrophoneEnabled(value) - } - - setScreenShareTarget(targetId = null) { - if (!this.room) return console.error('[livekit] setScreenShareTarget failed (not connected)') - if (this.status.screenshare === targetId) return - const metadata = JSON.stringify({ - screenTargetId: targetId, - }) - this.room.localParticipant.setMetadata(metadata) - this.room.localParticipant.setScreenShareEnabled(!!targetId, { - // audio: true, - // systemAudio: 'include', - }) - } - - onTrackMuted = track => { - // console.log('onTrackMuted', track) - if (track.isLocal && track.source === 'microphone') { - this.status.mic = false - this.emit('status', this.status) - } - } - - onTrackUnmuted = track => { - // console.log('onTrackUnmuted', track) - if (track.isLocal && track.source === 'microphone') { - this.status.mic = true - this.emit('status', this.status) - } - } - - onLocalTrackPublished = publication => { - const world = this.world - const track = publication.track - const playerId = this.world.network.id - // console.log('onLocalTrackPublished', publication) - if (publication.source === 'microphone') { - this.status.mic = true - this.emit('status', this.status) - } - if (publication.source === 'screen_share') { - const metadata = JSON.parse(this.room.localParticipant.metadata || '{}') - const targetId = metadata.screenTargetId - this.status.screenshare = targetId - const screen = createPlayerScreen({ world, playerId, targetId, track, publication }) - this.addScreen(screen) - this.emit('status', this.status) - } - } - - onLocalTrackUnpublished = publication => { - const playerId = this.world.network.id - // console.log('onLocalTrackUnpublished', pub) - if (publication.source === 'microphone') { - this.status.mic = false - this.emit('status', this.status) - } - if (publication.source === 'screen_share') { - const screen = this.screens.find(s => s.playerId === playerId) - this.removeScreen(screen) - this.status.screenshare = null - this.emit('status', this.status) - } - } - - onTrackSubscribed = (track, publication, participant) => { - // console.log('onTrackSubscribed', track, publication, participant) - const playerId = participant.identity - const player = this.world.entities.getPlayer(playerId) - if (!player) return console.error('onTrackSubscribed failed: no player') - const world = this.world - if (track.source === 'microphone') { - const voice = new PlayerVoice(world, player, track, participant) - this.voices.set(playerId, voice) - } - if (track.source === 'screen_share') { - const metadata = JSON.parse(participant.metadata || '{}') - const targetId = metadata.screenTargetId - const screen = createPlayerScreen({ world, playerId, targetId, track, publication }) - this.addScreen(screen) - } - } - - onTrackUnsubscribed = (track, publication, participant) => { - // console.log('onTrackUnsubscribed todo') - const playerId = participant.identity - if (track.source === 'microphone') { - const voice = this.voices.get(playerId) - voice?.destroy() - this.voices.delete(playerId) - } - if (track.source === 'screen_share') { - const screen = this.screens.find(s => s.playerId === playerId) - this.removeScreen(screen) - } - } - - addScreen(screen) { - this.screens.push(screen) - for (const node of this.screenNodes) { - if (node._screenId === screen.targetId) { - node.needsRebuild = true - node.setDirty() - } - } - } - - removeScreen(screen) { - screen.destroy() - this.screens = this.screens.filter(s => s !== screen) - for (const node of this.screenNodes) { - if (node._screenId === screen.targetId) { - node.needsRebuild = true - node.setDirty() - } - } - } - - registerScreenNode(node) { - this.screenNodes.add(node) - let match - for (const screen of this.screens) { - if (screen.targetId === node._screenId) { - match = screen - } - } - return match - } - - unregisterScreenNode(node) { - this.screenNodes.delete(node) - } - - destroy() { - this.voices.forEach(voice => { - voice.destroy() - }) - this.voices.clear() - this.screens.forEach(screen => { - screen.destroy() - }) - this.screens = [] - this.screenNodes.clear() - if (this.room) { - this.room.off(RoomEvent.TrackMuted, this.onTrackMuted) - this.room.off(RoomEvent.TrackUnmuted, this.onTrackUnmuted) - this.room.off(RoomEvent.LocalTrackPublished, this.onLocalTrackPublished) - this.room.off(RoomEvent.LocalTrackUnpublished, this.onLocalTrackUnpublished) - this.room.off(RoomEvent.TrackSubscribed, this.onTrackSubscribed) - this.room.off(RoomEvent.TrackUnsubscribed, this.onTrackUnsubscribed) - if (this.room.localParticipant) { - this.room.localParticipant.off(ParticipantEvent.IsSpeakingChanged) - } - this.room?.disconnect() - } - } -} - -class PlayerVoice { - constructor(world, player, track, participant) { - this.world = world - this.player = player - this.track = track - this.participant = participant - this.track.setAudioContext(world.audio.ctx) - this.spatial = true // todo: switch to global - this.panner = world.audio.ctx.createPanner() - this.panner.panningModel = 'HRTF' - this.panner.panningModel = 'HRTF' - this.panner.distanceModel = 'inverse' - this.panner.refDistance = 1 - this.panner.maxDistance = 40 - this.panner.rolloffFactor = 3 - this.panner.coneInnerAngle = 360 - this.panner.coneOuterAngle = 360 - this.panner.coneOuterGain = 0 - this.gain = world.audio.groupGains.voice - this.panner.connect(this.gain) - this.track.attach() - this.track.setWebAudioPlugins([this.spatial ? this.panner : this.gain]) - this.participant.on(ParticipantEvent.IsSpeakingChanged, speaking => { - this.player.setSpeaking(speaking) - }) - } - - lateUpdate(delta) { - const audio = this.world.audio - const matrix = this.player.base.matrixWorld - const pos = v1.setFromMatrixPosition(matrix) - const qua = q1.setFromRotationMatrix(matrix) - const dir = v2.set(0, 0, -1).applyQuaternion(qua) - if (this.panner.positionX) { - const endTime = audio.ctx.currentTime + audio.lastDelta - this.panner.positionX.linearRampToValueAtTime(pos.x, endTime) - this.panner.positionY.linearRampToValueAtTime(pos.y, endTime) - this.panner.positionZ.linearRampToValueAtTime(pos.z, endTime) - this.panner.orientationX.linearRampToValueAtTime(dir.x, endTime) - this.panner.orientationY.linearRampToValueAtTime(dir.y, endTime) - this.panner.orientationZ.linearRampToValueAtTime(dir.z, endTime) - } else { - this.panner.setPosition(pos.x, pos.y, pos.z) - this.panner.setOrientation(dir.x, dir.y, dir.z) - } - } - - destroy() { - this.player.setSpeaking(false) - this.track.detach() - } -} - -function createPlayerScreen({ world, playerId, targetId, track, participant }) { - // NOTE: this follows the same construct in ClientLoader.js -> createVideoFactory - // so that it is automatically compatible with the video node - const elem = document.createElement('video') - elem.playsInline = true - elem.muted = true - // elem.style.width = '1px' - // elem.style.height = '1px' - // elem.style.position = 'absolute' - // elem.style.opacity = '0' - // elem.style.zIndex = '-1000' - // elem.style.pointerEvents = 'none' - // elem.style.overflow = 'hidden' - // document.body.appendChild(elem) - track.attach(elem) - // elem.play() - const texture = new THREE.VideoTexture(elem) - texture.colorSpace = THREE.SRGBColorSpace - texture.minFilter = THREE.LinearFilter - texture.magFilter = THREE.LinearFilter - texture.anisotropy = world.graphics.maxAnisotropy - texture.needsUpdate = true - let width - let height - let ready = false - const prepare = (function () { - /** - * - * A regular video will load data automatically BUT a stream - * needs to hit play() before it gets that data. - * - * The following code handles this for us, and when streaming - * will hit play just until we get the data needed, then pause. - */ - return new Promise(async resolve => { - let playing = false - let data = false - elem.addEventListener( - 'loadeddata', - async () => { - // if we needed to hit play to fetch data then revert back to paused - // console.log('[video] loadeddata', { playing }) - if (playing) elem.pause() - data = true - // await new Promise(resolve => setTimeout(resolve, 2000)) - width = elem.videoWidth - height = elem.videoHeight - console.log({ width, height }) - ready = true - resolve() - }, - { once: true } - ) - elem.addEventListener( - 'loadedmetadata', - async () => { - // we need a gesture before we can potentially hit play - // console.log('[video] ready') - // await this.engine.driver.gesture - // if we already have data do nothing, we're done! - // console.log('[video] gesture', { data }) - if (data) return - // otherwise hit play to force data loading for streams - // elem.play() - // playing = true - }, - { once: true } - ) - }) - })() - function isPlaying() { - return true - // return elem.currentTime > 0 && !elem.paused && !elem.ended && elem.readyState > 2 - } - function play(restartIfPlaying = false) { - // if (restartIfPlaying) elem.currentTime = 0 - // elem.play() - } - function pause() { - // elem.pause() - } - function stop() { - // elem.currentTime = 0 - // elem.pause() - } - function release() { - // stop() - // audio.disconnect() - // track.detach() - // texture.dispose() - // document.body.removeChild(elem) - } - function destroy() { - console.log('destory') - texture.dispose() - // help to prevent chrome memory leaks - // see: https://github.com/facebook/react/issues/15583#issuecomment-490912533 - // elem.src = '' - // elem.load() - } - const handle = { - isScreen: true, - playerId, - targetId, - elem, - audio: null, - texture, - prepare, - get ready() { - return ready - }, - get width() { - return width - }, - get height() { - return height - }, - get loop() { - return false - // return elem.loop - }, - set loop(value) { - // elem.loop = value - }, - get isPlaying() { - return isPlaying() - }, - get currentTime() { - return elem.currentTime - }, - set currentTime(value) { - elem.currentTime = value - }, - play, - pause, - stop, - release, - destroy, - } - return handle -} diff --git "a/src/world/collections/default/Geoff\360\237\223\271.hyp" "b/src/world/collections/default/Geoff\360\237\223\271.hyp" new file mode 100644 index 00000000..e61991c9 Binary files /dev/null and "b/src/world/collections/default/Geoff\360\237\223\271.hyp" differ diff --git "a/src/world/collections/default/Place\360\237\214\200.hyp" "b/src/world/collections/default/Place\360\237\214\200.hyp" new file mode 100644 index 00000000..bde4a17e Binary files /dev/null and "b/src/world/collections/default/Place\360\237\214\200.hyp" differ diff --git "a/src/world/collections/default/Portal\360\237\214\200.hyp" "b/src/world/collections/default/Portal\360\237\214\200.hyp" new file mode 100644 index 00000000..d201e151 Binary files /dev/null and "b/src/world/collections/default/Portal\360\237\214\200.hyp" differ diff --git "a/src/world/collections/default/Portal\360\237\214\220.hyp" "b/src/world/collections/default/Portal\360\237\214\220.hyp" new file mode 100644 index 00000000..fa694520 Binary files /dev/null and "b/src/world/collections/default/Portal\360\237\214\220.hyp" differ diff --git "a/src/world/collections/default/Seat\360\237\252\221.hyp" "b/src/world/collections/default/Seat\360\237\252\221.hyp" new file mode 100644 index 00000000..377bc4f4 Binary files /dev/null and "b/src/world/collections/default/Seat\360\237\252\221.hyp" differ diff --git a/src/world/collections/default/Spherea.hyp b/src/world/collections/default/Spherea.hyp new file mode 100644 index 00000000..66113df8 Binary files /dev/null and b/src/world/collections/default/Spherea.hyp differ diff --git "a/src/world/collections/default/Sphere\360\237\214\200.hyp" "b/src/world/collections/default/Sphere\360\237\214\200.hyp" new file mode 100644 index 00000000..aca50c57 Binary files /dev/null and "b/src/world/collections/default/Sphere\360\237\214\200.hyp" differ diff --git "a/src/world/collections/default/Sphere\360\237\224\212.hyp" "b/src/world/collections/default/Sphere\360\237\224\212.hyp" new file mode 100644 index 00000000..82a70f23 Binary files /dev/null and "b/src/world/collections/default/Sphere\360\237\224\212.hyp" differ diff --git a/src/world/collections/default/manifest.json b/src/world/collections/default/manifest.json index 7113d996..e1f9398d 100644 --- a/src/world/collections/default/manifest.json +++ b/src/world/collections/default/manifest.json @@ -1,4 +1,17 @@ { "name": "Default", - "apps": ["Model.hyp", "Image.hyp", "Video.hyp", "Text.hyp"] -} + "apps": [ + "Model.hyp", + "Image.hyp", + "Video.hyp", + "Text.hyp", + "Geoff📹.hyp", + "Place🌀.hyp", + "Portal🌀.hyp", + "Portal🌐.hyp", + "Seat🪑.hyp", + "Sphere🌀.hyp", + "Sphere🔊.hyp", + "Spherea.hyp" + ] +} \ No newline at end of file