From 26e16747289592d21c3ac009143a017cd145e4f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Can=20Erdog=CC=86an?= Date: Wed, 11 Feb 2026 00:40:13 +0300 Subject: [PATCH 1/5] docs: Add live demos to all doc pages and update content - Add 12 new interactive demo HTML files (2048, cookie-clicker, gem-collector, neon-pong, quiz-game, rps-game, simon-says, slingshot, whack-a-mole, etc.) - Update 24 documentation pages with LiveDemo components placed after title - Add demo placement rule to CLAUDE.md for consistent future docs - Enhance changelog, architecture, configuration, and getting-started pages - Improve rendering, physics, input, layout, and UI component docs Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 9 + docs-site/docs/assets/loading-caching.md | 11 +- docs-site/docs/audio/overview.md | 11 +- docs-site/docs/boilerplate/overview.md | 2 + docs-site/docs/boilerplate/simple-screen.md | 9 +- docs-site/docs/changelog/v1.3.0.md | 21 + docs-site/docs/core-concepts/architecture.md | 18 + docs-site/docs/core-concepts/configuration.md | 8 + .../getting-started/first-game-tutorial.md | 4 +- .../docs/getting-started/installation.md | 8 + docs-site/docs/getting-started/quick-start.md | 22 +- docs-site/docs/input/overview.md | 13 +- docs-site/docs/input/touch.md | 6 + docs-site/docs/layout/examples.md | 11 +- docs-site/docs/layout/layout-manager.md | 9 +- docs-site/docs/layout/overview.md | 14 +- docs-site/docs/layout/presets.md | 9 +- docs-site/docs/overview.md | 14 +- docs-site/docs/physics/overview.md | 11 +- docs-site/docs/rendering/3d-three.md | 10 + docs-site/docs/rendering/hybrid-mode.md | 16 +- docs-site/docs/rendering/overview.md | 21 +- docs-site/docs/scenes/scene-management.md | 19 +- docs-site/docs/ui-components/overview.md | 18 + .../docs/ui-components/responsive-layout.md | 9 +- docs-site/static/demos/2048-demo.html | 516 +++++++++++++++++ docs-site/static/demos/architecture-demo.html | 274 +++++++++ .../static/demos/config-playground-demo.html | 393 +++++++++++++ .../static/demos/cookie-clicker-demo.html | 408 +++++++++++++ .../static/demos/gem-collector-demo.html | 481 ++++++++++++++++ docs-site/static/demos/hello-world-demo.html | 147 +++++ docs-site/static/demos/neon-pong-demo.html | 418 ++++++++++++++ docs-site/static/demos/quiz-game-demo.html | 458 +++++++++++++++ docs-site/static/demos/rps-game-demo.html | 529 +++++++++++++++++ docs-site/static/demos/simon-says-demo.html | 372 ++++++++++++ .../static/demos/slingshot-game-demo.html | 541 ++++++++++++++++++ docs-site/static/demos/whack-a-mole-demo.html | 508 ++++++++++++++++ 37 files changed, 5253 insertions(+), 95 deletions(-) create mode 100644 docs-site/static/demos/2048-demo.html create mode 100644 docs-site/static/demos/architecture-demo.html create mode 100644 docs-site/static/demos/config-playground-demo.html create mode 100644 docs-site/static/demos/cookie-clicker-demo.html create mode 100644 docs-site/static/demos/gem-collector-demo.html create mode 100644 docs-site/static/demos/hello-world-demo.html create mode 100644 docs-site/static/demos/neon-pong-demo.html create mode 100644 docs-site/static/demos/quiz-game-demo.html create mode 100644 docs-site/static/demos/rps-game-demo.html create mode 100644 docs-site/static/demos/simon-says-demo.html create mode 100644 docs-site/static/demos/slingshot-game-demo.html create mode 100644 docs-site/static/demos/whack-a-mole-demo.html diff --git a/CLAUDE.md b/CLAUDE.md index 9f32fd7..99caba2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -314,6 +314,15 @@ For browser usage without bundlers, use `dist/gamebyte.umd.js`. It exposes `wind - Constants in SCREAMING_SNAKE_CASE - Private members prefixed with underscore or using `private` keyword +## Documentation Site - Demo Placement Rule + +When adding or editing documentation pages in `docs-site/docs/`: +- **Primary LiveDemo must appear right after the page title** (before any code blocks or detailed text) +- `import LiveDemo` goes after frontmatter, before the `# Title` +- Each page should have at least one relevant demo +- Demo HTML files live in `docs-site/static/demos/` +- New demos should follow the existing pattern: Pixi CDN + `../gamebyte.umd.js`, dark theme, self-contained + ## Documentation - Main README: `README.md` diff --git a/docs-site/docs/assets/loading-caching.md b/docs-site/docs/assets/loading-caching.md index 8f3df33..ceef6c7 100644 --- a/docs-site/docs/assets/loading-caching.md +++ b/docs-site/docs/assets/loading-caching.md @@ -9,10 +9,16 @@ llm_summary: "Assets.load([{ key, url, type }]). Types: texture, audio, json, mo +import LiveDemo from '@site/src/components/LiveDemo'; + # Asset Loading & Caching GameByte's asset system handles loading, caching, and memory management. + + + + ## Basic Loading ```typescript @@ -118,8 +124,3 @@ if (Assets.isBundleLoaded('level-2')) { } ``` -## Live Demo - -import LiveDemo from '@site/src/components/LiveDemo'; - - diff --git a/docs-site/docs/audio/overview.md b/docs-site/docs/audio/overview.md index e8d89ee..60b66c6 100644 --- a/docs-site/docs/audio/overview.md +++ b/docs-site/docs/audio/overview.md @@ -9,10 +9,16 @@ llm_summary: "Audio system: Music (background loops), SFX (effects), Spatial (3D +import LiveDemo from '@site/src/components/LiveDemo'; + # Audio Overview GameByte's audio system handles music, sound effects, and spatial audio. + + + + ## Audio Types | Type | Use Case | Example | @@ -72,8 +78,3 @@ Audio.unmute(); Audio.toggle(); ``` -## Live Demo - -import LiveDemo from '@site/src/components/LiveDemo'; - - diff --git a/docs-site/docs/boilerplate/overview.md b/docs-site/docs/boilerplate/overview.md index ac25b74..d4b4b81 100644 --- a/docs-site/docs/boilerplate/overview.md +++ b/docs-site/docs/boilerplate/overview.md @@ -6,6 +6,8 @@ The GameByte Framework provides a complete boilerplate system for building hyper ## Live Demo + + + ## When to Use Use `SimpleScreen` instead of `BaseUIScreen` when you need: @@ -160,8 +164,3 @@ class LoadingScreen extends SimpleScreen { } ``` -## Live Demo - -import LiveDemo from '@site/src/components/LiveDemo'; - - diff --git a/docs-site/docs/changelog/v1.3.0.md b/docs-site/docs/changelog/v1.3.0.md index 13cab84..190bf06 100644 --- a/docs-site/docs/changelog/v1.3.0.md +++ b/docs-site/docs/changelog/v1.3.0.md @@ -16,6 +16,7 @@ llm_summary: "GameByte v1.3.0 release notes. 10 new modules: TickSystem (per-com import Head from '@docusaurus/Head'; +import LiveDemo from '@site/src/components/LiveDemo'; @@ -414,6 +415,26 @@ sounds.play('coin', { volume: 0.5, pitch: 1.2, variation: 0.3 }); --- +## Live Feature Demos + +### TickSystem & Game Loop in Action + + + +### GameSoundPresets - Procedural Audio + + + +### PostProcessing Pipeline + + + +### 3D Features (InstanceManager, Environment) + + + +--- + ## Other Improvements ### Responsive Resize Integration diff --git a/docs-site/docs/core-concepts/architecture.md b/docs-site/docs/core-concepts/architecture.md index 6b16923..128d2a9 100644 --- a/docs-site/docs/core-concepts/architecture.md +++ b/docs-site/docs/core-concepts/architecture.md @@ -9,10 +9,20 @@ llm_summary: "Laravel-inspired DI container. game.bind(key, factory) registers s +import LiveDemo from '@site/src/components/LiveDemo'; + # Architecture GameByte uses a Laravel-inspired architecture with a service container, service providers, and facades. + + + + ## Service Container The service container manages all game services and their dependencies. @@ -238,6 +248,14 @@ interface ServiceMap { const score = game.make('score.manager'); ``` +## Reactive State + + + ## Architecture Diagram ``` diff --git a/docs-site/docs/core-concepts/configuration.md b/docs-site/docs/core-concepts/configuration.md index 96429fd..347335a 100644 --- a/docs-site/docs/core-concepts/configuration.md +++ b/docs-site/docs/core-concepts/configuration.md @@ -9,10 +9,18 @@ llm_summary: "Configure via createGame({ config }) or game.setConfig(). Key opti +import LiveDemo from '@site/src/components/LiveDemo'; + # Configuration GameByte provides sensible defaults with full customization options. + + ## Configuration Methods ### At Creation diff --git a/docs-site/docs/getting-started/first-game-tutorial.md b/docs-site/docs/getting-started/first-game-tutorial.md index eb6532b..dde81f7 100644 --- a/docs-site/docs/getting-started/first-game-tutorial.md +++ b/docs-site/docs/getting-started/first-game-tutorial.md @@ -360,7 +360,9 @@ private collectCoin(): void { } ``` -## Complete Code +## Complete Game Demo + + See the [full example on GitHub](https://github.com/gamebyte-ai/gamebyte-framework/tree/main/examples/platformer). diff --git a/docs-site/docs/getting-started/installation.md b/docs-site/docs/getting-started/installation.md index 383c12a..c8c975c 100644 --- a/docs-site/docs/getting-started/installation.md +++ b/docs-site/docs/getting-started/installation.md @@ -9,10 +9,18 @@ llm_summary: "Install via npm: 'npm install @gamebyte/framework'. For browser: u +import LiveDemo from '@site/src/components/LiveDemo'; + # Installation GameByte Framework can be installed via npm or used directly in the browser via CDN. + + ## npm (Recommended) ```bash diff --git a/docs-site/docs/getting-started/quick-start.md b/docs-site/docs/getting-started/quick-start.md index 2f1c4a2..a2c0eb8 100644 --- a/docs-site/docs/getting-started/quick-start.md +++ b/docs-site/docs/getting-started/quick-start.md @@ -15,6 +15,16 @@ import LiveDemo from '@site/src/components/LiveDemo'; Build your first GameByte game in 5 minutes. + + +:::tip Theme Support +This demo automatically adapts to your selected theme. Try toggling the theme using the 🌙/☀️ button in the navigation bar! +::: + ## Step 1: Setup HTML ```html @@ -103,18 +113,6 @@ Open the HTML file in a browser or use a local server: npx http-server -p 8080 ``` -## Live Demo - - - -:::tip Theme Support -This demo automatically adapts to your selected theme. Try toggling the theme using the 🌙/☀️ button in the navigation bar! -::: - ## What's Happening? 1. **`createGame()`** - Creates the game instance with service container diff --git a/docs-site/docs/input/overview.md b/docs-site/docs/input/overview.md index d474532..251c092 100644 --- a/docs-site/docs/input/overview.md +++ b/docs-site/docs/input/overview.md @@ -9,10 +9,18 @@ llm_summary: "Unified input: Input.keyboard, Input.touch, Input.gamepad. Event-b +import LiveDemo from '@site/src/components/LiveDemo'; + # Input Overview GameByte provides a unified input system for all platforms. + + + + + + ## Input Sources | Source | Use Case | @@ -61,8 +69,3 @@ function update(deltaTime: number) { } ``` -## Live Demo - -import LiveDemo from '@site/src/components/LiveDemo'; - - diff --git a/docs-site/docs/input/touch.md b/docs-site/docs/input/touch.md index 0aa8731..47d0b36 100644 --- a/docs-site/docs/input/touch.md +++ b/docs-site/docs/input/touch.md @@ -15,6 +15,12 @@ import LiveDemo from '@site/src/components/LiveDemo'; Mobile-optimized touch handling with gesture support. + + ## Basic Touch ```typescript diff --git a/docs-site/docs/layout/examples.md b/docs-site/docs/layout/examples.md index 401dd94..52aef28 100644 --- a/docs-site/docs/layout/examples.md +++ b/docs-site/docs/layout/examples.md @@ -7,10 +7,14 @@ keywords: [layout, examples, game-ui, patterns, flexbox] llm_summary: "Common game UI layout patterns: Game HUD with top/bottom bars, level select grid, settings menu, shop screen, leaderboard. All use flexbox via @pixi/layout." --- +import LiveDemo from '@site/src/components/LiveDemo'; + # Layout Examples Common layout patterns for mobile game UI using the GameByte layout system. + + ## Game HUD Typical game screen with top bar, content area, and bottom navigation: @@ -430,10 +434,3 @@ window.addEventListener('resize', () => { }); ``` -## Live Demo - -See all these patterns in action: - -import LiveDemo from '@site/src/components/LiveDemo'; - - diff --git a/docs-site/docs/layout/layout-manager.md b/docs-site/docs/layout/layout-manager.md index e0a6d93..f3c1849 100644 --- a/docs-site/docs/layout/layout-manager.md +++ b/docs-site/docs/layout/layout-manager.md @@ -7,10 +7,14 @@ keywords: [layout-manager, responsive, breakpoints, scaling, api] llm_summary: "LayoutManager handles layout initialization, responsive scaling, and breakpoints. Use getLayoutManager() for singleton access. Provides presets and responsive utilities." --- +import LiveDemo from '@site/src/components/LiveDemo'; + # LayoutManager API The `LayoutManager` class provides centralized layout management with responsive scaling support. + + ## Basic Usage ```typescript @@ -346,8 +350,3 @@ async function createGame() { } ``` -## Live Demo - -import LiveDemo from '@site/src/components/LiveDemo'; - - diff --git a/docs-site/docs/layout/overview.md b/docs-site/docs/layout/overview.md index 0d3195e..5d2e2df 100644 --- a/docs-site/docs/layout/overview.md +++ b/docs-site/docs/layout/overview.md @@ -7,10 +7,16 @@ keywords: [layout, flexbox, yoga, responsive, pixi-layout] llm_summary: "GameByte uses @pixi/layout (Yoga flexbox engine) for responsive UI layouts. Supports flex direction, justify content, align items, gap, wrap, and percentage sizing." --- +import LiveDemo from '@site/src/components/LiveDemo'; + # Layout System GameByte integrates [@pixi/layout](https://layout.pixijs.io/) - a powerful Yoga-based flexbox layout system for PixiJS. This enables CSS-like flexbox layouts for game UI. + + + + ## Why Flexbox for Games? - **Responsive Design**: Automatically adapts to different screen sizes @@ -113,14 +119,6 @@ container.layout = LayoutPresets.center; hudContainer.layout = GameLayoutPresets.gameScreen; ``` -## Live Demo - -Interactive examples of all layout features: - -import LiveDemo from '@site/src/components/LiveDemo'; - - - ## Next Steps - [Layout Presets](./presets) - Pre-defined layout configurations diff --git a/docs-site/docs/layout/presets.md b/docs-site/docs/layout/presets.md index 7a7a5a6..5dc9bd2 100644 --- a/docs-site/docs/layout/presets.md +++ b/docs-site/docs/layout/presets.md @@ -7,10 +7,14 @@ keywords: [layout, presets, flexbox, ui-patterns, game-ui] llm_summary: "LayoutPresets provides pre-defined flexbox configurations. GameLayoutPresets has game-specific layouts. Helper functions create custom layouts easily." --- +import LiveDemo from '@site/src/components/LiveDemo'; + # Layout Presets GameByte provides pre-defined layout configurations for common UI patterns. + + ## LayoutPresets General-purpose layout presets: @@ -286,8 +290,3 @@ const scaledLayout = scaleLayout(LayoutPresets.card, 1.5); container.layout = scaledLayout; ``` -## Live Demo - -import LiveDemo from '@site/src/components/LiveDemo'; - - diff --git a/docs-site/docs/overview.md b/docs-site/docs/overview.md index 1ce1e5a..b68434d 100644 --- a/docs-site/docs/overview.md +++ b/docs-site/docs/overview.md @@ -94,8 +94,20 @@ GameByte is optimized for AI-driven development: See the [AI Agent Guide](/ai-agent-guide) for integration details. -## Live Demo +## Live Demos import LiveDemo from '@site/src/components/LiveDemo'; + +### Space Shooter + + + +### 3D Rendering + + + +### Physics Engine + + diff --git a/docs-site/docs/physics/overview.md b/docs-site/docs/physics/overview.md index c8e891b..a0897fb 100644 --- a/docs-site/docs/physics/overview.md +++ b/docs-site/docs/physics/overview.md @@ -9,10 +9,16 @@ llm_summary: "2D: Matter.js via Physics.create2DWorld(). 3D: Cannon.js via Physi +import LiveDemo from '@site/src/components/LiveDemo'; + # Physics Overview GameByte integrates industry-standard physics engines for realistic simulations. + + + + ## Engines | Dimension | Engine | Use Cases | @@ -84,8 +90,3 @@ Physics.onCollision('player', 'enemy', (playerBody, enemyBody) => { - **FPS/TPS**: 3D (Cannon.js) - **Hybrid (3D world, 2D gameplay)**: Either based on gameplay needs -## Live Demo - -import LiveDemo from '@site/src/components/LiveDemo'; - - diff --git a/docs-site/docs/rendering/3d-three.md b/docs-site/docs/rendering/3d-three.md index bbe0da0..c6ce9f8 100644 --- a/docs-site/docs/rendering/3d-three.md +++ b/docs-site/docs/rendering/3d-three.md @@ -332,3 +332,13 @@ mesh.frustumCulled = true; // Disable for always-visible objects skybox.frustumCulled = false; ``` + +## More 3D Demos + +### Camera Controls + + + +### Pathfinding in 3D + + diff --git a/docs-site/docs/rendering/hybrid-mode.md b/docs-site/docs/rendering/hybrid-mode.md index fe5fdac..5dfbb7f 100644 --- a/docs-site/docs/rendering/hybrid-mode.md +++ b/docs-site/docs/rendering/hybrid-mode.md @@ -15,6 +15,12 @@ import LiveDemo from '@site/src/components/LiveDemo'; Hybrid mode combines Three.js (3D world) with Pixi.js (2D UI overlay) for the best of both worlds. + + ## When to Use Hybrid Mode - 3D games with complex UI (RPGs, strategy games) @@ -161,16 +167,6 @@ class GameScene extends BaseScene { } ``` - - -:::tip Theme Support -This demo automatically adapts to your selected theme. Try toggling the theme using the 🌙/☀️ button in the navigation bar! -::: - ## Layer Management ### Z-Order Control diff --git a/docs-site/docs/rendering/overview.md b/docs-site/docs/rendering/overview.md index a1803b6..baf3c04 100644 --- a/docs-site/docs/rendering/overview.md +++ b/docs-site/docs/rendering/overview.md @@ -9,10 +9,26 @@ llm_summary: "Three render modes: '2d' (Pixi.js), '3d' (Three.js), 'hybrid' (bot +import LiveDemo from '@site/src/components/LiveDemo'; + # Rendering Overview GameByte supports three rendering modes to cover any game type. + + +### 2D Rendering (Pixi.js) + + + +### 3D Rendering (Three.js) + + + +### Hybrid Mode (2D + 3D) + + + ## Rendering Modes | Mode | Engine | Use Case | @@ -196,8 +212,3 @@ renderer.add(mesh); - [3D Rendering](/rendering/3d-three) - Deep dive into Three.js - [Hybrid Mode](/rendering/hybrid-mode) - Combining 2D and 3D -## Live Demo - -import LiveDemo from '@site/src/components/LiveDemo'; - - diff --git a/docs-site/docs/scenes/scene-management.md b/docs-site/docs/scenes/scene-management.md index b50d397..92315e9 100644 --- a/docs-site/docs/scenes/scene-management.md +++ b/docs-site/docs/scenes/scene-management.md @@ -9,10 +9,18 @@ llm_summary: "SceneManager: add(), switchTo(), remove(). BaseScene lifecycle: in +import LiveDemo from '@site/src/components/LiveDemo'; + # Scene Management Scenes organize your game into distinct states like menus, gameplay, and game over screens. + + + + + + ## Basic Usage ```typescript @@ -232,14 +240,3 @@ await sceneManager.switchTo('game'); // Instant switch await sceneManager.preloadAll(['game', 'settings', 'shop']); ``` -## Live Demo - -import LiveDemo from '@site/src/components/LiveDemo'; - - - -### Memory Game Example - -This memory card game demonstrates scene state management with card matching logic, timer, and score tracking: - - diff --git a/docs-site/docs/ui-components/overview.md b/docs-site/docs/ui-components/overview.md index 2f5aab5..e2c6f9d 100644 --- a/docs-site/docs/ui-components/overview.md +++ b/docs-site/docs/ui-components/overview.md @@ -237,6 +237,24 @@ const button = new UIButton({ }); ``` +## More Component Demos + +### Game Buttons + + + +### Top Bar (HUD) + + + +### Hexagon Level Selector + + + +### Bottom Navigation + + + ## Performance Tips 1. **Reuse components** instead of creating/destroying diff --git a/docs-site/docs/ui-components/responsive-layout.md b/docs-site/docs/ui-components/responsive-layout.md index 74b8f45..915b27d 100644 --- a/docs-site/docs/ui-components/responsive-layout.md +++ b/docs-site/docs/ui-components/responsive-layout.md @@ -9,10 +9,14 @@ llm_summary: "ResponsiveLayoutManager handles screen adaptation. Use UIContainer +import LiveDemo from '@site/src/components/LiveDemo'; + # Responsive Layout GameByte's layout system adapts to any screen size. + + ## Screen Adaptation ```typescript @@ -236,8 +240,3 @@ layout.on('orientationchange', (orientation) => { layout.lockOrientation('landscape'); ``` -## Live Demo - -import LiveDemo from '@site/src/components/LiveDemo'; - - diff --git a/docs-site/static/demos/2048-demo.html b/docs-site/static/demos/2048-demo.html new file mode 100644 index 0000000..407df53 --- /dev/null +++ b/docs-site/static/demos/2048-demo.html @@ -0,0 +1,516 @@ + + + + + + GameByte - 2048 Demo + + + +

2048

+

GameByte Layout System - Grid & Dynamic Repositioning

+
+
Arrow keys or swipe to slide tiles. Merge equal numbers!
+ + + + + diff --git a/docs-site/static/demos/architecture-demo.html b/docs-site/static/demos/architecture-demo.html new file mode 100644 index 0000000..b852360 --- /dev/null +++ b/docs-site/static/demos/architecture-demo.html @@ -0,0 +1,274 @@ + + + + + + GameByte - Architecture Demo + + + +

Service Container Architecture

+

Click services to resolve them - see singleton vs factory behavior

+
+
+ Singletons return the same instance (same color). Factories create new instances each time (different colors). +
+ + + + + + + diff --git a/docs-site/static/demos/config-playground-demo.html b/docs-site/static/demos/config-playground-demo.html new file mode 100644 index 0000000..bf95ab4 --- /dev/null +++ b/docs-site/static/demos/config-playground-demo.html @@ -0,0 +1,393 @@ + + + + + + GameByte - Config Playground + + + +

Configuration Playground

+

Adjust settings and see changes in real-time

+ +
+
+

Renderer

+ +
+ + +
+ +
+ + +
+ +
+ + Antialias +
+ +

Debug

+ +
+ + Show FPS +
+ +
+ + Show Grid +
+ +
+ + +
+ +

Animation

+ +
+ + +
+ +
+ + +
+ +
+
+ +
+
+ + + + + + + diff --git a/docs-site/static/demos/cookie-clicker-demo.html b/docs-site/static/demos/cookie-clicker-demo.html new file mode 100644 index 0000000..5604de7 --- /dev/null +++ b/docs-site/static/demos/cookie-clicker-demo.html @@ -0,0 +1,408 @@ + + + + + + GameByte - Cookie Clicker Demo + + + +

Cookie Clicker

+

GameByte Core Concepts - Service Container & Reactive State

+
+
Click the cookie! Buy upgrades to earn more.
+ + + + + + + diff --git a/docs-site/static/demos/gem-collector-demo.html b/docs-site/static/demos/gem-collector-demo.html new file mode 100644 index 0000000..b9efc87 --- /dev/null +++ b/docs-site/static/demos/gem-collector-demo.html @@ -0,0 +1,481 @@ + + + + + + GameByte - Gem Collector Demo + + + +

Gem Collector

+

GameByte Asset System - Loading, Progress & Caching

+
+
Arrow keys or tap sides to move. Catch gems, avoid bombs!
+ + + + + diff --git a/docs-site/static/demos/hello-world-demo.html b/docs-site/static/demos/hello-world-demo.html new file mode 100644 index 0000000..8122708 --- /dev/null +++ b/docs-site/static/demos/hello-world-demo.html @@ -0,0 +1,147 @@ + + + + + + GameByte - Hello World Demo + + + +

Hello World

+

Verify your GameByte installation is working

+
+
Initializing...
+
If you see the rotating square and version text, GameByte is correctly installed!
+ + + + + + + diff --git a/docs-site/static/demos/neon-pong-demo.html b/docs-site/static/demos/neon-pong-demo.html new file mode 100644 index 0000000..41fc185 --- /dev/null +++ b/docs-site/static/demos/neon-pong-demo.html @@ -0,0 +1,418 @@ + + + + + + GameByte - Neon Pong Demo + + + +

Neon Pong

+

GameByte Rendering - Graphics Primitives & Effects

+
+
Move mouse or arrow keys to control paddle. First to 5 wins!
+ + + + + diff --git a/docs-site/static/demos/quiz-game-demo.html b/docs-site/static/demos/quiz-game-demo.html new file mode 100644 index 0000000..a5788c4 --- /dev/null +++ b/docs-site/static/demos/quiz-game-demo.html @@ -0,0 +1,458 @@ + + + + + + GameByte - Quiz Game Demo + + + +

Trivia Quiz

+

GameByte Screens & Panels - Hub, Modals & Bottom Sheets

+
+
Choose a category, answer questions, see your score!
+ + + + + diff --git a/docs-site/static/demos/rps-game-demo.html b/docs-site/static/demos/rps-game-demo.html new file mode 100644 index 0000000..d0d245f --- /dev/null +++ b/docs-site/static/demos/rps-game-demo.html @@ -0,0 +1,529 @@ + + + + + + GameByte - Rock Paper Scissors + + + +

Rock Paper Scissors

+

GameByte Scene Management - Multi-Scene Transitions

+
+
Best of 5 rounds! Choose your weapon.
+ + + + + + diff --git a/docs-site/static/demos/simon-says-demo.html b/docs-site/static/demos/simon-says-demo.html new file mode 100644 index 0000000..2e7745c --- /dev/null +++ b/docs-site/static/demos/simon-says-demo.html @@ -0,0 +1,372 @@ + + + + + + GameByte - Simon Says Demo + + + +

Simon Says

+

GameByte Audio System - Tones, Sequences & Sound Feedback

+
+
Watch the pattern, repeat the sequence! Each button plays a unique tone.
+ + + + + diff --git a/docs-site/static/demos/slingshot-game-demo.html b/docs-site/static/demos/slingshot-game-demo.html new file mode 100644 index 0000000..4a3576c --- /dev/null +++ b/docs-site/static/demos/slingshot-game-demo.html @@ -0,0 +1,541 @@ + + + + + + GameByte - Slingshot Game Demo + + + + +

Slingshot

+

GameByte Physics - Forces, Collisions & Gravity

+
+
Drag to aim, release to launch! Clear all targets.
+ + + + + + diff --git a/docs-site/static/demos/whack-a-mole-demo.html b/docs-site/static/demos/whack-a-mole-demo.html new file mode 100644 index 0000000..658ef22 --- /dev/null +++ b/docs-site/static/demos/whack-a-mole-demo.html @@ -0,0 +1,508 @@ + + + + + + GameByte - Whack-a-Mole Demo + + + +

Whack-a-Mole

+

GameByte Input System - Touch, Click & Keyboard

+
+
Click/tap moles or use keys 1-9! Golden mole = 50pts, Red mole = 25pts, Brown mole = 10pts.
+ + + + + From d09b47cdc7f5d944dc2ff22e87a63e3015002cc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Can=20Erdog=CC=86an?= Date: Wed, 11 Feb 2026 10:46:25 +0300 Subject: [PATCH 2/5] fix: Escape dynamic values in config-playground innerHTML (CodeQL #10) Add esc() helper to sanitize DOM values before interpolating into innerHTML template string. Prevents potential XSS via DOM manipulation of range/select input values. Co-Authored-By: Claude Opus 4.6 --- .../static/demos/config-playground-demo.html | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/docs-site/static/demos/config-playground-demo.html b/docs-site/static/demos/config-playground-demo.html index bf95ab4..6b3521b 100644 --- a/docs-site/static/demos/config-playground-demo.html +++ b/docs-site/static/demos/config-playground-demo.html @@ -333,17 +333,23 @@

Animation

return parseInt(hex.replace('#', ''), 16); } + function esc(v) { + const d = document.createElement('div'); + d.textContent = String(v); + return d.innerHTML; + } + function updateConfigOutput() { const bgHex = '#' + bgColorHex.toString(16).padStart(6, '0'); const configStr = `{ renderer: { - backgroundColor: ${bgHex}, - resolution: ${document.getElementById('resolution').value}, - antialias: ${document.getElementById('antialias').checked} + backgroundColor: ${esc(bgHex)}, + resolution: ${esc(document.getElementById('resolution').value)}, + antialias: ${esc(document.getElementById('antialias').checked)} }, debug: { - showFPS: ${showFPS}, - logLevel: '${document.getElementById('logLevel').value}' + showFPS: ${esc(showFPS)}, + logLevel: '${esc(document.getElementById('logLevel').value)}' } }`; document.getElementById('configOutput').innerHTML = configStr; From 78be5bdcc854832081803dee1c2a95b2354bd228 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Can=20Erdog=CC=86an?= Date: Wed, 11 Feb 2026 23:24:23 +0300 Subject: [PATCH 3/5] fix: Resolve remaining CodeQL security alerts (#1,#3,#4,#5,#6,#7,#8,#9) - UIAnimationSystem: Guard setNestedProperty against prototype pollution (__proto__, constructor, prototype keys rejected) - GameSplash: Sanitize cfg.logoUrl in HTML template, fix incomplete style-tag stripping with iterative replacement loop - PixiVersionDetection: Add input length guard to prevent regex backtracking - archero-menu-demo: Add SRI integrity hash for GSAP CDN script - Rebuild UMD bundle with all source fixes applied Co-Authored-By: Claude Opus 4.6 --- docs-site/static/demos/archero-menu-demo.html | 2 +- docs-site/static/gamebyte.umd.js | 3584 ++++++++++++++++- src/ui/animations/UIAnimationSystem.ts | 11 +- src/ui/splash/GameSplash.ts | 11 +- src/utils/PixiVersionDetection.ts | 4 + 5 files changed, 3470 insertions(+), 142 deletions(-) diff --git a/docs-site/static/demos/archero-menu-demo.html b/docs-site/static/demos/archero-menu-demo.html index b7ca65d..0beca1d 100644 --- a/docs-site/static/demos/archero-menu-demo.html +++ b/docs-site/static/demos/archero-menu-demo.html @@ -11,7 +11,7 @@ canvas { display: block; } - + diff --git a/docs-site/static/gamebyte.umd.js b/docs-site/static/gamebyte.umd.js index 1f01be9..c3fd46a 100644 --- a/docs-site/static/gamebyte.umd.js +++ b/docs-site/static/gamebyte.umd.js @@ -1,8 +1,8 @@ (function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('pixi.js'), require('matter-js'), require('cannon-es'), require('@pixi/layout')) : - typeof define === 'function' && define.amd ? define(['exports', 'pixi.js', 'matter-js', 'cannon-es', '@pixi/layout'], factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.GameByteFramework = {}, global.PIXI, global.Matter, global.CANNON)); -})(this, (function (exports, PIXI, Matter, CANNON) { 'use strict'; + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('pixi.js'), require('matter-js'), require('cannon-es'), require('@pixi/layout'), require('three'), require('three/examples/jsm/loaders/GLTFLoader.js'), require('three/examples/jsm/loaders/DRACOLoader.js')) : + typeof define === 'function' && define.amd ? define(['exports', 'pixi.js', 'matter-js', 'cannon-es', '@pixi/layout', 'three', 'three/examples/jsm/loaders/GLTFLoader.js', 'three/examples/jsm/loaders/DRACOLoader.js'], factory) : + (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.GameByteFramework = {}, global.PIXI, global.Matter, global.CANNON, global.PIXI, global.THREE, global.GLTFLoader_js, global.DRACOLoader_js)); +})(this, (function (exports, PIXI, Matter, CANNON, layout, THREE, GLTFLoader_js, DRACOLoader_js) { 'use strict'; function _interopNamespaceDefault(e) { var n = Object.create(null); @@ -23,6 +23,7 @@ var PIXI__namespace = /*#__PURE__*/_interopNamespaceDefault(PIXI); var CANNON__namespace = /*#__PURE__*/_interopNamespaceDefault(CANNON); + var THREE__namespace = /*#__PURE__*/_interopNamespaceDefault(THREE); function getDefaultExportFromCjs (x) { return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; @@ -691,6 +692,43 @@ } } } + /** + * Pixi.js Filter Wrapper + */ + class PixiFilterWrapper { + constructor(filter, type) { + this.filter = filter; + this.type = type; + } + get enabled() { + return this.filter.enabled; + } + set enabled(value) { + this.filter.enabled = value; + } + get native() { + return this.filter; + } + destroy() { + this.filter.destroy(); + } + } + /** + * Pixi.js Mask Wrapper + */ + class PixiMaskWrapper { + constructor(native, type) { + this._native = native; + this.type = type; + this.inverted = false; + } + get native() { + return this._native; + } + destroy() { + this._native.destroy(); + } + } /** * Pixi.js Graphics Factory */ @@ -762,6 +800,63 @@ }); return new PixiFillGradientWrapper(gradient, 'radial'); } + // ============================================ + // FILTERS (Pixi.js v8) + // Core filters: BlurFilter, ColorMatrixFilter, DisplacementFilter, NoiseFilter, AlphaFilter + // Extended filters require 'pixi-filters' package + // ============================================ + createBlurFilter(options = {}) { + const filter = new PIXI__namespace.BlurFilter({ + strength: options.strength ?? 8, + quality: options.quality ?? 4, + kernelSize: options.kernelSize ?? 5, + }); + return new PixiFilterWrapper(filter, 'blur'); + } + createColorMatrixFilter(options = {}) { + const filter = new PIXI__namespace.ColorMatrixFilter(); + if (options.matrix && options.matrix.length === 20) { + // ColorMatrix requires exactly 20 values + filter.matrix = options.matrix; + } + return new PixiFilterWrapper(filter, 'colorMatrix'); + } + createDropShadowFilter(options = {}) { + // DropShadowFilter requires 'pixi-filters' package + // Fallback: BlurFilter only (limited shadow effect) + console.warn('DropShadowFilter requires pixi-filters package (npm install pixi-filters). ' + + 'Using BlurFilter as limited fallback.'); + const blur = options.blur ?? 4; + const filter = new PIXI__namespace.BlurFilter({ strength: blur }); + return new PixiFilterWrapper(filter, 'dropShadow'); + } + createGlowFilter(options = {}) { + // GlowFilter requires 'pixi-filters' package + // Fallback: BlurFilter only (limited glow effect) + console.warn('GlowFilter requires pixi-filters package (npm install pixi-filters). ' + + 'Using BlurFilter as limited fallback.'); + const distance = options.distance ?? 10; + const filter = new PIXI__namespace.BlurFilter({ strength: distance * 0.5 }); + return new PixiFilterWrapper(filter, 'glow'); + } + createOutlineFilter(options = {}) { + // OutlineFilter requires 'pixi-filters' package + // No good fallback available + console.warn('OutlineFilter requires pixi-filters package (npm install pixi-filters). ' + + 'No fallback available - returning no-op filter.'); + // Return a no-op filter + const filter = new PIXI__namespace.AlphaFilter({ alpha: 1 }); + return new PixiFilterWrapper(filter, 'outline'); + } + // ============================================ + // MASKS (Pixi.js v8) + // ============================================ + createMaskFromGraphics(graphics) { + return new PixiMaskWrapper(graphics, 'graphics'); + } + createMaskFromSprite(sprite) { + return new PixiMaskWrapper(sprite, 'sprite'); + } /** * Convert framework text style to Pixi v8 style object format * Pixi v8 uses object format for stroke and dropShadow @@ -1112,15 +1207,6 @@ document.body.appendChild(canvas); } } - // Handle resize to container - if (config.resizeToContainer && containerElement) { - game.resizeObserver = new ResizeObserver(() => { - canvas.width = containerElement.clientWidth; - canvas.height = containerElement.clientHeight; - game.emit('resize', canvas.width, canvas.height); - }); - game.resizeObserver.observe(containerElement); - } // Determine rendering mode (default to 2D) let mode; switch (config.mode) { @@ -1143,7 +1229,34 @@ resolution: config.resolution ?? window.devicePixelRatio, ...config.rendererOptions }; + // Responsive resize integration + // When resizeToContainer is true, automatically enable responsive mode with container tracking. + // This replaces the old manual ResizeObserver that only updated canvas dimensions. + const baseWidth = (config.width || canvas.width); + const baseHeight = (config.height || canvas.height); + if (config.resizeToContainer && containerElement) { + // Container-based responsive: tracks container size via ResizeObserver + scale calculation + const userResponsive = typeof config.responsive === 'object' ? config.responsive : null; + rendererOptions.responsive = { + baseWidth: userResponsive?.baseWidth ?? baseWidth, + baseHeight: userResponsive?.baseHeight ?? baseHeight, + minScale: userResponsive?.minScale, + maxScale: userResponsive?.maxScale, + container: containerElement + }; + } + else if (config.responsive) { + // Window-based responsive: tracks window size + const userResponsive = typeof config.responsive === 'object' ? config.responsive : null; + rendererOptions.responsive = { + baseWidth: userResponsive?.baseWidth ?? baseWidth, + baseHeight: userResponsive?.baseHeight ?? baseHeight, + minScale: userResponsive?.minScale, + maxScale: userResponsive?.maxScale + }; + } // Initialize game (this sets up update/render event hooks automatically) + // rendererOptions may contain renderer-specific fields (e.g., responsive) beyond RendererOptions await game.initialize(canvas, mode, rendererOptions); // Auto-start if requested if (config.autoStart !== false) { @@ -6511,6 +6624,10 @@ * Parse semantic version string into components */ function parseVersion(versionString) { + // Guard against excessively long input to prevent regex backtracking + if (!versionString || versionString.length > 128) { + return { major: 0, minor: 0, patch: 0, raw: versionString || '' }; + } const match = versionString.match(/(\d+)\.(\d+)\.(\d+)/); if (!match) { return { major: 0, minor: 0, patch: 0, raw: versionString }; @@ -6964,21 +7081,25 @@ class ResponsiveScaleCalculator { constructor(config) { this.resizeCallbacks = []; + this.resizeObserver = null; + this.windowResizeHandler = null; this.config = { baseWidth: config.baseWidth, baseHeight: config.baseHeight, minScale: config.minScale ?? 0.5, maxScale: config.maxScale ?? 2.0 }; + this.container = config.container ?? null; this.currentSize = this.calculate(); this.setupResizeObserver(); } /** - * Calculate current responsive size and scale factor + * Calculate current responsive size and scale factor. + * Uses container dimensions if container is set, otherwise window dimensions. */ calculate() { - const width = window.innerWidth; - const height = window.innerHeight; + const width = this.container ? this.container.clientWidth : window.innerWidth; + const height = this.container ? this.container.clientHeight : window.innerHeight; // Calculate scale factor based on width (mobile-first) const scale = Math.max(this.config.minScale, Math.min(width / this.config.baseWidth, this.config.maxScale)); return { width, height, scale }; @@ -7018,22 +7139,51 @@ } } /** - * Set up window resize observer + * Set up resize observer — uses ResizeObserver on container or window resize event */ setupResizeObserver() { - window.addEventListener('resize', () => { - const oldScale = this.currentSize.scale; - this.currentSize = this.calculate(); - // Only trigger callbacks if scale changed significantly - if (Math.abs(this.currentSize.scale - oldScale) > 0.01) { - this.resizeCallbacks.forEach(callback => callback(this.currentSize)); - } - }); + if (this.container) { + // Container-based: use ResizeObserver for precise container tracking + this.resizeObserver = new ResizeObserver(() => { + this.handleResize(); + }); + this.resizeObserver.observe(this.container); + } + else { + // Window-based: use window resize event + this.windowResizeHandler = () => { + this.handleResize(); + }; + window.addEventListener('resize', this.windowResizeHandler); + } + } + /** + * Common resize handler — recalculates and notifies if changed + */ + handleResize() { + const oldSize = this.currentSize; + this.currentSize = this.calculate(); + // Container mode: always notify (container dimensions are the source of truth) + // Window mode: only notify if scale changed significantly + const shouldNotify = this.container + ? (oldSize.width !== this.currentSize.width || oldSize.height !== this.currentSize.height) + : Math.abs(this.currentSize.scale - oldSize.scale) > 0.01; + if (shouldNotify) { + this.resizeCallbacks.forEach(callback => callback(this.currentSize)); + } } /** * Destroy and clean up */ destroy() { + if (this.resizeObserver) { + this.resizeObserver.disconnect(); + this.resizeObserver = null; + } + if (this.windowResizeHandler) { + window.removeEventListener('resize', this.windowResizeHandler); + this.windowResizeHandler = null; + } this.resizeCallbacks = []; } } @@ -16864,20 +17014,22 @@ } return current; } - /** - * Set nested property value - */ setNestedProperty(obj, path, value) { const keys = path.split('.'); let current = obj; for (let i = 0; i < keys.length - 1; i++) { const key = keys[i]; + if (GameByteUIAnimationSystem.FORBIDDEN_KEYS.has(key)) + return; if (!(key in current) || typeof current[key] !== 'object') { current[key] = {}; } current = current[key]; } - current[keys[keys.length - 1]] = value; + const finalKey = keys[keys.length - 1]; + if (GameByteUIAnimationSystem.FORBIDDEN_KEYS.has(finalKey)) + return; + current[finalKey] = value; } /** * Generate unique animation ID @@ -16899,6 +17051,10 @@ this.animationFrameId = requestAnimationFrame(update); } } + /** + * Set nested property value + */ + GameByteUIAnimationSystem.FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']); /** * Timeline for sequencing animations */ @@ -40419,7 +40575,7 @@ * 3. Call GameSplash.init() once JS loads * 4. Call splash.hide() to transition out */ - const DEFAULT_CONFIG$4 = { + const DEFAULT_CONFIG$5 = { logoUrl: '/img/logo-icon.svg', backgroundColor: '#0a0a1a', glowColor: '#6366f1', @@ -40443,7 +40599,7 @@ constructor(config = {}) { this.element = null; this.onComplete = null; - this.config = { ...DEFAULT_CONFIG$4, ...config }; + this.config = { ...DEFAULT_CONFIG$5, ...config }; } /** * Initialize splash screen (call once JS loads) @@ -40470,7 +40626,14 @@ // Create splash if not found in HTML const css = document.createElement('style'); css.id = 'gamebyte-splash-styles'; - css.textContent = GameSplash.getInlineCSS(this.config).replace(/<\/?style[^>]*>/g, ''); + let cssText = GameSplash.getInlineCSS(this.config); + // Strip style tags iteratively to prevent bypass via nesting + let prev = ''; + while (prev !== cssText) { + prev = cssText; + cssText = cssText.replace(/<\/?style[^>]*>/gi, ''); + } + css.textContent = cssText; document.head.appendChild(css); document.body.insertAdjacentHTML('afterbegin', GameSplash.getInlineHTML(this.config)); this.element = document.getElementById('gamebyte-splash'); @@ -40516,7 +40679,7 @@ * Premium effects: light rays, lens flare, orbital rings, sparkles, particles */ static getInlineCSS(config = {}) { - const cfg = { ...DEFAULT_CONFIG$4, ...config }; + const cfg = { ...DEFAULT_CONFIG$5, ...config }; const glowRgb = hexToRgb(cfg.glowColor); const accentRgb = hexToRgb(cfg.accentColor); return ` @@ -40825,7 +40988,7 @@ * Includes: light rays, lens flare, orbital rings, sparkles, particles, logo */ static getInlineHTML(config = {}) { - const cfg = { ...DEFAULT_CONFIG$4, ...config }; + const cfg = { ...DEFAULT_CONFIG$5, ...config }; // SVG for 4-pointed star const starSvg = ''; return ` @@ -40873,7 +41036,7 @@ `; @@ -40904,7 +41067,7 @@ * loading.setProgress(50); * await loading.hide(); */ - const DEFAULT_CONFIG$3 = { + const DEFAULT_CONFIG$4 = { width: 400, height: 600, backgroundColor: 0x1a1a2e, @@ -40927,7 +41090,7 @@ this.progressFill = null; this.loadingText = null; this.currentProgress = 0; - this.config = { ...DEFAULT_CONFIG$3, ...config }; + this.config = { ...DEFAULT_CONFIG$4, ...config }; const gfx = graphics(); this.container = gfx.createContainer(); // Ensure font is loaded @@ -41428,7 +41591,7 @@ createSettingsButton() { const buttonSize = this.config.height - 10; this.settingsButton = graphics().createContainer(); - this.settingsButton.x = this.config.padding; + this.settingsButton.x = this.config.padding + buttonSize / 2; this.settingsButton.y = this.config.height / 2; // Background circle const bg = graphics().createGraphics(); @@ -46690,23 +46853,23 @@ } SignalConnections$1.SignalConnections = SignalConnections; - (function (exports) { - Object.defineProperty(exports, "__esModule", { value: true }); - exports.SignalConnections = exports.Signal = exports.CollectorWhile0 = exports.CollectorUntil0 = exports.CollectorLast = exports.CollectorArray = exports.Collector = void 0; + (function (exports$1) { + Object.defineProperty(exports$1, "__esModule", { value: true }); + exports$1.SignalConnections = exports$1.Signal = exports$1.CollectorWhile0 = exports$1.CollectorUntil0 = exports$1.CollectorLast = exports$1.CollectorArray = exports$1.Collector = void 0; var Collector_1 = Collector$1; - Object.defineProperty(exports, "Collector", { enumerable: true, get: function () { return Collector_1.Collector; } }); + Object.defineProperty(exports$1, "Collector", { enumerable: true, get: function () { return Collector_1.Collector; } }); var CollectorArray_1 = CollectorArray$1; - Object.defineProperty(exports, "CollectorArray", { enumerable: true, get: function () { return CollectorArray_1.CollectorArray; } }); + Object.defineProperty(exports$1, "CollectorArray", { enumerable: true, get: function () { return CollectorArray_1.CollectorArray; } }); var CollectorLast_1 = CollectorLast$1; - Object.defineProperty(exports, "CollectorLast", { enumerable: true, get: function () { return CollectorLast_1.CollectorLast; } }); + Object.defineProperty(exports$1, "CollectorLast", { enumerable: true, get: function () { return CollectorLast_1.CollectorLast; } }); var CollectorUntil0_1 = CollectorUntil0$1; - Object.defineProperty(exports, "CollectorUntil0", { enumerable: true, get: function () { return CollectorUntil0_1.CollectorUntil0; } }); + Object.defineProperty(exports$1, "CollectorUntil0", { enumerable: true, get: function () { return CollectorUntil0_1.CollectorUntil0; } }); var CollectorWhile0_1 = CollectorWhile0$1; - Object.defineProperty(exports, "CollectorWhile0", { enumerable: true, get: function () { return CollectorWhile0_1.CollectorWhile0; } }); + Object.defineProperty(exports$1, "CollectorWhile0", { enumerable: true, get: function () { return CollectorWhile0_1.CollectorWhile0; } }); var Signal_1 = Signal$1; - Object.defineProperty(exports, "Signal", { enumerable: true, get: function () { return Signal_1.Signal; } }); + Object.defineProperty(exports$1, "Signal", { enumerable: true, get: function () { return Signal_1.Signal; } }); var SignalConnections_1 = SignalConnections$1; - Object.defineProperty(exports, "SignalConnections", { enumerable: true, get: function () { return SignalConnections_1.SignalConnections; } }); + Object.defineProperty(exports$1, "SignalConnections", { enumerable: true, get: function () { return SignalConnections_1.SignalConnections; } }); } (dist)); var __defProp$k = Object.defineProperty; @@ -54672,7 +54835,7 @@ /** * Default confetti configuration */ - const DEFAULT_CONFIG$2 = { + const DEFAULT_CONFIG$3 = { colors: [0xFFD700, 0xFF6B6B, 0x6BCB77, 0x4D96FF, 0xFF69B4, 0xFFFFFF], particleCount: 50, duration: 2000, @@ -54718,7 +54881,7 @@ * Best for: Victory screens, level complete */ rain(config = {}) { - const cfg = { ...DEFAULT_CONFIG$2, ...config }; + const cfg = { ...DEFAULT_CONFIG$3, ...config }; this.isActive = true; this.pendingParticles = cfg.particleCount; // Track pending particles for (let i = 0; i < cfg.particleCount; i++) { @@ -54741,7 +54904,7 @@ * Best for: Star earned, reward received */ burst(x, y, config = {}) { - const cfg = { ...DEFAULT_CONFIG$2, ...config }; + const cfg = { ...DEFAULT_CONFIG$3, ...config }; this.isActive = true; const spreadRad = (cfg.spread * Math.PI) / 180; for (let i = 0; i < cfg.particleCount; i++) { @@ -54757,7 +54920,7 @@ * Best for: Bonus, jackpot, special rewards */ fountain(x, y, config = {}) { - const cfg = { ...DEFAULT_CONFIG$2, ...config }; + const cfg = { ...DEFAULT_CONFIG$3, ...config }; this.isActive = true; const spreadRad = (cfg.spread * Math.PI) / 180 / 2; for (let i = 0; i < cfg.particleCount; i++) { @@ -54841,7 +55004,7 @@ // Update life p.life += deltaTime; // Apply gravity - p.vy += DEFAULT_CONFIG$2.gravity * dt * 60; + p.vy += DEFAULT_CONFIG$3.gravity * dt * 60; // Apply wobble (horizontal oscillation) const wobble = Math.sin(p.life * 0.01 * p.wobbleSpeed + p.wobblePhase) * 0.5; // Update position @@ -55320,7 +55483,7 @@ /** * Default configuration */ - const DEFAULT_CONFIG$1 = { + const DEFAULT_CONFIG$2 = { radius: 30, count: 4, colors: [0xFFFFFF, 0xFFF8DC, 0xFFD700], @@ -55371,7 +55534,7 @@ * Sparkles will continuously appear and animate around the target */ addZone(target, config = {}) { - const cfg = { ...DEFAULT_CONFIG$1, ...config }; + const cfg = { ...DEFAULT_CONFIG$2, ...config }; const factory = getGraphicsFactory(); // Create container for this zone's particles const zoneContainer = factory.createContainer(); @@ -56068,7 +56231,7 @@ /** * Default configuration */ - const DEFAULT_CONFIG = { + const DEFAULT_CONFIG$1 = { rayCount: 12, innerRadius: 20, outerRadius: 150, @@ -56131,7 +56294,7 @@ this.rayMask = null; // For global method this.raysGraphics = null; - this.config = { ...DEFAULT_CONFIG, ...config }; + this.config = { ...DEFAULT_CONFIG$1, ...config }; const factory = getGraphicsFactory(); this.container = factory.createContainer(); this.build(); @@ -59375,108 +59538,3236 @@ return isReactive(value) ? value() : value; } + /** Maximum delta clamp in seconds (prevents huge jumps on tab background) */ + const MAX_DELTA_S = 0.1; // 100ms + /** EMA smoothing factor for FPS (lower = smoother) */ + const FPS_ALPHA = 0.05; /** - * GameByte Framework - Main Entry Point + * Component-Level Render Loop System. * - * A comprehensive JavaScript game engine framework that unifies 2D and 3D - * game development with Laravel-inspired architecture. + * Allows per-component tick subscriptions with priority ordering, + * inspired by R3F's useFrame hook. + * + * Performance characteristics: + * - O(1) per-frame FPS tracking (EMA, not rolling array) + * - Pre-allocated TickState object (zero GC pressure) + * - for-loop iteration (15-30% faster than forEach in hot paths) + * - Dirty-flag re-sorting (only on add/remove) + * - Lazy removal during iteration (flag-based, no splice) + * - Visibility API: auto-pause when document.hidden */ - // Core Framework - function createGame(config) { - // If config provided, use quick setup - if (config) { - return GameByte.quick(config); + class TickSystem extends EventEmitter { + constructor() { + super(); + this.subscribers = []; + this.onceQueue = []; + this.nextId = 1; + this.needsSort = false; + this.paused = false; + this.destroyed = false; + this.smoothFps = 60; + this.boundVisibilityHandler = null; + /** Pre-allocated state object - mutated in-place each frame */ + this.state = { + delta: 0, + deltaMs: 0, + elapsed: 0, + frame: 0, + fps: 60 + }; + this.setupVisibilityAPI(); + } + /** + * Subscribe a callback to the tick loop. + * Lower priority numbers run first (negative = early, positive = late). + * Default priority is 0. + */ + subscribe(callback, priority = 0, options) { + const id = this.nextId++; + const subscriber = { + id, + callback, + priority, + active: true, + fixedStep: options?.fixedStep ?? null, + fixedAccumulator: 0, + removed: false + }; + this.subscribers.push(subscriber); + this.needsSort = true; + const handle = { + id, + priority, + active: true, + pause: () => { + subscriber.active = false; + handle.active = false; + }, + resume: () => { + subscriber.active = true; + handle.active = true; + }, + unsubscribe: () => { + this.unsubscribe(handle); + } + }; + return handle; + } + /** + * Remove a subscription by handle. + * Uses flag-based lazy removal to avoid splice during iteration. + */ + unsubscribe(handle) { + const len = this.subscribers.length; + for (let i = 0; i < len; i++) { + if (this.subscribers[i].id === handle.id) { + this.subscribers[i].removed = true; + this.needsSort = true; // will compact on next sort + break; + } + } + } + /** + * Register a one-shot callback (runs once on next tick, then auto-removed). + */ + runOnce(callback) { + this.onceQueue.push(callback); + } + /** + * Get current tick state (readonly snapshot). + */ + getState() { + return this.state; + } + /** + * Get count of active subscribers. + */ + getSubscriberCount() { + let count = 0; + const len = this.subscribers.length; + for (let i = 0; i < len; i++) { + if (!this.subscribers[i].removed) + count++; + } + return count; + } + /** + * Pause the tick system (subscribers won't receive ticks). + */ + pause() { + this.paused = true; + this.emit('paused'); + } + /** + * Resume the tick system. + */ + resume() { + this.paused = false; + this.emit('resumed'); + } + /** + * Called by the renderer's tick event each frame. + * This is the hot path - zero allocations, for-loop iteration. + * @param deltaMs - Delta time in milliseconds from renderer + */ + tick(deltaMs) { + if (this.paused || this.destroyed) + return; + // Clamp delta to prevent huge jumps (tab backgrounding) + const clampedMs = Math.min(deltaMs, MAX_DELTA_S * 1000); + const deltaS = clampedMs / 1000; + // Update state object in-place (zero allocation) + this.state.deltaMs = clampedMs; + this.state.delta = deltaS; + this.state.elapsed += deltaS; + this.state.frame++; + // EMA FPS calculation - O(1), zero allocations (Babylon.js pattern) + if (clampedMs > 0) { + const instantFps = 1000 / clampedMs; + this.smoothFps += FPS_ALPHA * (instantFps - this.smoothFps); + this.state.fps = Math.round(this.smoothFps); + } + // Sort if dirty (only happens on add/remove, not every frame) + if (this.needsSort) { + this.compactAndSort(); + } + // Iterate subscribers - for-loop, NOT forEach (Phaser 3 pattern) + const subs = this.subscribers; + const len = subs.length; + for (let i = 0; i < len; i++) { + const sub = subs[i]; + if (!sub.active || sub.removed) + continue; + if (sub.fixedStep !== null) { + // Fixed timestep accumulator (Gaffer pattern) + sub.fixedAccumulator += deltaS; + while (sub.fixedAccumulator >= sub.fixedStep) { + sub.fixedAccumulator -= sub.fixedStep; + // Temporarily set delta to fixed step for the callback + const originalDelta = this.state.delta; + const originalDeltaMs = this.state.deltaMs; + this.state.delta = sub.fixedStep; + this.state.deltaMs = sub.fixedStep * 1000; + sub.callback(this.state); + this.state.delta = originalDelta; + this.state.deltaMs = originalDeltaMs; + } + } + else { + sub.callback(this.state); + } + } + // Drain once queue after main loop + if (this.onceQueue.length > 0) { + const queue = this.onceQueue; + this.onceQueue = []; + for (let i = 0; i < queue.length; i++) { + queue[i](this.state); + } + } + this.emit('tick', this.state); + } + /** + * Compact removed entries and re-sort by priority. + * Only called when needsSort is true (add/remove occurred). + */ + compactAndSort() { + // Filter out removed subscribers + let writeIdx = 0; + for (let i = 0; i < this.subscribers.length; i++) { + if (!this.subscribers[i].removed) { + this.subscribers[writeIdx++] = this.subscribers[i]; + } + } + this.subscribers.length = writeIdx; + // Sort by priority (lower runs first) + this.subscribers.sort((a, b) => a.priority - b.priority); + this.needsSort = false; + } + /** + * Setup Visibility API to auto-pause when tab is hidden. + */ + setupVisibilityAPI() { + if (typeof document === 'undefined') + return; + this.boundVisibilityHandler = () => { + if (document.hidden) { + this.pause(); + } + else { + this.resume(); + } + }; + document.addEventListener('visibilitychange', this.boundVisibilityHandler); + } + /** + * Destroy the tick system and clean up all resources. + */ + destroy() { + this.destroyed = true; + this.subscribers.length = 0; + this.onceQueue.length = 0; + if (this.boundVisibilityHandler) { + document.removeEventListener('visibilitychange', this.boundVisibilityHandler); + this.boundVisibilityHandler = null; + } + this.removeAllListeners(); } - // Traditional setup - just create and register providers - const app = GameByte.create(); - // Register core service providers - app.register(new RenderingServiceProvider()); - app.register(new SceneServiceProvider()); - app.register(new PluginServiceProvider()); - app.register(new AssetServiceProvider()); - app.register(new UIServiceProvider()); - app.register(new InputServiceProvider()); - app.register(new PhysicsServiceProvider()); - app.register(new PerformanceServiceProvider()); - app.register(new AudioServiceProvider()); - return app; } - // Utility function to create a GameByte instance with UI system optimized for mobile games - function createMobileGame() { - const app = createGame(); - // Additional mobile optimizations could be applied here - // such as specific renderer configurations, performance settings, etc. - return app; + + /** + * Service provider for the component-level TickSystem. + * Wires TickSystem to the renderer's tick event. + */ + class TickServiceProvider extends AbstractServiceProvider { + register(app) { + app.singleton('tick', () => new TickSystem()); + } + boot(app) { + const container = app.getContainer(); + // Wire tick system to renderer's tick event when renderer is available + if (container.bound('renderer')) { + const renderer = app.make('renderer'); + const tick = app.make('tick'); + renderer.on('tick', (deltaMs) => { + tick.tick(deltaMs); + }); + } + // Cleanup on app destroy + app.on('destroyed', () => { + if (container.bound('tick')) { + const tick = app.make('tick'); + tick.destroy(); + } + }); + } + provides() { + return ['tick']; + } + isDeferred() { + return false; + } } + /** - * Create a GameByte instance pre-configured for merge puzzle games. + * Static Tick facade for component-level render loop participation. * * @example * ```typescript - * const game = createMergeGame(); - * await game.initialize(canvas, '2d'); + * import { Tick } from 'gamebyte-framework'; * - * // Use the Merge facade - * Merge.createGame({ rows: 5, cols: 5 }); - * scene.addChild(Merge.getContainer()); - * Merge.start(); + * // Simple subscription + * Tick.subscribe(({ delta }) => player.move(delta)); * - * game.start(); + * // Priority ordering (lower runs first) + * Tick.subscribe(updatePhysics, -10); + * Tick.subscribe(updateAnimation, 0); + * Tick.subscribe(updateParticles, 10); + * + * // Fixed timestep for physics + * Tick.subscribe(physicsStep, -10, { fixedStep: 1/60 }); + * + * // One-shot + * Tick.runOnce(({ elapsed }) => console.log('Time:', elapsed)); * ``` */ - function createMergeGame() { - const app = createGame(); - // Register merge service provider - app.register(new MergeServiceProvider()); - return app; + class Tick extends Facade { + static getFacadeAccessor() { + return 'tick'; + } + static getSystem() { + return this.resolve(); + } + static subscribe(callback, priority = 0, options) { + return this.getSystem().subscribe(callback, priority, options); + } + static unsubscribe(handle) { + this.getSystem().unsubscribe(handle); + } + static runOnce(callback) { + this.getSystem().runOnce(callback); + } + static getState() { + return this.getSystem().getState(); + } + static getSubscriberCount() { + return this.getSystem().getSubscriberCount(); + } + static pause() { + this.getSystem().pause(); + } + static resume() { + this.getSystem().resume(); + } } - // Default export for convenient importing - const GameByteFramework = { - GameByte, - createGame, - createMobileGame, - RenderingMode: exports.RenderingMode, - ServiceContainer, - Assets, - Gradients, - // Facades for static access - Renderer: null, // Will be set after app initialization - Scenes: null, // Will be set after app initialization - Plugins: null, // Will be set after app initialization - UI: null, // Will be set after app initialization - Animations: null, // Will be set after app initialization - Themes: null, // Will be set after app initialization - Input: null, // Will be set after app initialization - Physics: null, // Will be set after app initialization - Performance: null, // Will be set after app initialization - Audio: null, // Will be set after app initialization - Merge: null // Will be set after app initialization - }; + /** - * Initialize facades with a GameByte application instance. - * This should be called after creating your GameByte app. + * Registry of type-check + disposer pairs. + * First match wins when disposing a resource. + * + * Pre-registers disposers for common Three.js and Pixi.js types + * via duck-typing (no hard dependency on either library). */ - function initializeFacades(app) { - // Import facades (already imported at the top of this file) - Facade.setApplication(app); - // Update default export facades - GameByteFramework.Renderer = Renderer; - GameByteFramework.Scenes = Scenes; - GameByteFramework.Plugins = Plugins; - GameByteFramework.UI = UI; - GameByteFramework.Animations = Animations; - GameByteFramework.Themes = Themes; - GameByteFramework.Input = Input$1; - GameByteFramework.Physics = Physics; - GameByteFramework.Performance = Performance; - GameByteFramework.Audio = Audio; - GameByteFramework.Merge = Merge; + class DisposableRegistry { + constructor() { + this.entries = []; + this.registerDefaults(); + } + /** + * Register a custom disposer. + * Added to the front of the list (higher priority than defaults). + */ + register(typeCheck, disposer) { + this.entries.unshift({ typeCheck, disposer: disposer }); + } + /** + * Dispose a resource using the first matching disposer. + * Returns true if a disposer was found and called. + */ + dispose(resource) { + const len = this.entries.length; + for (let i = 0; i < len; i++) { + const entry = this.entries[i]; + if (entry.typeCheck(resource)) { + entry.disposer(resource); + return true; + } + } + return false; + } + /** + * Register default disposers for common resource types. + * Uses duck-typing to avoid hard dependencies. + */ + registerDefaults() { + // THREE.BufferGeometry (has .dispose() and .attributes) + this.entries.push({ + typeCheck: (obj) => obj != null && + typeof obj.dispose === 'function' && + obj.attributes !== undefined && + obj.index !== undefined, + disposer: (obj) => obj.dispose() + }); + // THREE.Material (has .dispose() and .uniforms or .type contains 'Material') + this.entries.push({ + typeCheck: (obj) => obj != null && + typeof obj.dispose === 'function' && + (obj.isMaterial === true || + (typeof obj.type === 'string' && obj.type.includes('Material'))), + disposer: (obj) => obj.dispose() + }); + // THREE.Texture (has .dispose() and .image) + this.entries.push({ + typeCheck: (obj) => obj != null && + typeof obj.dispose === 'function' && + obj.isTexture === true, + disposer: (obj) => obj.dispose() + }); + // THREE.WebGLRenderTarget + this.entries.push({ + typeCheck: (obj) => obj != null && + typeof obj.dispose === 'function' && + obj.isWebGLRenderTarget === true, + disposer: (obj) => obj.dispose() + }); + // Pixi.js objects (have .destroy()) + this.entries.push({ + typeCheck: (obj) => obj != null && + typeof obj.destroy === 'function' && + typeof obj.dispose !== 'function', + disposer: (obj) => { + try { + obj.destroy({ children: true, texture: true, baseTexture: true }); + } + catch { + // Some Pixi objects don't accept options + obj.destroy(); + } + } + }); + // Generic fallback: any object with .dispose() + this.entries.push({ + typeCheck: (obj) => obj != null && typeof obj.dispose === 'function', + disposer: (obj) => obj.dispose() + }); + // Generic fallback: any object with .destroy() (catch remaining) + this.entries.push({ + typeCheck: (obj) => obj != null && typeof obj.destroy === 'function', + disposer: (obj) => obj.destroy() + }); + } + } + + /** + * A hierarchical resource scope for tracking disposable resources. + * + * Features: + * - Reference counting for shared resources (textures, materials) + * - Child scopes dispose before parent (cascading cleanup) + * - Deferred disposal via microtask to avoid mid-frame GPU stalls + */ + class ResourceScope { + constructor(id, registry, parent = null) { + this.tracked = new Set(); + this.refCounts = new Map(); + this.children = []; + this.disposed = false; + this.childIdCounter = 0; + this.id = id; + this.registry = registry; + this.parent = parent; + } + /** + * Track a resource in this scope. Returns the same resource for chaining. + * Increments reference count for shared resources. + */ + track(resource) { + if (this.disposed) { + console.warn(`ResourceScope '${this.id}' is disposed, cannot track new resources`); + return resource; + } + const ref = this.refCounts.get(resource) ?? 0; + this.refCounts.set(resource, ref + 1); + this.tracked.add(resource); + return resource; + } + /** + * Release a resource. Decrements ref count; disposes at zero. + * Returns true if the resource was actually disposed. + */ + release(resource) { + if (!this.tracked.has(resource)) + return false; + const ref = (this.refCounts.get(resource) ?? 1) - 1; + if (ref <= 0) { + this.tracked.delete(resource); + this.refCounts.delete(resource); + this.registry.dispose(resource); + return true; + } + this.refCounts.set(resource, ref); + return false; + } + /** + * Create a child scope. Child disposes before parent. + */ + createChild(id) { + const childId = id ?? `${this.id}:child-${this.childIdCounter++}`; + const child = new ResourceScope(childId, this.registry, this); + this.children.push(child); + return child; + } + /** + * Dispose all tracked resources in this scope and child scopes. + * Children are disposed first (leaf-to-root order). + */ + dispose() { + if (this.disposed) + return; + this.disposed = true; + // Dispose children first (leaf-to-root) + for (let i = this.children.length - 1; i >= 0; i--) { + this.children[i].dispose(); + } + this.children.length = 0; + // Dispose all tracked resources + for (const resource of this.tracked) { + this.registry.dispose(resource); + } + this.tracked.clear(); + this.refCounts.clear(); + } + /** + * Get count of tracked resources in this scope (not including children). + */ + getTrackedCount() { + return this.tracked.size; + } + /** + * Check if this scope has been disposed. + */ + isDisposed() { + return this.disposed; + } + } + + /** + * Main resource lifecycle manager. + * + * Manages named scopes and provides centralized resource cleanup. + * Integrates with scene lifecycle for automatic scope disposal. + * + * @example + * ```typescript + * const tracker = new ResourceTracker(); + * const scope = tracker.createScope('level-1'); + * const geo = scope.track(new THREE.BoxGeometry(1, 1, 1)); + * const mat = scope.track(new THREE.MeshStandardMaterial()); + * // When done: + * tracker.disposeScope('level-1'); // both auto-disposed + * ``` + */ + class ResourceTracker extends EventEmitter { + constructor() { + super(); + this.scopes = new Map(); + this.registry = new DisposableRegistry(); + } + /** + * Create a named resource scope. + * If a scope with this ID already exists, returns the existing one. + */ + createScope(id) { + const existing = this.scopes.get(id); + if (existing && !existing.isDisposed()) { + return existing; + } + const scope = new ResourceScope(id, this.registry); + this.scopes.set(id, scope); + this.emit('scope:created', id); + return scope; + } + /** + * Get an existing scope by ID. + */ + getScope(id) { + const scope = this.scopes.get(id); + if (scope && scope.isDisposed()) { + this.scopes.delete(id); + return undefined; + } + return scope; + } + /** + * Dispose a named scope and all its tracked resources. + */ + disposeScope(id) { + const scope = this.scopes.get(id); + if (scope) { + scope.dispose(); + this.scopes.delete(id); + this.emit('scope:disposed', id); + } + } + /** + * Register a custom disposer for a resource type. + * Custom disposers take priority over defaults. + */ + registerDisposer(typeCheck, disposer) { + this.registry.register(typeCheck, disposer); + } + /** + * Get total count of tracked resources across all scopes. + */ + getTotalTrackedCount() { + let total = 0; + for (const scope of this.scopes.values()) { + if (!scope.isDisposed()) { + total += scope.getTrackedCount(); + } + } + return total; + } + /** + * Dispose all scopes and their resources. + */ + disposeAll() { + for (const [id, scope] of this.scopes) { + scope.dispose(); + this.emit('scope:disposed', id); + } + this.scopes.clear(); + this.emit('all:disposed'); + } + /** + * Destroy the tracker entirely. + */ + destroy() { + this.disposeAll(); + this.removeAllListeners(); + } + } + + /** + * Service provider for auto-dispose / resource lifecycle management. + * Integrates ResourceTracker with scene lifecycle for automatic cleanup. + */ + class ResourceServiceProvider extends AbstractServiceProvider { + register(app) { + app.singleton('resources', () => new ResourceTracker()); + } + boot(app) { + const container = app.getContainer(); + // Auto-create scene scopes and dispose on scene deactivation + app.on('scene:activated', (sceneName) => { + if (container.bound('resources')) { + const tracker = app.make('resources'); + tracker.createScope(`scene:${sceneName}`); + } + }); + app.on('scene:deactivated', (sceneName) => { + if (container.bound('resources')) { + const tracker = app.make('resources'); + tracker.disposeScope(`scene:${sceneName}`); + } + }); + // Dispose all on app destroy + app.on('destroyed', () => { + if (container.bound('resources')) { + const tracker = app.make('resources'); + tracker.destroy(); + } + }); + } + provides() { + return ['resources']; + } + isDeferred() { + return false; + } + } + + /** + * Static Resources facade for resource lifecycle management. + * + * @example + * ```typescript + * import { Resources } from 'gamebyte-framework'; + * + * const scope = Resources.createScope('level-1'); + * const geo = scope.track(new THREE.BoxGeometry(1, 1, 1)); + * const mat = scope.track(new THREE.MeshStandardMaterial()); + * // When done: + * Resources.disposeScope('level-1'); // both auto-disposed + * ``` + */ + class Resources extends Facade { + static getFacadeAccessor() { + return 'resources'; + } + static getTracker() { + return this.resolve(); + } + static createScope(id) { + return this.getTracker().createScope(id); + } + static getScope(id) { + return this.getTracker().getScope(id); + } + static disposeScope(id) { + this.getTracker().disposeScope(id); + } + static registerDisposer(typeCheck, disposer) { + this.getTracker().registerDisposer(typeCheck, disposer); + } + static getTotalTrackedCount() { + return this.getTracker().getTotalTrackedCount(); + } + static disposeAll() { + this.getTracker().disposeAll(); + } + } + + /** + * Procedural game sound library. + * All sounds generated via oscillators - no audio files needed. + */ + class GameSoundPresets { + constructor() { + this.ctx = null; + this.presets = new Map(); + this.activeSounds = 0; + const isMobile = typeof navigator !== 'undefined' && /Mobi|Android/i.test(navigator.userAgent); + this.maxConcurrent = isMobile ? 4 : 8; + this.registerBuiltinPresets(); + } + /** + * Play a procedural sound preset. + */ + play(type, config) { + if (this.activeSounds >= this.maxConcurrent) + return; + const generator = this.presets.get(type); + if (!generator) { + console.warn(`GameSoundPresets: unknown preset '${type}'`); + return; + } + // Lazy AudioContext init + if (!this.ctx) { + this.ctx = new AudioContext(); + } + // Resume if suspended (autoplay policy) + if (this.ctx.state === 'suspended') { + this.ctx.resume(); + } + const resolved = { + volume: config?.volume ?? 0.3, + pitch: config?.pitch ?? 1.0, + duration: config?.duration ?? 0, + variation: config?.variation ?? 0 + }; + // Apply random variation to pitch + if (resolved.variation > 0) { + const range = resolved.variation * 0.4; // max ±20% pitch shift at variation=1 + resolved.pitch *= 1 + (Math.random() * range * 2 - range); + } + this.activeSounds++; + try { + generator(this.ctx, this.ctx.destination, resolved); + } + catch { + // Silently fail if audio context is in bad state + } + // Auto-decrement after max reasonable duration + const timeout = Math.max((resolved.duration || 2) * 1000, 100); + setTimeout(() => { this.activeSounds = Math.max(0, this.activeSounds - 1); }, timeout); + } + /** + * Register a custom sound preset. + */ + register(name, generator) { + this.presets.set(name, generator); + } + /** + * Get list of available preset names. + */ + getPresets() { + return Array.from(this.presets.keys()); + } + /** + * Destroy and clean up. + */ + destroy() { + if (this.ctx) { + this.ctx.close(); + this.ctx = null; + } + this.presets.clear(); + this.activeSounds = 0; + } + // ─── Built-in Presets ────────────────────────── + registerBuiltinPresets() { + this.presets.set('click', (ctx, dest, cfg) => { + const t = ctx.currentTime + 0.01; + const dur = cfg.duration || 0.08; + const osc = ctx.createOscillator(); + const gain = ctx.createGain(); + osc.type = 'sine'; + osc.frequency.setValueAtTime(800 * cfg.pitch, t); + osc.frequency.exponentialRampToValueAtTime(400 * cfg.pitch, t + dur); + gain.gain.setValueAtTime(cfg.volume, t); + gain.gain.exponentialRampToValueAtTime(0.001, t + dur); + osc.connect(gain).connect(dest); + osc.start(t); + osc.stop(t + dur); + }); + this.presets.set('hit', (ctx, dest, cfg) => { + const t = ctx.currentTime + 0.01; + const dur = cfg.duration || 0.15; + // Impact oscillator + const osc = ctx.createOscillator(); + const gain = ctx.createGain(); + osc.type = 'sawtooth'; + osc.frequency.setValueAtTime(200 * cfg.pitch, t); + osc.frequency.exponentialRampToValueAtTime(60 * cfg.pitch, t + dur); + gain.gain.setValueAtTime(cfg.volume, t); + gain.gain.exponentialRampToValueAtTime(0.001, t + dur); + osc.connect(gain).connect(dest); + osc.start(t); + osc.stop(t + dur); + // Noise burst + this.addNoiseBurst(ctx, dest, t, dur * 0.5, cfg.volume * 0.5); + }); + this.presets.set('pickup', (ctx, dest, cfg) => { + const t = ctx.currentTime + 0.01; + const dur = cfg.duration || 0.2; + const osc = ctx.createOscillator(); + const gain = ctx.createGain(); + osc.type = 'sine'; + osc.frequency.setValueAtTime(400 * cfg.pitch, t); + osc.frequency.exponentialRampToValueAtTime(1200 * cfg.pitch, t + dur * 0.6); + osc.frequency.exponentialRampToValueAtTime(800 * cfg.pitch, t + dur); + gain.gain.setValueAtTime(cfg.volume, t); + gain.gain.linearRampToValueAtTime(cfg.volume * 0.8, t + dur * 0.3); + gain.gain.exponentialRampToValueAtTime(0.001, t + dur); + osc.connect(gain).connect(dest); + osc.start(t); + osc.stop(t + dur); + }); + this.presets.set('coin', (ctx, dest, cfg) => { + const t = ctx.currentTime + 0.01; + const dur = cfg.duration || 0.25; + // Two-tone ding + for (let i = 0; i < 2; i++) { + const osc = ctx.createOscillator(); + const gain = ctx.createGain(); + osc.type = 'sine'; + const freq = (i === 0 ? 988 : 1319) * cfg.pitch; // B5, E6 + const offset = i * 0.08; + osc.frequency.setValueAtTime(freq, t + offset); + gain.gain.setValueAtTime(cfg.volume, t + offset); + gain.gain.exponentialRampToValueAtTime(0.001, t + offset + dur); + osc.connect(gain).connect(dest); + osc.start(t + offset); + osc.stop(t + offset + dur); + } + }); + this.presets.set('jump', (ctx, dest, cfg) => { + const t = ctx.currentTime + 0.01; + const dur = cfg.duration || 0.15; + const osc = ctx.createOscillator(); + const gain = ctx.createGain(); + osc.type = 'square'; + osc.frequency.setValueAtTime(150 * cfg.pitch, t); + osc.frequency.exponentialRampToValueAtTime(600 * cfg.pitch, t + dur); + gain.gain.setValueAtTime(cfg.volume * 0.6, t); + gain.gain.exponentialRampToValueAtTime(0.001, t + dur); + osc.connect(gain).connect(dest); + osc.start(t); + osc.stop(t + dur); + }); + this.presets.set('land', (ctx, dest, cfg) => { + const t = ctx.currentTime + 0.01; + const dur = cfg.duration || 0.12; + const osc = ctx.createOscillator(); + const gain = ctx.createGain(); + osc.type = 'triangle'; + osc.frequency.setValueAtTime(300 * cfg.pitch, t); + osc.frequency.exponentialRampToValueAtTime(80 * cfg.pitch, t + dur); + gain.gain.setValueAtTime(cfg.volume * 0.7, t); + gain.gain.exponentialRampToValueAtTime(0.001, t + dur); + osc.connect(gain).connect(dest); + osc.start(t); + osc.stop(t + dur); + this.addNoiseBurst(ctx, dest, t, dur * 0.3, cfg.volume * 0.3); + }); + this.presets.set('explosion', (ctx, dest, cfg) => { + const t = ctx.currentTime + 0.01; + const dur = cfg.duration || 0.6; + // Low rumble + const osc = ctx.createOscillator(); + const gain = ctx.createGain(); + osc.type = 'sawtooth'; + osc.frequency.setValueAtTime(80 * cfg.pitch, t); + osc.frequency.exponentialRampToValueAtTime(20 * cfg.pitch, t + dur); + gain.gain.setValueAtTime(cfg.volume, t); + gain.gain.exponentialRampToValueAtTime(0.001, t + dur); + osc.connect(gain).connect(dest); + osc.start(t); + osc.stop(t + dur); + // Noise component + this.addNoiseBurst(ctx, dest, t, dur * 0.8, cfg.volume * 0.8); + }); + this.presets.set('laser', (ctx, dest, cfg) => { + const t = ctx.currentTime + 0.01; + const dur = cfg.duration || 0.2; + const osc = ctx.createOscillator(); + const gain = ctx.createGain(); + osc.type = 'sawtooth'; + osc.frequency.setValueAtTime(1200 * cfg.pitch, t); + osc.frequency.exponentialRampToValueAtTime(200 * cfg.pitch, t + dur); + gain.gain.setValueAtTime(cfg.volume * 0.5, t); + gain.gain.exponentialRampToValueAtTime(0.001, t + dur); + osc.connect(gain).connect(dest); + osc.start(t); + osc.stop(t + dur); + }); + this.presets.set('powerUp', (ctx, dest, cfg) => { + const t = ctx.currentTime + 0.01; + const dur = cfg.duration || 0.5; + // Rising arpeggio + const notes = [262, 330, 392, 523]; // C4, E4, G4, C5 + const noteDur = dur / notes.length; + for (let i = 0; i < notes.length; i++) { + const osc = ctx.createOscillator(); + const gain = ctx.createGain(); + osc.type = 'sine'; + osc.frequency.setValueAtTime(notes[i] * cfg.pitch, t + i * noteDur); + gain.gain.setValueAtTime(cfg.volume, t + i * noteDur); + gain.gain.exponentialRampToValueAtTime(0.001, t + (i + 1) * noteDur); + osc.connect(gain).connect(dest); + osc.start(t + i * noteDur); + osc.stop(t + (i + 1) * noteDur); + } + }); + this.presets.set('death', (ctx, dest, cfg) => { + const t = ctx.currentTime + 0.01; + const dur = cfg.duration || 0.5; + const osc = ctx.createOscillator(); + const gain = ctx.createGain(); + osc.type = 'square'; + osc.frequency.setValueAtTime(400 * cfg.pitch, t); + osc.frequency.exponentialRampToValueAtTime(50 * cfg.pitch, t + dur); + gain.gain.setValueAtTime(cfg.volume * 0.6, t); + gain.gain.exponentialRampToValueAtTime(0.001, t + dur); + osc.connect(gain).connect(dest); + osc.start(t); + osc.stop(t + dur); + }); + this.presets.set('error', (ctx, dest, cfg) => { + const t = ctx.currentTime + 0.01; + const dur = cfg.duration || 0.3; + // Two descending tones + for (let i = 0; i < 2; i++) { + const osc = ctx.createOscillator(); + const gain = ctx.createGain(); + osc.type = 'square'; + const freq = (i === 0 ? 400 : 300) * cfg.pitch; + const offset = i * 0.12; + osc.frequency.setValueAtTime(freq, t + offset); + gain.gain.setValueAtTime(cfg.volume * 0.4, t + offset); + gain.gain.exponentialRampToValueAtTime(0.001, t + offset + dur * 0.5); + osc.connect(gain).connect(dest); + osc.start(t + offset); + osc.stop(t + offset + dur * 0.5); + } + }); + this.presets.set('success', (ctx, dest, cfg) => { + const t = ctx.currentTime + 0.01; + const dur = cfg.duration || 0.4; + // Major chord arpeggio + const notes = [523, 659, 784]; // C5, E5, G5 + for (let i = 0; i < notes.length; i++) { + const osc = ctx.createOscillator(); + const gain = ctx.createGain(); + osc.type = 'sine'; + osc.frequency.setValueAtTime(notes[i] * cfg.pitch, t + i * 0.06); + gain.gain.setValueAtTime(cfg.volume, t + i * 0.06); + gain.gain.exponentialRampToValueAtTime(0.001, t + dur); + osc.connect(gain).connect(dest); + osc.start(t + i * 0.06); + osc.stop(t + dur); + } + }); + this.presets.set('whoosh', (ctx, dest, cfg) => { + const t = ctx.currentTime + 0.01; + const dur = cfg.duration || 0.25; + // Filtered noise sweep + const bufferSize = ctx.sampleRate * dur; + const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate); + const data = buffer.getChannelData(0); + for (let i = 0; i < bufferSize; i++) { + data[i] = Math.random() * 2 - 1; + } + const source = ctx.createBufferSource(); + source.buffer = buffer; + const filter = ctx.createBiquadFilter(); + filter.type = 'bandpass'; + filter.frequency.setValueAtTime(200 * cfg.pitch, t); + filter.frequency.exponentialRampToValueAtTime(4000 * cfg.pitch, t + dur * 0.3); + filter.frequency.exponentialRampToValueAtTime(200 * cfg.pitch, t + dur); + filter.Q.value = 2; + const gain = ctx.createGain(); + gain.gain.setValueAtTime(0.001, t); + gain.gain.linearRampToValueAtTime(cfg.volume * 0.5, t + dur * 0.2); + gain.gain.exponentialRampToValueAtTime(0.001, t + dur); + source.connect(filter).connect(gain).connect(dest); + source.start(t); + source.stop(t + dur); + }); + this.presets.set('thrust', (ctx, dest, cfg) => { + const t = ctx.currentTime + 0.01; + const dur = cfg.duration || 0.4; + // Low noise with modulation + const osc = ctx.createOscillator(); + const gain = ctx.createGain(); + osc.type = 'sawtooth'; + osc.frequency.setValueAtTime(60 * cfg.pitch, t); + gain.gain.setValueAtTime(cfg.volume * 0.3, t); + gain.gain.exponentialRampToValueAtTime(0.001, t + dur); + osc.connect(gain).connect(dest); + osc.start(t); + osc.stop(t + dur); + this.addNoiseBurst(ctx, dest, t, dur, cfg.volume * 0.3); + }); + this.presets.set('nearMiss', (ctx, dest, cfg) => { + const t = ctx.currentTime + 0.01; + const dur = cfg.duration || 0.3; + // Quick pitch sweep (Doppler-like) + const osc = ctx.createOscillator(); + const gain = ctx.createGain(); + osc.type = 'sine'; + osc.frequency.setValueAtTime(800 * cfg.pitch, t); + osc.frequency.exponentialRampToValueAtTime(200 * cfg.pitch, t + dur); + gain.gain.setValueAtTime(0.001, t); + gain.gain.linearRampToValueAtTime(cfg.volume * 0.6, t + dur * 0.15); + gain.gain.exponentialRampToValueAtTime(0.001, t + dur); + osc.connect(gain).connect(dest); + osc.start(t); + osc.stop(t + dur); + }); + } + /** + * Helper: add a white noise burst. + */ + addNoiseBurst(ctx, dest, startTime, duration, volume) { + const bufferSize = Math.max(1, Math.floor(ctx.sampleRate * duration)); + const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate); + const data = buffer.getChannelData(0); + for (let i = 0; i < bufferSize; i++) { + data[i] = Math.random() * 2 - 1; + } + const source = ctx.createBufferSource(); + source.buffer = buffer; + const gain = ctx.createGain(); + gain.gain.setValueAtTime(volume, startTime); + gain.gain.exponentialRampToValueAtTime(0.001, startTime + duration); + source.connect(gain).connect(dest); + source.start(startTime); + source.stop(startTime + duration); + } + } + + /** Default quality tiers ordered from lowest to highest */ + const DEFAULT_TIERS = [ + { + name: 'ultra-low', + dpr: 0.5, + shadowMapSize: 0, + shadowsEnabled: false, + postProcessing: false, + drawDistance: 50, + particleMultiplier: 0.1, + textureResolution: 'low', + antialiasing: false, + maxLights: 2 + }, + { + name: 'low', + dpr: 0.75, + shadowMapSize: 512, + shadowsEnabled: true, + postProcessing: false, + drawDistance: 100, + particleMultiplier: 0.3, + textureResolution: 'low', + antialiasing: false, + maxLights: 4 + }, + { + name: 'medium', + dpr: 1.0, + shadowMapSize: 1024, + shadowsEnabled: true, + postProcessing: true, + drawDistance: 200, + particleMultiplier: 0.6, + textureResolution: 'medium', + antialiasing: true, + maxLights: 8 + }, + { + name: 'high', + dpr: 1.0, + shadowMapSize: 2048, + shadowsEnabled: true, + postProcessing: true, + drawDistance: 500, + particleMultiplier: 1.0, + textureResolution: 'high', + antialiasing: true, + maxLights: 16 + }, + { + name: 'ultra', + dpr: typeof window !== 'undefined' ? Math.min(window.devicePixelRatio, 2) : 2, + shadowMapSize: 4096, + shadowsEnabled: true, + postProcessing: true, + drawDistance: 1000, + particleMultiplier: 1.5, + textureResolution: 'high', + antialiasing: true, + maxLights: 32 + } + ]; + /** + * Manages quality tiers and tier transitions. + */ + class QualityTierManager { + constructor() { + this.minTierIndex = 0; + this.tiers = [...DEFAULT_TIERS]; + this.tierIndex = 2; // Start at 'medium' + this.maxTierIndex = this.tiers.length - 1; + } + /** + * Register a custom tier. Inserted in order by DPR * drawDistance (quality proxy). + */ + registerTier(tier) { + this.tiers.push(tier); + // Re-sort by quality proxy + this.tiers.sort((a, b) => (a.dpr * a.drawDistance) - (b.dpr * b.drawDistance)); + this.maxTierIndex = this.tiers.length - 1; + // Re-find current tier index by name + const idx = this.tiers.findIndex(t => t.name === this.getCurrentTier().name); + if (idx >= 0) + this.tierIndex = idx; + } + getCurrentTier() { + return this.tiers[this.tierIndex]; + } + getTierByName(name) { + return this.tiers.find(t => t.name === name); + } + setTierByName(name) { + const idx = this.tiers.findIndex(t => t.name === name); + if (idx >= 0 && idx >= this.minTierIndex && idx <= this.maxTierIndex) { + this.tierIndex = idx; + return this.tiers[idx]; + } + return null; + } + /** + * Move to the next lower tier. Returns new tier or null if already at min. + */ + downgrade() { + if (this.tierIndex <= this.minTierIndex) + return null; + this.tierIndex--; + return this.tiers[this.tierIndex]; + } + /** + * Move to the next higher tier. Returns new tier or null if already at max. + */ + upgrade() { + if (this.tierIndex >= this.maxTierIndex) + return null; + this.tierIndex++; + return this.tiers[this.tierIndex]; + } + canDowngrade() { + return this.tierIndex > this.minTierIndex; + } + canUpgrade() { + return this.tierIndex < this.maxTierIndex; + } + setMinTier(name) { + const idx = this.tiers.findIndex(t => t.name === name); + if (idx >= 0) + this.minTierIndex = idx; + } + setMaxTier(name) { + const idx = this.tiers.findIndex(t => t.name === name); + if (idx >= 0) + this.maxTierIndex = idx; + } + getTierNames() { + return this.tiers.map(t => t.name); + } } + const DEFAULT_CONFIG = { + targetFps: 55, + downgradeThreshold: 45, + upgradeThreshold: 58, + stabilityWindow: 2, + upgradeBackoffMultiplier: 2, + maxUpgradeBackoff: 16, + minTier: '', + maxTier: '', + thermalProtection: true + }; + /** + * Adaptive performance system with automatic quality tier adjustment. + * + * Features: + * - Hysteresis: separate up/down thresholds prevent oscillation + * - EMA FPS tracking (reads from TickSystem) + * - Exponential backoff for upgrades + * - Thermal throttling detection (sustained FPS degradation) + * - Battery API integration + * + * The dead zone between downgradeThreshold (45) and upgradeThreshold (58) + * means: if FPS is between 45-58, no action is taken. This prevents + * rapid tier flipping when FPS fluctuates near a single threshold. + */ + class PerformanceAdvisor extends EventEmitter { + constructor() { + super(); + this._active = false; + // FPS tracking (uses EMA from TickSystem, or internal tracking) + this.smoothFps = 60; + this.fpsAlpha = 0.05; + // Stability tracking + this.stableStartTime = 0; + this.lastDirection = 'none'; + this.lastUpgradeTime = 0; + this.consecutiveUpgrades = 0; + // Thermal detection (timestamped samples) + this.fpsHistory = []; + this.thermalCheckInterval = 30000; // 30s + this.lastThermalCheck = 0; + this.thermalThrottled = false; + this.config = { ...DEFAULT_CONFIG }; + this.tierManager = new QualityTierManager(); + this.currentUpgradeBackoff = this.config.stabilityWindow * 1000; + } + /** + * Enable adaptive quality management. + */ + enable(config) { + this.config = { ...DEFAULT_CONFIG, ...config }; + this.currentUpgradeBackoff = this.config.stabilityWindow * 1000; + if (this.config.minTier) + this.tierManager.setMinTier(this.config.minTier); + if (this.config.maxTier) + this.tierManager.setMaxTier(this.config.maxTier); + this._active = true; + this.stableStartTime = performance.now(); + this.emit('enabled'); + } + /** + * Disable adaptive quality management. + */ + disable() { + this._active = false; + this.emit('disabled'); + } + get active() { + return this._active; + } + /** + * Manually trigger a quality regression (e.g., during heavy interaction). + * Immediately drops one tier without waiting for stability window. + */ + regress() { + if (!this._active) + return; + const newTier = this.tierManager.downgrade(); + if (newTier) { + this.emitQualityChange(newTier, 'down'); + this.stableStartTime = performance.now(); + } + } + /** + * Manually set a specific quality tier. + */ + setTier(tierName) { + const tier = this.tierManager.setTierByName(tierName); + if (tier) { + this.emitQualityChange(tier, 'down'); + } + } + /** + * Get current quality tier settings. + */ + getCurrentTier() { + return this.tierManager.getCurrentTier(); + } + /** + * Register a custom quality tier. + */ + registerTier(name, tier) { + this.tierManager.registerTier({ ...tier, name }); + } + /** + * Register callback for quality changes. + */ + onQualityChange(callback) { + this.on('quality:changed', callback); + } + /** + * Called each frame with current FPS. + * Typically wired to TickSystem via service provider. + */ + sample(fps) { + if (!this._active) + return; + // EMA smoothing + this.smoothFps += this.fpsAlpha * (fps - this.smoothFps); + const now = performance.now(); + // Thermal detection: check for sustained FPS degradation + if (this.config.thermalProtection && now - this.lastThermalCheck > this.thermalCheckInterval) { + this.checkThermalThrottling(); + this.lastThermalCheck = now; + } + // Hysteresis decision + if (this.smoothFps < this.config.downgradeThreshold) { + // Below downgrade threshold + if (this.lastDirection !== 'down') { + this.lastDirection = 'down'; + this.stableStartTime = now; + } + const stableDuration = now - this.stableStartTime; + if (stableDuration >= this.config.stabilityWindow * 1000) { + // Downgrade immediately when stability window reached + const newTier = this.tierManager.downgrade(); + if (newTier) { + this.emitQualityChange(newTier, 'down'); + this.consecutiveUpgrades = 0; // Reset upgrade backoff + this.currentUpgradeBackoff = this.config.stabilityWindow * 1000; + } + this.stableStartTime = now; + this.lastDirection = 'none'; + } + } + else if (this.smoothFps > this.config.upgradeThreshold) { + // Above upgrade threshold + if (this.lastDirection !== 'up') { + this.lastDirection = 'up'; + this.stableStartTime = now; + } + // Upgrade with exponential backoff + const stableDuration = now - this.stableStartTime; + const sinceLastUpgrade = now - this.lastUpgradeTime; + if (stableDuration >= this.currentUpgradeBackoff && + sinceLastUpgrade >= this.currentUpgradeBackoff && + !this.thermalThrottled) { + const newTier = this.tierManager.upgrade(); + if (newTier) { + this.emitQualityChange(newTier, 'up'); + this.lastUpgradeTime = now; + this.consecutiveUpgrades++; + // Exponential backoff: doubles each consecutive upgrade + this.currentUpgradeBackoff = Math.min(this.currentUpgradeBackoff * this.config.upgradeBackoffMultiplier, this.config.maxUpgradeBackoff * 1000); + } + this.stableStartTime = now; + this.lastDirection = 'none'; + } + } + else { + // In dead zone - reset direction + this.lastDirection = 'none'; + this.stableStartTime = now; + } + // Track FPS for thermal detection (timestamped for accurate time-window comparison) + if (this.config.thermalProtection) { + this.fpsHistory.push({ fps: this.smoothFps, time: now }); + // Prune samples older than 2x the thermal check interval + const maxAge = this.thermalCheckInterval * 2; + while (this.fpsHistory.length > 0 && now - this.fpsHistory[0].time > maxAge) { + this.fpsHistory.shift(); + } + } + } + /** + * Detect thermal throttling: sustained FPS degradation over 30+ seconds. + * Compares average FPS from the older half vs the newer half of the time window. + */ + checkThermalThrottling() { + if (this.fpsHistory.length < 10) + return; + const now = this.fpsHistory[this.fpsHistory.length - 1].time; + const windowStart = this.fpsHistory[0].time; + const midTime = windowStart + (now - windowStart) / 2; + let firstSum = 0, firstCount = 0; + let secondSum = 0, secondCount = 0; + for (let i = 0; i < this.fpsHistory.length; i++) { + const entry = this.fpsHistory[i]; + if (entry.time < midTime) { + firstSum += entry.fps; + firstCount++; + } + else { + secondSum += entry.fps; + secondCount++; + } + } + if (firstCount === 0 || secondCount === 0) + return; + const avgFirst = firstSum / firstCount; + const avgSecond = secondSum / secondCount; + // If FPS dropped more than 15% from first half to second half = thermal + if (avgSecond < avgFirst * 0.85) { + if (!this.thermalThrottled) { + this.thermalThrottled = true; + this.emit('thermal:throttled'); + // Aggressive downgrade + this.regress(); + } + } + else if (avgSecond > avgFirst * 0.95) { + // Recovered + this.thermalThrottled = false; + } + } + emitQualityChange(tier, direction) { + this.emit('quality:changed', tier, direction); + } + /** + * Get the quality tier manager for direct access. + */ + getTierManager() { + return this.tierManager; + } + /** + * Destroy and clean up. + */ + destroy() { + this._active = false; + this.fpsHistory.length = 0; + this.removeAllListeners(); + } + } + + /** + * Built-in effect priorities (lower = processed first in the pipeline). + */ + const EFFECT_PRIORITIES = { + ssao: 5, + bloom: 10, + dof: 20, + chromaticAberration: 40, + vignette: 50, + toneMapping: 90, + fxaa: 100 + }; + /** + * Wrapped effect entry in the pipeline. + */ + class PostProcessingEffect { + constructor(name, nativeEffect, params) { + this._enabled = true; + this.name = name; + this.priority = EFFECT_PRIORITIES[name] ?? 50; + this.nativeEffect = nativeEffect; + this.params = params; + } + get enabled() { return this._enabled; } + set enabled(v) { + this._enabled = v; + // pmndrs effects have a .disabled property (inverse of enabled) + if (this.nativeEffect && 'disabled' in this.nativeEffect) { + this.nativeEffect.disabled = !v; + } + } + getNativeEffect() { + return this.nativeEffect; + } + setParams(params) { + Object.assign(this.params, params); + // Apply params to native effect + if (this.nativeEffect) { + for (const [key, value] of Object.entries(params)) { + if (key in this.nativeEffect) { + this.nativeEffect[key] = value; + } + } + } + } + dispose() { + if (this.nativeEffect?.dispose) { + this.nativeEffect.dispose(); + } + this.nativeEffect = null; + } + } + /** + * Post-processing pipeline using pmndrs/postprocessing for effect merging. + * + * Architecture: pmndrs/postprocessing merges compatible effects into single + * shader passes. 5 effects might compile into 1-2 passes instead of 5. + * This is ~80% fewer render operations compared to Three.js EffectComposer. + * + * Graceful degradation: if pmndrs/postprocessing is not installed, + * effects are registered but not applied, and render() is a no-op. + * + * Features: + * - Automatic effect merging via pmndrs EffectComposer + * - Built-in factories for common effects (bloom, vignette, fxaa, etc.) + * - Priority-ordered pipeline rebuilding + * - Lazy shader compilation (only when enabled) + * - PerformanceAdvisor integration: auto-disable on low quality tiers + */ + class PostProcessingPipeline extends EventEmitter { + constructor() { + super(...arguments); + this.effects = new Map(); + this.customFactories = new Map(); + this._enabled = true; + this.needsRebuild = false; + this.rebuilding = false; + // pmndrs/postprocessing instances (lazy loaded) + this.composer = null; + this.renderer = null; + this.scene = null; + this.camera = null; + this.pmndrs = null; // Module reference + } + get enabled() { return this._enabled; } + set enabled(v) { + this._enabled = v; + this.emit('enabled:changed', v); + } + /** + * Set renderer, scene, camera for the pipeline. + */ + setRenderer(renderer, scene, camera) { + this.renderer = renderer; + this.scene = scene; + this.camera = camera; + this.needsRebuild = true; + } + /** + * Add an effect by name. + */ + add(name, params) { + // Remove existing if re-adding + if (this.effects.has(name)) { + this.remove(name); + } + // Create the native effect (will be null if pmndrs not available) + const nativeEffect = this.createNativeEffect(name, params ?? {}); + const effect = new PostProcessingEffect(name, nativeEffect, params ?? {}); + this.effects.set(name, effect); + this.needsRebuild = true; + this.emit('effect:added', name); + return effect; + } + /** + * Remove an effect by name. + */ + remove(name) { + const effect = this.effects.get(name); + if (effect) { + effect.dispose(); + this.effects.delete(name); + this.needsRebuild = true; + this.emit('effect:removed', name); + } + } + /** + * Get an effect by name. + */ + get(name) { + return this.effects.get(name); + } + /** + * Get all active effects sorted by priority. + */ + getEffects() { + return Array.from(this.effects.values()) + .filter(e => e.enabled) + .sort((a, b) => a.priority - b.priority); + } + /** + * Register a custom effect factory. + */ + registerEffect(name, factory) { + this.customFactories.set(name, factory); + } + /** + * Render one frame through the post-processing pipeline. + * Falls back to standard renderer.render() if pipeline is disabled or unavailable. + */ + render() { + if (!this.renderer || !this.scene || !this.camera) + return; + if (!this._enabled || this.effects.size === 0) { + // Standard render + this.renderer.render(this.scene, this.camera); + return; + } + // Rebuild composer if needed (async, guarded against concurrent rebuilds) + if (this.needsRebuild && !this.rebuilding) { + this.rebuildComposer(); + } + if (this.composer) { + this.composer.render(); + } + else { + // Fallback: standard render + this.renderer.render(this.scene, this.camera); + } + } + /** + * Dispose all effects and the composer. + */ + dispose() { + for (const effect of this.effects.values()) { + effect.dispose(); + } + this.effects.clear(); + if (this.composer?.dispose) { + this.composer.dispose(); + } + this.composer = null; + this.renderer = null; + this.scene = null; + this.camera = null; + this.customFactories.clear(); + this.removeAllListeners(); + } + // ─── Private Methods ─────────────────────────── + async loadPmndrs() { + if (this.pmndrs) + return this.pmndrs; + try { + this.pmndrs = await import('postprocessing'); + return this.pmndrs; + } + catch { + console.warn('PostProcessingPipeline: pmndrs/postprocessing not installed. Effects will not be applied.'); + return null; + } + } + createNativeEffect(name, params) { + // Check custom factories first + const customFactory = this.customFactories.get(name); + if (customFactory) { + return customFactory(params); + } + // Built-in effect creation is deferred to rebuildComposer + // to handle async import of pmndrs/postprocessing + return { __deferred: true, name, params }; + } + async rebuildComposer() { + this.rebuilding = true; + if (!this.renderer || !this.scene || !this.camera) { + this.rebuilding = false; + return; + } + const pp = await this.loadPmndrs(); + if (!pp) { + this.rebuilding = false; + return; + } + // Dispose old composer + if (this.composer?.dispose) { + this.composer.dispose(); + } + // Create new composer + this.composer = new pp.EffectComposer(this.renderer); + // Add render pass + const renderPass = new pp.RenderPass(this.scene, this.camera); + this.composer.addPass(renderPass); + // Collect native effects, creating deferred ones now + const nativeEffects = []; + const sortedEffects = this.getEffects(); + for (const effect of sortedEffects) { + let native = effect.getNativeEffect(); + // Resolve deferred effects + if (native?.__deferred) { + native = this.createBuiltinEffect(pp, native.name, native.params); + // Replace the deferred placeholder + if (native) { + effect.nativeEffect = native; + } + } + if (native && !native.__deferred) { + nativeEffects.push(native); + } + } + // Create merged EffectPass (pmndrs groups compatible effects automatically) + if (nativeEffects.length > 0) { + const effectPass = new pp.EffectPass(this.camera, ...nativeEffects); + this.composer.addPass(effectPass); + } + this.rebuilding = false; + this.needsRebuild = false; + this.emit('pipeline:rebuilt', sortedEffects.length); + } + createBuiltinEffect(pp, name, params) { + switch (name) { + case 'bloom': + return new pp.BloomEffect({ + intensity: params.intensity ?? 1.0, + luminanceThreshold: params.threshold ?? 0.8, + luminanceSmoothing: params.smoothing ?? 0.075, + mipmapBlur: true, + ...params + }); + case 'vignette': + return new pp.VignetteEffect({ + darkness: params.darkness ?? 0.5, + offset: params.offset ?? 0.5, + ...params + }); + case 'fxaa': + return new pp.FXAAEffect(); + case 'chromaticAberration': + return new pp.ChromaticAberrationEffect({ + offset: params.offset ?? new (pp.Vector2 ?? THREE__namespace.Vector2)(0.001, 0.001), + ...params + }); + case 'ssao': + if (pp.SSAOEffect) { + return new pp.SSAOEffect(this.camera, null, { + samples: params.samples ?? 16, + rings: params.rings ?? 4, + intensity: params.intensity ?? 1.0, + ...params + }); + } + return null; + case 'dof': + if (pp.DepthOfFieldEffect) { + return new pp.DepthOfFieldEffect(this.camera, { + focusDistance: params.focusDistance ?? 0.02, + focalLength: params.focalLength ?? 0.05, + bokehScale: params.bokehScale ?? 2.0, + ...params + }); + } + return null; + case 'toneMapping': + if (pp.ToneMappingEffect) { + return new pp.ToneMappingEffect({ + mode: params.mode ?? 1, // ACESFilmicToneMapping + ...params + }); + } + return null; + default: + console.warn(`PostProcessingPipeline: unknown built-in effect '${name}'`); + return null; + } + } + } + + /** + * Service provider for post-processing pipeline. + * Deferred: only loaded when post-processing is requested. + */ + class PostProcessingServiceProvider extends AbstractServiceProvider { + register(app) { + app.singleton('postprocessing', () => new PostProcessingPipeline()); + } + boot(app) { + app.on('destroyed', () => { + if (app.getContainer().bound('postprocessing')) { + const pp = app.make('postprocessing'); + pp.dispose(); + } + }); + } + provides() { + return ['postprocessing']; + } + isDeferred() { + return true; + } + } + + /** + * Static PostProcessing facade. + * + * @example + * ```typescript + * import { PostProcessing } from 'gamebyte-framework'; + * + * PostProcessing.add('bloom', { intensity: 0.5, threshold: 0.8 }); + * PostProcessing.add('vignette', { darkness: 0.3 }); + * PostProcessing.add('fxaa'); + * PostProcessing.get('bloom')?.setParams({ intensity: 1.0 }); + * ``` + */ + class PostProcessing extends Facade { + static getFacadeAccessor() { + return 'postprocessing'; + } + static getPipeline() { + return this.resolve(); + } + static add(name, params) { + return this.getPipeline().add(name, params); + } + static remove(name) { + this.getPipeline().remove(name); + } + static get(name) { + return this.getPipeline().get(name); + } + static get enabled() { + return this.getPipeline().enabled; + } + static set enabled(value) { + this.getPipeline().enabled = value; + } + static getEffects() { + return this.getPipeline().getEffects(); + } + static registerEffect(name, factory) { + this.getPipeline().registerEffect(name, factory); + } + } + + // ─── Built-in Presets ───────────────────────── + const DAY_PRESET = { + sunPosition: [50, 100, 50], + sunColor: '#ffffff', + sunIntensity: 1.5, + ambientIntensity: 0.6, + skyColor: '#87CEEB', + groundColor: '#4a7c59', + fog: { color: '#c8e0f0', near: 50, far: 500, type: 'linear' } + }; + const SUNSET_PRESET = { + sunPosition: [100, 20, -50], + sunColor: '#ff7733', + sunIntensity: 1.2, + ambientIntensity: 0.3, + skyColor: '#ff6b35', + groundColor: '#2d1b0e', + fog: { color: '#ff9966', near: 30, far: 300, type: 'linear' } + }; + const NIGHT_PRESET = { + sunPosition: [-50, -10, 50], + sunColor: '#4466aa', + sunIntensity: 0.1, + ambientIntensity: 0.15, + skyColor: '#0a0a2e', + groundColor: '#050510', + fog: { color: '#0a0a1a', near: 20, far: 200, type: 'exponential', density: 0.01 } + }; + const OVERCAST_PRESET = { + sunPosition: [0, 80, 30], + sunColor: '#cccccc', + sunIntensity: 0.6, + ambientIntensity: 0.7, + skyColor: '#999999', + groundColor: '#555555', + fog: { color: '#aaaaaa', near: 30, far: 250, type: 'linear' } + }; + /** + * Environment & Skybox system for Three.js scenes. + * + * Manages directional light (sun), hemisphere light (ambient), fog, + * and environment maps (HDRI or procedural sky). + * + * Features: + * - 4 built-in presets: day, sunset, night, overcast + * - Smooth transitions between presets (lerp colors/values) + * - HDRI environment map loading + * - Fog management (linear and exponential) + * - Custom preset registration + */ + class EnvironmentSystem extends EventEmitter { + constructor(scene, renderer) { + super(); + this.renderer = null; + this.directionalLight = null; + this.hemisphereLight = null; + this.presets = new Map(); + this.transitioning = false; + // Reused color objects for lerping + this.tempColor1 = new THREE__namespace.Color(); + this.tempColor2 = new THREE__namespace.Color(); + this.tempColor3 = new THREE__namespace.Color(); + this.scene = scene; + this.renderer = renderer ?? null; + this.currentConfig = { ...DAY_PRESET }; + // Register built-in presets + this.presets.set('day', DAY_PRESET); + this.presets.set('sunset', SUNSET_PRESET); + this.presets.set('night', NIGHT_PRESET); + this.presets.set('overcast', OVERCAST_PRESET); + // Create lights + this.createLights(); + } + /** + * Apply a named preset immediately. + */ + preset(name) { + const config = this.presets.get(name); + if (!config) { + console.warn(`EnvironmentSystem: unknown preset '${name}'`); + return; + } + this.applyConfig(config); + this.currentConfig = { ...config }; + this.emit('preset:applied', name); + } + /** + * Load and apply an HDRI environment map. + */ + /** + * Set the renderer (required for HDRI loading). + * Called by EnvironmentServiceProvider during boot. + */ + setRenderer(renderer) { + this.renderer = renderer; + } + async setHDRI(url, _options) { + if (!this.renderer) { + throw new Error('EnvironmentSystem: renderer is required for HDRI loading. Call setRenderer() first.'); + } + const { RGBELoader } = await import('three/examples/jsm/loaders/RGBELoader.js'); + return new Promise((resolve, reject) => { + const loader = new RGBELoader(); + loader.load(url, (texture) => { + texture.mapping = THREE__namespace.EquirectangularReflectionMapping; + const pmremGenerator = new THREE__namespace.PMREMGenerator(this.renderer); + pmremGenerator.compileEquirectangularShader(); + const envMap = pmremGenerator.fromEquirectangular(texture).texture; + this.scene.environment = envMap; + this.scene.background = envMap; + texture.dispose(); + pmremGenerator.dispose(); + this.emit('hdri:loaded', url); + resolve(); + }, undefined, (error) => reject(error)); + }); + } + /** + * Set procedural sky (uses Three.js Sky shader). + */ + setProceduralSky(config) { + // Store sky config for later use + this.currentConfig.sky = { + turbidity: config?.turbidity ?? 10, + rayleigh: config?.rayleigh ?? 2, + mieCoefficient: config?.mieCoefficient ?? 0.005 + }; + this.emit('sky:configured', this.currentConfig.sky); + } + /** + * Set fog configuration. + */ + setFog(config) { + const fogConfig = { ...this.currentConfig.fog, ...config }; + this.currentConfig.fog = fogConfig; + this.applyFog(fogConfig); + this.emit('fog:changed', fogConfig); + } + /** + * Remove fog. + */ + clearFog() { + this.scene.fog = null; + this.emit('fog:cleared'); + } + /** + * Smooth transition to another preset over duration (seconds). + * Lerps colors and values each frame. + */ + async transitionTo(presetName, duration) { + const target = this.presets.get(presetName); + if (!target) { + console.warn(`EnvironmentSystem: unknown preset '${presetName}'`); + return; + } + if (this.transitioning) + return; // Already transitioning + this.transitioning = true; + const start = { ...this.currentConfig }; + const startTime = performance.now(); + const durationMs = duration * 1000; + return new Promise((resolve) => { + const animate = () => { + const elapsed = performance.now() - startTime; + const t = Math.min(elapsed / durationMs, 1); + const eased = t * t * (3 - 2 * t); // smoothstep + this.lerpConfig(start, target, eased); + if (t < 1) { + requestAnimationFrame(animate); + } + else { + this.currentConfig = { ...target }; + this.transitioning = false; + this.emit('transition:complete', presetName); + resolve(); + } + }; + requestAnimationFrame(animate); + }); + } + /** + * Register a custom preset. + */ + registerPreset(name, config) { + this.presets.set(name, config); + } + /** + * Get current environment configuration. + */ + getConfig() { + return { ...this.currentConfig }; + } + /** + * Dispose lights and cleanup. + */ + dispose() { + if (this.directionalLight) { + this.scene.remove(this.directionalLight); + this.directionalLight.dispose(); + this.directionalLight = null; + } + if (this.hemisphereLight) { + this.scene.remove(this.hemisphereLight); + this.hemisphereLight.dispose(); + this.hemisphereLight = null; + } + this.scene.fog = null; + this.scene.environment = null; + this.presets.clear(); + this.removeAllListeners(); + } + // ─── Private Methods ─────────────────────────── + createLights() { + this.directionalLight = new THREE__namespace.DirectionalLight(0xffffff, 1.5); + this.directionalLight.castShadow = true; + this.directionalLight.shadow.mapSize.set(2048, 2048); + this.scene.add(this.directionalLight); + this.hemisphereLight = new THREE__namespace.HemisphereLight(0x87CEEB, 0x4a7c59, 0.6); + this.scene.add(this.hemisphereLight); + } + applyConfig(config) { + if (this.directionalLight) { + this.directionalLight.color.set(config.sunColor); + this.directionalLight.intensity = config.sunIntensity; + this.directionalLight.position.set(...config.sunPosition); + } + if (this.hemisphereLight) { + this.hemisphereLight.color.set(config.skyColor); + this.hemisphereLight.groundColor.set(config.groundColor); + this.hemisphereLight.intensity = config.ambientIntensity; + } + this.applyFog(config.fog); + } + applyFog(fog) { + if (fog.type === 'exponential') { + this.scene.fog = new THREE__namespace.FogExp2(fog.color, fog.density ?? 0.01); + } + else { + this.scene.fog = new THREE__namespace.Fog(fog.color, fog.near, fog.far); + } + } + lerpConfig(from, to, t) { + if (this.directionalLight) { + this.tempColor1.set(from.sunColor); + this.tempColor2.set(to.sunColor); + this.directionalLight.color.copy(this.tempColor1.lerp(this.tempColor2, t)); + this.directionalLight.intensity = from.sunIntensity + (to.sunIntensity - from.sunIntensity) * t; + // Lerp position + this.directionalLight.position.set(from.sunPosition[0] + (to.sunPosition[0] - from.sunPosition[0]) * t, from.sunPosition[1] + (to.sunPosition[1] - from.sunPosition[1]) * t, from.sunPosition[2] + (to.sunPosition[2] - from.sunPosition[2]) * t); + } + if (this.hemisphereLight) { + this.tempColor1.set(from.skyColor); + this.tempColor2.set(to.skyColor); + this.hemisphereLight.color.copy(this.tempColor1.lerp(this.tempColor2, t)); + this.tempColor1.set(from.groundColor); + this.tempColor2.set(to.groundColor); + this.hemisphereLight.groundColor.copy(this.tempColor1.lerp(this.tempColor2, t)); + this.hemisphereLight.intensity = from.ambientIntensity + (to.ambientIntensity - from.ambientIntensity) * t; + } + // Lerp fog + this.tempColor1.set(from.fog.color); + this.tempColor2.set(to.fog.color); + this.tempColor3.copy(this.tempColor1.lerp(this.tempColor2, t)); + const fogColorStr = '#' + this.tempColor3.getHexString(); + const fogNear = from.fog.near + (to.fog.near - from.fog.near) * t; + const fogFar = from.fog.far + (to.fog.far - from.fog.far) * t; + if (to.fog.type === 'exponential') { + const fromDensity = from.fog.density ?? 0.01; + const toDensity = to.fog.density ?? 0.01; + const density = fromDensity + (toDensity - fromDensity) * t; + this.scene.fog = new THREE__namespace.FogExp2(fogColorStr, density); + } + else { + this.scene.fog = new THREE__namespace.Fog(fogColorStr, fogNear, fogFar); + } + } + } + + /** + * Service provider for the 3D environment system. + * Lazy-initializes EnvironmentSystem when first resolved (requires a Three.js scene). + */ + class EnvironmentServiceProvider extends AbstractServiceProvider { + register(app) { + app.singleton('environment', () => { + const renderer = app.make('renderer'); + const scene = renderer.getStage(); + if (!scene) { + throw new Error('EnvironmentServiceProvider: renderer stage (THREE.Scene) not available'); + } + const nativeRenderer = renderer.getNativeRenderer?.() ?? null; + const env = new EnvironmentSystem(scene, nativeRenderer); + return env; + }); + } + boot(app) { + app.on('destroyed', () => { + if (app.getContainer().bound('environment')) { + const env = app.make('environment'); + env.dispose(); + } + }); + } + provides() { + return ['environment']; + } + isDeferred() { + return true; // Only load when requested (3D feature) + } + } + + /** + * Static Environment facade for 3D scene environment management. + * + * @example + * ```typescript + * import { Environment } from 'gamebyte-framework'; + * + * Environment.preset('sunset'); + * await Environment.transitionTo('night', 5.0); + * await Environment.setHDRI('/hdris/studio.hdr'); + * ``` + */ + class Environment extends Facade { + static getFacadeAccessor() { + return 'environment'; + } + static getSystem() { + return this.resolve(); + } + static preset(name) { + this.getSystem().preset(name); + } + static async setHDRI(url, options) { + return this.getSystem().setHDRI(url, options); + } + static setProceduralSky(config) { + this.getSystem().setProceduralSky(config); + } + static setFog(config) { + this.getSystem().setFog(config); + } + static clearFog() { + this.getSystem().clearFog(); + } + static async transitionTo(preset, duration) { + return this.getSystem().transitionTo(preset, duration); + } + static registerPreset(name, config) { + this.getSystem().registerPreset(name, config); + } + static getConfig() { + return this.getSystem().getConfig(); + } + } + + /** Auto-incrementing entity ID counter */ + let entityIdCounter = 0; + /** + * Internal entity implementation with component storage. + */ + class Entity { + constructor(prefabId, object, tags) { + this.components = new Map(); + this.alive = true; + this.id = `entity_${entityIdCounter++}`; + this.prefabId = prefabId; + this.object = object; + this.tags = new Set(tags); + } + getComponent(name) { + return this.components.get(name); + } + addComponent(name, component) { + this.components.set(name, component); + const lifecycle = component; + if (lifecycle?.onAttach) { + lifecycle.onAttach(this); + } + } + removeComponent(name) { + const component = this.components.get(name); + if (component) { + const lifecycle = component; + if (lifecycle?.onDetach) { + lifecycle.onDetach(this); + } + this.components.delete(name); + } + } + hasComponent(name) { + return this.components.has(name); + } + getTags() { + return Array.from(this.tags); + } + hasTag(tag) { + return this.tags.has(tag); + } + /** Internal: call onUpdate on all components with lifecycle */ + _update(delta) { + if (!this.alive) + return; + for (const component of this.components.values()) { + const lifecycle = component; + if (lifecycle?.onUpdate) { + lifecycle.onUpdate(this, delta); + } + } + } + destroy() { + if (!this.alive) + return; + this.alive = false; + // Call onDestroy on all components + for (const component of this.components.values()) { + const lifecycle = component; + if (lifecycle?.onDestroy) { + lifecycle.onDestroy(this); + } + } + this.components.clear(); + // Remove from scene + if (this.object.parent) { + this.object.parent.remove(this.object); + } + } + get isAlive() { + return this.alive; + } + } + /** + * Prefab / Entity Component System. + * + * Features: + * - JSON-driven prefab definitions with template inheritance (extends) + * - Component lifecycle hooks (onAttach, onDetach, onUpdate, onDestroy) + * - Tag-based entity queries + * - Entity serialization/deserialization for save/load + * - Entity pool with free-list for spawn/despawn performance + * - Config resolution cache for extends chains + */ + class PrefabSystem extends EventEmitter { + constructor(scene) { + super(); + this.definitions = new Map(); + this.resolvedConfigs = new Map(); + this.entities = []; + this.tagIndex = new Map(); + this.scene = null; + this.scene = scene ?? null; + } + /** + * Set the scene for spawning entities. + */ + setScene(scene) { + this.scene = scene; + } + /** + * Register a prefab definition. + */ + register(config) { + this.definitions.set(config.id, config); + // Invalidate resolved cache + this.resolvedConfigs.delete(config.id); + this.emit('prefab:registered', config.id); + } + /** + * Register prefabs from a JSON object or array. + */ + registerFromJSON(json) { + const configs = Array.isArray(json) ? json : [json]; + for (const config of configs) { + if (config && typeof config === 'object' && 'id' in config) { + this.register(config); + } + } + } + /** + * Check if a prefab is registered. + */ + has(prefabId) { + return this.definitions.has(prefabId); + } + /** + * Spawn an entity from a prefab definition. + */ + async spawn(prefabId, overrides) { + const config = this.resolveConfig(prefabId); + if (!config) { + throw new Error(`PrefabSystem: unknown prefab '${prefabId}'`); + } + // Create visual + const object = await this.createVisual(config); + // Apply transform + const transform = { ...config.transform, ...overrides }; + if (transform?.position) { + object.position.set(...transform.position); + } + if (transform?.rotation) { + object.rotation.set(...transform.rotation); + } + if (transform?.scale != null) { + if (typeof transform.scale === 'number') { + object.scale.setScalar(transform.scale); + } + else { + object.scale.set(...transform.scale); + } + } + // Create entity + const entity = new Entity(prefabId, object, config.tags ?? []); + // Attach components + if (config.components) { + for (const [name, data] of Object.entries(config.components)) { + entity.addComponent(name, data); + } + } + // Add to scene + if (this.scene) { + this.scene.add(object); + } + // Track entity + this.entities.push(entity); + // Update tag index + for (const tag of config.tags ?? []) { + if (!this.tagIndex.has(tag)) { + this.tagIndex.set(tag, new Set()); + } + this.tagIndex.get(tag).add(entity); + } + this.emit('entity:spawned', entity); + return entity; + } + /** + * Despawn an entity (removes from scene and tracking). + */ + despawn(entity) { + const e = entity; + e.destroy(); + // Remove from entities list + const idx = this.entities.indexOf(e); + if (idx >= 0) { + this.entities.splice(idx, 1); + } + // Remove from tag index + for (const tag of e.getTags()) { + this.tagIndex.get(tag)?.delete(e); + } + this.emit('entity:despawned', entity.id); + } + /** + * Get all alive entities. + */ + getEntities() { + return this.entities.filter(e => e.isAlive); + } + /** + * Get entities by tag (O(1) lookup via tag index). + */ + getEntitiesByTag(tag) { + const set = this.tagIndex.get(tag); + if (!set) + return []; + return Array.from(set).filter(e => e.isAlive); + } + /** + * Call onUpdate on all entity components (wire to TickSystem). + */ + update(delta) { + const len = this.entities.length; + for (let i = 0; i < len; i++) { + this.entities[i]._update(delta); + } + } + /** + * Serialize all entities to JSON string (for save/load). + */ + serialize() { + const data = this.entities + .filter(e => e.isAlive) + .map(e => ({ + prefabId: e.prefabId, + position: [e.object.position.x, e.object.position.y, e.object.position.z], + rotation: [e.object.rotation.x, e.object.rotation.y, e.object.rotation.z], + scale: [e.object.scale.x, e.object.scale.y, e.object.scale.z] + })); + return JSON.stringify(data); + } + /** + * Deserialize and respawn entities from JSON string. + */ + async deserialize(data) { + const entries = JSON.parse(data); + for (const entry of entries) { + await this.spawn(entry.prefabId, { + position: entry.position, + rotation: entry.rotation, + scale: entry.scale + }); + } + } + /** + * Dispose all entities and clear definitions. + */ + dispose() { + for (const entity of this.entities) { + entity.destroy(); + } + this.entities.length = 0; + this.tagIndex.clear(); + this.definitions.clear(); + this.resolvedConfigs.clear(); + this.scene = null; + this.removeAllListeners(); + } + // ─── Private Methods ─────────────────────────── + /** + * Resolve a prefab config, handling extends chains. + * Results are cached for performance. + */ + resolveConfig(prefabId) { + // Check cache + const cached = this.resolvedConfigs.get(prefabId); + if (cached) + return cached; + const config = this.definitions.get(prefabId); + if (!config) + return null; + if (config.extends) { + const parent = this.resolveConfig(config.extends); + if (parent) { + const resolved = this.deepMerge(parent, config); + this.resolvedConfigs.set(prefabId, resolved); + return resolved; + } + } + this.resolvedConfigs.set(prefabId, config); + return config; + } + /** + * Deep merge two prefab configs (parent + child overrides). + */ + deepMerge(parent, child) { + return { + id: child.id, + name: child.name || parent.name, + visual: { ...parent.visual, ...child.visual }, + transform: { ...parent.transform, ...child.transform }, + physics: child.physics ?? parent.physics, + audio: { ...parent.audio, ...child.audio }, + components: { ...parent.components, ...child.components }, + tags: [...(parent.tags ?? []), ...(child.tags ?? [])], + extends: undefined // Resolved, no longer needed + }; + } + /** + * Create a visual (THREE.Object3D) from a prefab config. + */ + async createVisual(config) { + if (config.visual.type === 'primitive' && config.visual.primitive) { + return this.createPrimitive(config.visual.primitive); + } + if (config.visual.type === 'model' && config.visual.url) { + // Dynamic import to avoid hard dependency on ModelLoader + try { + const { ModelLoader } = await Promise.resolve().then(function () { return ModelLoader$1; }); + const loader = new ModelLoader(); + const model = await loader.load(config.visual.url); + return model.scene.clone(); + } + catch { + console.warn(`PrefabSystem: failed to load model '${config.visual.url}', using placeholder`); + return this.createPrimitive({ shape: 'box', color: 0xff0000, size: 1 }); + } + } + return new THREE__namespace.Object3D(); + } + /** + * Create a primitive geometry mesh. + */ + createPrimitive(config) { + const size = typeof config.size === 'number' ? config.size : 1; + const s = Array.isArray(config.size) ? config.size : [size, size, size]; + let geometry; + switch (config.shape) { + case 'box': + geometry = new THREE__namespace.BoxGeometry(s[0], s[1], s[2]); + break; + case 'sphere': + geometry = new THREE__namespace.SphereGeometry(s[0] / 2, 16, 16); + break; + case 'cylinder': + geometry = new THREE__namespace.CylinderGeometry(s[0] / 2, s[0] / 2, s[1], 16); + break; + case 'plane': + geometry = new THREE__namespace.PlaneGeometry(s[0], s[1]); + break; + case 'cone': + geometry = new THREE__namespace.ConeGeometry(s[0] / 2, s[1], 16); + break; + case 'torus': + geometry = new THREE__namespace.TorusGeometry(s[0] / 2, s[0] / 6, 16, 32); + break; + default: + geometry = new THREE__namespace.BoxGeometry(1, 1, 1); + } + const material = new THREE__namespace.MeshStandardMaterial({ + color: config.color ?? 0x888888 + }); + return new THREE__namespace.Mesh(geometry, material); + } + } + + /** + * Service provider for the Prefab / Entity Component System. + */ + class PrefabServiceProvider extends AbstractServiceProvider { + register(app) { + app.singleton('prefabs', () => { + const prefabs = new PrefabSystem(); + // Set scene if renderer is available + if (app.getContainer().bound('renderer')) { + const renderer = app.make('renderer'); + const stage = renderer.getStage?.(); + if (stage) + prefabs.setScene(stage); + } + return prefabs; + }); + } + boot(app) { + const container = app.getContainer(); + // Wire to TickSystem for component updates + if (container.bound('tick')) { + const tick = app.make('tick'); + tick.subscribe((state) => { + if (container.bound('prefabs')) { + const prefabs = app.make('prefabs'); + prefabs.update(state.delta); + } + }, 5); // Priority 5: after physics, before rendering + } + app.on('destroyed', () => { + if (container.bound('prefabs')) { + app.make('prefabs').dispose(); + } + }); + } + provides() { + return ['prefabs']; + } + isDeferred() { + return true; + } + } + + /** + * Static Prefabs facade for entity/prefab management. + * + * @example + * ```typescript + * import { Prefabs } from 'gamebyte-framework'; + * + * Prefabs.register({ + * id: 'enemy', + * name: 'Enemy', + * visual: { type: 'model', url: '/models/enemy.glb' }, + * tags: ['npc', 'enemy'] + * }); + * + * const enemy = await Prefabs.spawn('enemy', { position: [0, 0, 5] }); + * enemy.addComponent('health', { current: 100, max: 100 }); + * + * const npcs = Prefabs.getEntitiesByTag('npc'); + * const save = Prefabs.serialize(); + * ``` + */ + class Prefabs extends Facade { + static getFacadeAccessor() { + return 'prefabs'; + } + static getSystem() { + return this.resolve(); + } + static register(config) { + this.getSystem().register(config); + } + static registerFromJSON(json) { + this.getSystem().registerFromJSON(json); + } + static has(prefabId) { + return this.getSystem().has(prefabId); + } + static async spawn(prefabId, overrides) { + return this.getSystem().spawn(prefabId, overrides); + } + static despawn(entity) { + this.getSystem().despawn(entity); + } + static getEntities() { + return this.getSystem().getEntities(); + } + static getEntitiesByTag(tag) { + return this.getSystem().getEntitiesByTag(tag); + } + static serialize() { + return this.getSystem().serialize(); + } + static async deserialize(data) { + return this.getSystem().deserialize(data); + } + } + + /** Priority weights for load ordering */ + const PRIORITY_WEIGHTS = { + critical: 0, + high: 1, + normal: 2, + low: 3 + }; + /** + * Smart Asset Pipeline with priority-ordered loading, memory budgeting, and LRU eviction. + * + * Features: + * - Priority queue: critical assets load first + * - Adaptive concurrency (based on device/network capabilities) + * - Memory budgeting with LRU eviction (frequency-boosted) + * - Format fallback: WebP → PNG, KTX2 → standard + * - Abort controller: cancel pending loads on scene switch + * - Progress events per-asset and overall + * - Error resilience: continues loading even if one asset fails + */ + class SmartAssetPipeline extends EventEmitter { + constructor() { + super(); + this.manifest = null; + this.loaded = new Map(); + this.loading = new Map(); + this.memoryBudgetBytes = 256 * 1024 * 1024; // 256MB default + this.totalLoadedBytes = 0; + this.abortController = null; + // Progress tracking + this.totalToLoad = 0; + this.totalLoaded = 0; + // Adaptive concurrency based on hardware + const cores = typeof navigator !== 'undefined' ? (navigator.hardwareConcurrency ?? 4) : 4; + this.concurrency = Math.min(Math.max(Math.floor(cores / 2), 2), 8); + } + /** + * Register an asset manifest. + */ + registerManifest(manifest) { + this.manifest = manifest; + } + /** + * Load all assets required by a scene. + * Respects priority ordering (critical first, then high, normal, low). + */ + async loadScene(sceneId) { + if (!this.manifest) { + throw new Error('SmartAssetPipeline: no manifest registered'); + } + const assetIds = this.manifest.scenes[sceneId]; + if (!assetIds) { + throw new Error(`SmartAssetPipeline: unknown scene '${sceneId}'`); + } + // Cancel any pending loads from previous scene + this.abortController?.abort(); + this.abortController = new AbortController(); + // Sort by priority + const sorted = [...assetIds].sort((a, b) => { + const pa = this.manifest.assets[a]?.priority ?? 'normal'; + const pb = this.manifest.assets[b]?.priority ?? 'normal'; + return (PRIORITY_WEIGHTS[pa] ?? 2) - (PRIORITY_WEIGHTS[pb] ?? 2); + }); + this.totalToLoad = sorted.length; + this.totalLoaded = 0; + // Load in batches respecting concurrency limit + for (let i = 0; i < sorted.length; i += this.concurrency) { + if (this.abortController.signal.aborted) + break; + const batch = sorted.slice(i, i + this.concurrency); + const promises = batch.map(id => this.loadAsset(id)); + await Promise.allSettled(promises); + } + this.emit('scene:loaded', sceneId); + } + /** + * Get overall loading progress (0-1). + */ + getProgress() { + if (this.totalToLoad === 0) + return 1; + return this.totalLoaded / this.totalToLoad; + } + /** + * Set memory budget in MB. + */ + setMemoryBudget(budgetMB) { + this.memoryBudgetBytes = budgetMB * 1024 * 1024; + this.evictIfOverBudget(); + } + /** + * Load critical assets (blocks until all loaded). + */ + async loadCritical(assetIds) { + this.totalToLoad = assetIds.length; + this.totalLoaded = 0; + await Promise.all(assetIds.map(id => this.loadAsset(id))); + } + /** + * Preload assets in the background (non-blocking). + */ + preloadInBackground(assetIds) { + // Fire and forget - load in batches + (async () => { + for (let i = 0; i < assetIds.length; i += this.concurrency) { + const batch = assetIds.slice(i, i + this.concurrency); + await Promise.allSettled(batch.map(id => this.loadAsset(id))); + } + })(); + } + /** + * Evict unused/least-recently-used assets to free memory. + */ + unloadUnused() { + this.evictIfOverBudget(); + } + /** + * Get a loaded asset by ID. + */ + get(assetId) { + const entry = this.loaded.get(assetId); + if (!entry) + return undefined; + // Update access tracking + entry.lastAccess = performance.now(); + entry.accessCount++; + return entry.data; + } + /** + * Dispose all loaded assets and clear state. + */ + dispose() { + this.abortController?.abort(); + this.abortController = null; + for (const entry of this.loaded.values()) { + this.disposeAssetData(entry.data); + } + this.loaded.clear(); + this.loading.clear(); + this.totalLoadedBytes = 0; + this.totalToLoad = 0; + this.totalLoaded = 0; + this.manifest = null; + this.removeAllListeners(); + } + // ─── Private Methods ─────────────────────────── + async loadAsset(assetId) { + // Already loaded + if (this.loaded.has(assetId)) { + this.totalLoaded++; + this.emitProgress(assetId); + return this.loaded.get(assetId).data; + } + // Already loading (dedup) + if (this.loading.has(assetId)) { + return this.loading.get(assetId); + } + const assetDef = this.manifest?.assets[assetId]; + if (!assetDef) { + console.warn(`SmartAssetPipeline: asset '${assetId}' not found in manifest`); + this.totalLoaded++; + this.emitProgress(assetId); + return undefined; + } + const promise = this.fetchAsset(assetId, assetDef); + this.loading.set(assetId, promise); + try { + const data = await promise; + const size = assetDef.size ?? this.estimateSize(data); + this.loaded.set(assetId, { + data, + size, + lastAccess: performance.now(), + accessCount: 0, + assetId + }); + this.totalLoadedBytes += size; + this.totalLoaded++; + this.emitProgress(assetId); + this.emit('asset:loaded', assetId, data); + // Check memory budget + this.evictIfOverBudget(); + return data; + } + catch (error) { + this.totalLoaded++; + this.emitProgress(assetId); + this.emit('asset:failed', assetId, error); + console.warn(`SmartAssetPipeline: failed to load '${assetId}':`, error); + return undefined; + } + finally { + this.loading.delete(assetId); + } + } + async fetchAsset(assetId, def) { + const url = def.cdn ? `${def.cdn}/${def.url}` : def.url; + const signal = this.abortController?.signal; + switch (def.type) { + case 'json': + return (await fetch(url, { signal })).json(); + case 'binary': + return (await fetch(url, { signal })).arrayBuffer(); + case 'audio': { + const response = await fetch(url, { signal }); + return response.arrayBuffer(); + } + case 'texture': { + // Try WebP fallback if available + const fallbackUrl = def.fallbacks?.webp; + const textureUrl = fallbackUrl ?? url; + return new Promise((resolve, reject) => { + const img = new Image(); + img.crossOrigin = 'anonymous'; + img.onload = () => resolve(img); + img.onerror = () => { + if (fallbackUrl && textureUrl !== url) { + // Fallback to original URL + img.src = url; + } + else { + reject(new Error(`Failed to load texture: ${textureUrl}`)); + } + }; + img.src = textureUrl; + }); + } + case 'model': { + // Return the URL for model loaders to handle + return { url, type: 'model-reference' }; + } + default: + return (await fetch(url, { signal })).blob(); + } + } + evictIfOverBudget() { + if (this.totalLoadedBytes <= this.memoryBudgetBytes) + return; + // LRU with frequency boost: score = lastAccessTime + (accessCount * 1000) + // Lower score = older & less frequently used = evict first + const entries = Array.from(this.loaded.entries()) + .map(([id, entry]) => ({ + id, + entry, + score: entry.lastAccess + entry.accessCount * 1000 + })) + .sort((a, b) => a.score - b.score); + // Evict until under budget + for (const { id, entry } of entries) { + if (this.totalLoadedBytes <= this.memoryBudgetBytes * 0.8) + break; // 80% target + this.disposeAssetData(entry.data); + this.totalLoadedBytes -= entry.size; + this.loaded.delete(id); + this.emit('asset:evicted', id); + } + } + disposeAssetData(data) { + if (!data) + return; + const d = data; + if (typeof d.dispose === 'function') + d.dispose(); + else if (typeof d.destroy === 'function') + d.destroy(); + } + estimateSize(data) { + if (data instanceof ArrayBuffer) + return data.byteLength; + if (data instanceof Blob) + return data.size; + if (data instanceof HTMLImageElement) + return (data.width * data.height * 4); // RGBA + return 1024; // Default 1KB estimate + } + emitProgress(assetId) { + const progress = this.getProgress(); + this.emit('progress', progress, assetId); + } + } + + /** + * Service provider for the Smart Asset Pipeline. + */ + class AssetPipelineServiceProvider extends AbstractServiceProvider { + register(app) { + app.singleton('assets.pipeline', () => new SmartAssetPipeline()); + } + boot(app) { + app.on('destroyed', () => { + if (app.getContainer().bound('assets.pipeline')) { + app.make('assets.pipeline').dispose(); + } + }); + } + provides() { + return ['assets.pipeline']; + } + isDeferred() { + return true; + } + } + + /** + * GameByte Framework - Main Entry Point + * + * A comprehensive JavaScript game engine framework that unifies 2D and 3D + * game development with Laravel-inspired architecture. + */ + // Core Framework + function createGame(config) { + // If config provided, use quick setup + if (config) { + return GameByte.quick(config); + } + // Traditional setup - just create and register providers + const app = GameByte.create(); + // Register core service providers + app.register(new RenderingServiceProvider()); + app.register(new SceneServiceProvider()); + app.register(new PluginServiceProvider()); + app.register(new AssetServiceProvider()); + app.register(new UIServiceProvider()); + app.register(new InputServiceProvider()); + app.register(new PhysicsServiceProvider()); + app.register(new PerformanceServiceProvider()); + app.register(new AudioServiceProvider()); + // Register new feature service providers + app.register(new TickServiceProvider()); + app.register(new ResourceServiceProvider()); + app.register(new PostProcessingServiceProvider()); + app.register(new EnvironmentServiceProvider()); + app.register(new PrefabServiceProvider()); + app.register(new AssetPipelineServiceProvider()); + return app; + } + // Utility function to create a GameByte instance with UI system optimized for mobile games + function createMobileGame() { + const app = createGame(); + // Additional mobile optimizations could be applied here + // such as specific renderer configurations, performance settings, etc. + return app; + } + /** + * Create a GameByte instance pre-configured for merge puzzle games. + * + * @example + * ```typescript + * const game = createMergeGame(); + * await game.initialize(canvas, '2d'); + * + * // Use the Merge facade + * Merge.createGame({ rows: 5, cols: 5 }); + * scene.addChild(Merge.getContainer()); + * Merge.start(); + * + * game.start(); + * ``` + */ + function createMergeGame() { + const app = createGame(); + // Register merge service provider + app.register(new MergeServiceProvider()); + return app; + } + // Default export for convenient importing + const GameByteFramework = { + GameByte, + createGame, + createMobileGame, + RenderingMode: exports.RenderingMode, + ServiceContainer, + Assets, + Gradients, + // Facades for static access + Renderer: null, // Will be set after app initialization + Scenes: null, // Will be set after app initialization + Plugins: null, // Will be set after app initialization + UI: null, // Will be set after app initialization + Animations: null, // Will be set after app initialization + Themes: null, // Will be set after app initialization + Input: null, // Will be set after app initialization + Physics: null, // Will be set after app initialization + Performance: null, // Will be set after app initialization + Audio: null, // Will be set after app initialization + Merge: null, // Will be set after app initialization + Tick: null, // Will be set after app initialization + Resources: null, // Will be set after app initialization + PostProcessing: null, // Will be set after app initialization + Environment: null, // Will be set after app initialization + Prefabs: null // Will be set after app initialization + }; + /** + * Initialize facades with a GameByte application instance. + * This should be called after creating your GameByte app. + */ + function initializeFacades(app) { + // Import facades (already imported at the top of this file) + Facade.setApplication(app); + // Update default export facades + GameByteFramework.Renderer = Renderer; + GameByteFramework.Scenes = Scenes; + GameByteFramework.Plugins = Plugins; + GameByteFramework.UI = UI; + GameByteFramework.Animations = Animations; + GameByteFramework.Themes = Themes; + GameByteFramework.Input = Input$1; + GameByteFramework.Physics = Physics; + GameByteFramework.Performance = Performance; + GameByteFramework.Audio = Audio; + GameByteFramework.Merge = Merge; + GameByteFramework.Tick = Tick; + GameByteFramework.Resources = Resources; + GameByteFramework.PostProcessing = PostProcessing; + GameByteFramework.Environment = Environment; + GameByteFramework.Prefabs = Prefabs; + } + + /** + * ModelLoader - Wrapper for Three.js GLTF/GLB model loading + * + * Provides easy loading of 3D models with animation support. + * + * @example + * ```typescript + * const loader = new ModelLoader(); + * + * // Load a model + * const result = await loader.load('/models/character.glb'); + * scene.add(result.scene); + * + * // With progress tracking + * const result = await loader.load('/models/character.glb', { + * onProgress: (progress) => console.log(`Loading: ${progress * 100}%`) + * }); + * + * // Access animations + * if (result.animations.length > 0) { + * const mixer = new THREE.AnimationMixer(result.scene); + * mixer.clipAction(result.animations[0]).play(); + * } + * ``` + */ + class ModelLoader extends EventEmitter { + constructor(config = {}) { + super(); + this.dracoLoader = null; + this.cache = new Map(); + this.config = { + dracoDecoderPath: config.dracoDecoderPath ?? '/draco/', + enableDraco: config.enableDraco ?? false, + basePath: config.basePath ?? '', + }; + this.loader = new GLTFLoader_js.GLTFLoader(); + if (this.config.enableDraco) { + this.setupDraco(); + } + } + setupDraco() { + this.dracoLoader = new DRACOLoader_js.DRACOLoader(); + this.dracoLoader.setDecoderPath(this.config.dracoDecoderPath); + this.loader.setDRACOLoader(this.dracoLoader); + } + /** + * Enable Draco compression support + */ + enableDraco(decoderPath) { + if (decoderPath) { + this.config.dracoDecoderPath = decoderPath; + } + this.config.enableDraco = true; + this.setupDraco(); + return this; + } + /** + * Load a GLTF/GLB model + */ + async load(url, options = {}) { + const fullUrl = (options.basePath ?? this.config.basePath) + url; + // Check cache first + if (this.cache.has(fullUrl)) { + return this.cloneModel(this.cache.get(fullUrl)); + } + return new Promise((resolve, reject) => { + this.loader.load(fullUrl, (gltf) => { + const model = { + scene: gltf.scene, + animations: gltf.animations, + scenes: gltf.scenes, + cameras: gltf.cameras, + parser: gltf.parser, + userData: gltf.userData, + }; + // Cache the original + this.cache.set(fullUrl, model); + this.emit('load', model, fullUrl); + resolve(this.cloneModel(model)); + }, (progress) => { + const percent = progress.total > 0 ? progress.loaded / progress.total : 0; + options.onProgress?.(percent); + this.emit('progress', percent, fullUrl); + }, (error) => { + const err = error instanceof Error ? error : new Error(String(error)); + this.emit('error', err, fullUrl); + reject(err); + }); + }); + } + /** + * Preload multiple models for later use + */ + async preload(urls, options = {}) { + return Promise.all(urls.map(url => this.load(url, options))); + } + /** + * Check if a model is cached + */ + isCached(url) { + const fullUrl = this.config.basePath + url; + return this.cache.has(fullUrl); + } + /** + * Get a cached model (returns clone if cached, undefined if not) + */ + getCached(url) { + const fullUrl = this.config.basePath + url; + const cached = this.cache.get(fullUrl); + return cached ? this.cloneModel(cached) : undefined; + } + /** + * Clear the model cache + */ + clearCache() { + this.cache.forEach(model => { + this.disposeModel(model); + }); + this.cache.clear(); + } + /** + * Clone a model (for reusing cached models) + * @internal Used internally for cache management + */ + cloneModel(model) { + const clonedScene = model.scene.clone(); + // Deep-clone materials so each instance is independent + clonedScene.traverse((child) => { + if (child instanceof THREE__namespace.Mesh) { + if (Array.isArray(child.material)) { + child.material = child.material.map(m => m.clone()); + } + else if (child.material) { + child.material = child.material.clone(); + } + } + }); + return { + scene: clonedScene, + animations: model.animations.map(clip => clip.clone()), + scenes: model.scenes.map(scene => scene.clone()), + cameras: model.cameras.map(camera => camera.clone()), + parser: model.parser, + userData: { ...model.userData }, + }; + } + /** + * Dispose of a model's GPU resources (geometries, materials, textures) + * + * IMPORTANT: Models returned by load() and getCached() are clones. + * You are responsible for disposing them when no longer needed to prevent memory leaks. + * + * @example + * ```typescript + * const model = await loader.load('/models/character.glb'); + * scene.add(model.scene); + * + * // When done with the model + * loader.disposeModel(model); + * scene.remove(model.scene); + * ``` + */ + disposeModel(model) { + model.scene.traverse((object) => { + if (object instanceof THREE__namespace.Mesh) { + object.geometry?.dispose(); + if (Array.isArray(object.material)) { + object.material.forEach(mat => mat.dispose()); + } + else { + object.material?.dispose(); + } + } + }); + } + /** + * Dispose of the loader and all cached models + */ + dispose() { + this.clearCache(); + this.dracoLoader?.dispose(); + } + } + + var ModelLoader$1 = /*#__PURE__*/Object.freeze({ + __proto__: null, + ModelLoader: ModelLoader + }); + exports.ARCHERO_COLORS = ARCHERO_COLORS; exports.AbstractServiceProvider = AbstractServiceProvider; exports.Animations = Animations; exports.ArcheroMenu = ArcheroMenu; + exports.AssetPipelineServiceProvider = AssetPipelineServiceProvider; exports.AssetServiceProvider = AssetServiceProvider; exports.Assets = Assets; exports.AudioFacade = Audio; @@ -59498,6 +62789,10 @@ exports.DefaultUITheme = DefaultUITheme; exports.DesignScaler = DesignScaler; exports.DeviceDetector = DeviceDetector; + exports.DisposableRegistry = DisposableRegistry; + exports.Environment = Environment; + exports.EnvironmentServiceProvider = EnvironmentServiceProvider; + exports.EnvironmentSystem = EnvironmentSystem; exports.EventEmitter = EventEmitter; exports.Facade = Facade; exports.FrameRateManagerClass = FrameRateManager; @@ -59550,6 +62845,7 @@ exports.GameSelectColors = GameSelectColors; exports.GameSlider = GameSlider; exports.GameSliderColors = GameSliderColors; + exports.GameSoundPresets = GameSoundPresets; exports.GameSplash = GameSplash; exports.GameStyleButton = GameStyleButton; exports.GameStyleColors = GameStyleColors; @@ -59585,6 +62881,7 @@ exports.Music = MusicSystemFacade; exports.PanelManager = PanelManager; exports.Performance = Performance; + exports.PerformanceAdvisor = PerformanceAdvisor; exports.PerformanceDebugOverlayClass = PerformanceDebugOverlay; exports.PerformanceFacade = PerformanceFacade; exports.PerformanceMonitor = PerformanceMonitor; @@ -59602,11 +62899,22 @@ exports.PluginManager = PluginManager; exports.PluginServiceProvider = PluginServiceProvider; exports.Plugins = Plugins; + exports.PostProcessing = PostProcessing; + exports.PostProcessingPipeline = PostProcessingPipeline; + exports.PostProcessingServiceProvider = PostProcessingServiceProvider; + exports.PrefabServiceProvider = PrefabServiceProvider; + exports.PrefabSystem = PrefabSystem; + exports.Prefabs = Prefabs; + exports.QualityTierManager = QualityTierManager; exports.RendererFacade = Renderer; exports.RendererFactory = RendererFactory; exports.RenderingCompatibility = RenderingCompatibility; exports.RenderingOptimizerClass = RenderingOptimizer; exports.RenderingServiceProvider = RenderingServiceProvider; + exports.ResourceScope = ResourceScope; + exports.ResourceServiceProvider = ResourceServiceProvider; + exports.ResourceTracker = ResourceTracker; + exports.Resources = Resources; exports.ResponsiveCanvas = ResponsiveCanvas; exports.ResponsiveContainer = ResponsiveContainer; exports.ResponsiveLayoutManager = ResponsiveLayoutManager; @@ -59620,11 +62928,15 @@ exports.ServiceContainer = ServiceContainer; exports.ShineEffect = ShineEffect; exports.SimpleScreen = SimpleScreen; + exports.SmartAssetPipeline = SmartAssetPipeline; exports.Spatial = SpatialAudioFacade; exports.SplashScreen = SplashScreen; exports.StarBurstEffect = StarBurstEffect; exports.SunburstEffect = SunburstEffect; exports.Themes = Themes; + exports.Tick = Tick; + exports.TickServiceProvider = TickServiceProvider; + exports.TickSystem = TickSystem; exports.TopBar = TopBar; exports.UI = UI; exports.UIButton = UIButton; diff --git a/src/ui/animations/UIAnimationSystem.ts b/src/ui/animations/UIAnimationSystem.ts index 9666ddb..6a3c650 100644 --- a/src/ui/animations/UIAnimationSystem.ts +++ b/src/ui/animations/UIAnimationSystem.ts @@ -476,19 +476,24 @@ export class GameByteUIAnimationSystem extends EventEmitter implements UIAnimati /** * Set nested property value */ + private static readonly FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']); + private setNestedProperty(obj: any, path: string, value: any): void { const keys = path.split('.'); let current = obj; - + for (let i = 0; i < keys.length - 1; i++) { const key = keys[i]; + if (GameByteUIAnimationSystem.FORBIDDEN_KEYS.has(key)) return; if (!(key in current) || typeof current[key] !== 'object') { current[key] = {}; } current = current[key]; } - - current[keys[keys.length - 1]] = value; + + const finalKey = keys[keys.length - 1]; + if (GameByteUIAnimationSystem.FORBIDDEN_KEYS.has(finalKey)) return; + current[finalKey] = value; } /** diff --git a/src/ui/splash/GameSplash.ts b/src/ui/splash/GameSplash.ts index 8b01cdc..bdd2ca7 100644 --- a/src/ui/splash/GameSplash.ts +++ b/src/ui/splash/GameSplash.ts @@ -94,7 +94,14 @@ export class GameSplash { // Create splash if not found in HTML const css = document.createElement('style'); css.id = 'gamebyte-splash-styles'; - css.textContent = GameSplash.getInlineCSS(this.config).replace(/<\/?style[^>]*>/g, ''); + let cssText = GameSplash.getInlineCSS(this.config); + // Strip style tags iteratively to prevent bypass via nesting + let prev = ''; + while (prev !== cssText) { + prev = cssText; + cssText = cssText.replace(/<\/?style[^>]*>/gi, ''); + } + css.textContent = cssText; document.head.appendChild(css); document.body.insertAdjacentHTML('afterbegin', GameSplash.getInlineHTML(this.config)); @@ -507,7 +514,7 @@ export class GameSplash { `; diff --git a/src/utils/PixiVersionDetection.ts b/src/utils/PixiVersionDetection.ts index c994bb8..b7e7b7f 100644 --- a/src/utils/PixiVersionDetection.ts +++ b/src/utils/PixiVersionDetection.ts @@ -24,6 +24,10 @@ export interface FeatureSupport { * Parse semantic version string into components */ export function parseVersion(versionString: string): VersionInfo { + // Guard against excessively long input to prevent regex backtracking + if (!versionString || versionString.length > 128) { + return { major: 0, minor: 0, patch: 0, raw: versionString || '' }; + } const match = versionString.match(/(\d+)\.(\d+)\.(\d+)/); if (!match) { return { major: 0, minor: 0, patch: 0, raw: versionString }; From 1330e12ad63feb85892b2d3b46a69e30636b32d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Can=20Erdog=CC=86an?= Date: Fri, 13 Feb 2026 19:36:14 +0300 Subject: [PATCH 4/5] fix: Resolve PR review issues - demo bugs, API mismatches, CodeQL alerts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeQL fixes: - UIAnimationSystem: Use inline key checks + hasOwnProperty for prototype pollution guard (CodeQL #11) - GameSplash: Use indexOf-based loop instead of regex for style-tag stripping to avoid polynomial backtracking (CodeQL #13) Demo bug fixes: - gem-collector: Move graphics() after createGame(), fix button positioning with getContainer().position.set() - whack-a-mole: Convert timer from ms to seconds (duration/1000) - hello-world: Add stage.addChild(t) and fix i*0 → i*18 spacing GameTopBar API alignment (6 demos): - Replace non-existent methods (updateScore, updateTitle, updateLevel, updateLeftText, updateRightText, setScore, setCustomValue) with actual API: resources[] config + updateResource(type, value) - Affected: 2048, cookie-clicker, gem-collector, neon-pong, simon-says, slingshot-game, whack-a-mole Standards compliance: - Fix UMD path in 9 demos: /gamebyte.umd.js → ../gamebyte.umd.js - Move import LiveDemo to after frontmatter in overview.md - Rebuild UMD bundle with source fixes Co-Authored-By: Claude Opus 4.6 --- docs-site/docs/overview.md | 4 ++-- docs-site/static/demos/2048-demo.html | 12 +++++++--- .../static/demos/cookie-clicker-demo.html | 11 +++++---- .../static/demos/gem-collector-demo.html | 23 +++++++++++-------- docs-site/static/demos/hello-world-demo.html | 4 ++-- docs-site/static/demos/neon-pong-demo.html | 13 +++++++---- docs-site/static/demos/quiz-game-demo.html | 2 +- docs-site/static/demos/rps-game-demo.html | 2 +- docs-site/static/demos/simon-says-demo.html | 14 ++++++----- .../static/demos/slingshot-game-demo.html | 12 ++++++---- docs-site/static/demos/whack-a-mole-demo.html | 13 ++++++----- docs-site/static/gamebyte.umd.js | 21 ++++++++--------- src/ui/animations/UIAnimationSystem.ts | 8 +++---- src/ui/splash/GameSplash.ts | 8 +++---- 14 files changed, 82 insertions(+), 65 deletions(-) diff --git a/docs-site/docs/overview.md b/docs-site/docs/overview.md index b68434d..2ac2a38 100644 --- a/docs-site/docs/overview.md +++ b/docs-site/docs/overview.md @@ -10,6 +10,8 @@ llm_summary: "GameByte is a comprehensive game framework unifying 2D (Pixi.js) a +import LiveDemo from '@site/src/components/LiveDemo'; + # GameByte Framework **Modern Mobile-First Game Development Framework** @@ -96,8 +98,6 @@ See the [AI Agent Guide](/ai-agent-guide) for integration details. ## Live Demos -import LiveDemo from '@site/src/components/LiveDemo'; - ### Space Shooter diff --git a/docs-site/static/demos/2048-demo.html b/docs-site/static/demos/2048-demo.html index 407df53..db3f982 100644 --- a/docs-site/static/demos/2048-demo.html +++ b/docs-site/static/demos/2048-demo.html @@ -28,7 +28,7 @@

2048

Arrow keys or swipe to slide tiles. Merge equal numbers!
- + - + - + - + - + - + - + - + - + - +