├── .gitignore ├── LICENSE ├── README.md ├── dist ├── scrub.cjs ├── scrub.cjs.map ├── scrub.d.ts ├── scrub.js ├── scrub.js.map ├── scrub.mjs └── scrub.mjs.map ├── docs ├── en │ ├── animations.md │ ├── colliders.md │ ├── composite_sprites.md │ ├── debugging.md │ ├── distance.md │ ├── drawing.md │ ├── game.md │ ├── game_loop.md │ ├── layers.md │ ├── main_objects.md │ ├── movement.md │ ├── multi_scene.md │ ├── pivot.md │ ├── sounds.md │ ├── sprite.md │ ├── stage.md │ └── visual_effects.md └── ru │ ├── animations.md │ ├── colliders.md │ ├── composite_sprites.md │ ├── debugging.md │ ├── distance.md │ ├── drawing.md │ ├── game.md │ ├── game_loop.md │ ├── layers.md │ ├── main_objects.md │ ├── movement.md │ ├── multi_scene.md │ ├── overview.md │ ├── pivot.md │ ├── sounds.md │ ├── sprite.md │ ├── stage.md │ └── visual_effects.md ├── package.json ├── public └── polyfills.js ├── src ├── Camera.ts ├── CameraChanges.ts ├── Costume.ts ├── EventEmitter.ts ├── Game.ts ├── MultiplayerControl.ts ├── MultiplayerGame.ts ├── MultiplayerSprite.ts ├── OrphanSharedData.ts ├── Player.ts ├── ScheduledCallbackExecutor.ts ├── ScheduledCallbackItem.ts ├── ScheduledState.ts ├── SharedData.ts ├── Sprite.ts ├── Stage.ts ├── SyncObjectInterface.ts ├── browser │ └── scrub.ts ├── collisions │ ├── BVH.ts │ ├── BVHBranch.ts │ ├── CircleCollider.ts │ ├── Collider.ts │ ├── CollisionResult.ts │ ├── CollisionSystem.ts │ ├── PointCollider.ts │ ├── PolygonCollider.ts │ ├── SAT.ts │ └── index.ts ├── jmp │ ├── JetcodeSocket.ts │ ├── JetcodeSocketConnect.ts │ ├── JetcodeSocketParameters.ts │ └── index.ts ├── scrub.ts └── utils │ ├── ErrorMessages.ts │ ├── Keyboard.ts │ ├── KeyboardMap.ts │ ├── Mouse.ts │ ├── Registry.ts │ ├── Styles.ts │ ├── ValidatorFactory.ts │ └── index.ts ├── tsconfig.json └── tsup.config.ts /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-present Andrey Nilov, JETCODE (jetcode.org) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ScrubJS — HTML5 Game Library with a Focus on Ease of Learning 2 | 3 | [Русская версия](docs/ru/overview.md) 4 | 5 | The architecture and naming system are inspired by Scratch, a popular visual programming environment, making ScrubJS intuitive for beginner developers. 6 | 7 | ## Purpose 8 | 9 | The library is designed to provide a simple and accessible way to learn the fundamentals of game development, such as: 10 | 11 | * Game loop 12 | * Sprite management 13 | * Event handling 14 | * Animation 15 | * Collisions 16 | 17 | ## Advantages 18 | 19 | * Multi-scene support 20 | * Built-in collider system and touch detection 21 | * Easy handling of sounds, costumes, and layers 22 | * Debugging tools and collider visualization 23 | * Error display with hints 24 | 25 | ## Quick Start: 26 | 27 | ```javascript 28 | const game = new Game(800, 600); 29 | const stage = new Stage(); 30 | 31 | const cat = new Sprite(); 32 | cat.addCostume("cat.png"); 33 | 34 | stage.forever(() => { 35 | cat.move(5); 36 | cat.bounceOnEdge(); 37 | }); 38 | 39 | game.run(); 40 | ``` 41 | 42 | ## Documentation: 43 | 44 | ### Architecture: 45 | 46 | * [Core Game Objects](docs/en/main_objects.md) 47 | * [Game Object](docs/en/game.md) 48 | * [Stage](docs/en/stage.md) 49 | * [Sprite Object](docs/en/sprite.md) 50 | * [Game Loop](docs/en/game_loop.md) 51 | 52 | ### Examples & Practices: 53 | 54 | * [Movement, rotation, coordinates](docs/en/movement.md) 55 | * [Pivot point](docs/en/pivot.md) 56 | * [Determining the distance between a sprite and another object](docs/en/distance.md ) 57 | * [Costumes and animation](docs/en/animations.md) 58 | * [Layers](docs/en/layers.md) 59 | * [Drawing capabilities](docs/en/drawing.md) 60 | * [In-game sounds](docs/en/sounds.md) 61 | * [Colliders, touches, and tags](docs/en/colliders.md) 62 | * [Multi-scene games](docs/en/multi_scene.md) 63 | * [Visual effects: opacity and CSS filters](docs/en/visual_effects.md) 64 | * [Composite sprites](docs/en/composite_sprites.md) 65 | * [Debugging and performance](docs/en/debugging.md) 66 | -------------------------------------------------------------------------------- /docs/en/animations.md: -------------------------------------------------------------------------------- 1 | # Costumes and Animation 2 | 3 | Costumes are visual representations of a sprite that can be switched to create the effect of animation. ScrubJS provides flexible tools for loading, configuring, and managing costumes, including support for images, sprite sheets (grids), dynamic drawing, and working with transparency and cropping. 4 | 5 | ## Adding a Single Costume with Settings 6 | 7 | The `addCostume()` method allows you to load an image and set display parameters. 8 | 9 | ### Simple Example: 10 | ```javascript 11 | sprite.addCostume('images/player_idle.png'); 12 | ``` 13 | 14 | ### Adding with Name and Rotation: 15 | ```javascript 16 | sprite.addCostume('images/player_idle.png', { 17 | name: 'idle', 18 | rotate: 90 19 | }); 20 | ``` 21 | Here, the sprite will be rotated by 90° when displayed. 22 | 23 | ### Cropping a Part of the Image: 24 | ```javascript 25 | sprite.addCostume('images/player_sheet.png', { 26 | name: 'head', 27 | x: 32, 28 | y: 0, 29 | width: 32, 30 | height: 32 31 | }); 32 | ``` 33 | This will allow you to take only a fragment of the image (32×32 pixels), starting from the coordinate (32, 0). 34 | 35 | ### Flipping Horizontally: 36 | ```javascript 37 | sprite.addCostume('images/player_sheet.png', { 38 | flipX: true 39 | }); 40 | ``` 41 | This will flip the image horizontally. 42 | 43 | ### Flipping Vertically: 44 | ```javascript 45 | sprite.addCostume('images/player_sheet.png', { 46 | flipY: true 47 | }); 48 | ``` 49 | This will flip the image vertically. 50 | 51 | ### Transparency by Color: 52 | ```javascript 53 | sprite.addCostume('images/enemy.png', { 54 | alphaColor: '#FF00FF', // The background color that will become transparent 55 | alphaTolerance: 15 // Tolerance (how close the color should be to also become transparent) 56 | }); 57 | ``` 58 | Convenient for removing background colors, such as magenta (#FF00FF). 59 | 60 | ### Cropping the Costume: 61 | ```javascript 62 | sprite.addCostume('images/player.png', { 63 | crop: 10 64 | }); 65 | ``` 66 | Removes 10 pixels from each side of the image. 67 | 68 | ### Cropping the Costume, More Precise Configuration: 69 | ```javascript 70 | sprite.addCostume('images/player.png', { 71 | cropTop: 5, 72 | cropRight: 10, 73 | cropBottom: 5, 74 | cropLeft: 10 75 | }); 76 | ``` 77 | Removes 5 pixels from the top and bottom, and 10 from the left and right. 78 | 79 | --- 80 | 81 | ## Loading Costumes from a Grid (Sprite Sheet) 82 | 83 | If you have an image with multiple frames, for example, 4×4: 84 | 85 | ```javascript 86 | sprite.addCostumeGrid('images/player_walk.png', { 87 | cols: 4, 88 | rows: 4, 89 | name: 'walk' 90 | }); 91 | ``` 92 | 93 | This will create 16 costumes with the names `walk0`, `walk1`, ..., `walk15`. 94 | 95 | ### With a Limit on the Number of Frames: 96 | ```javascript 97 | sprite.addCostumeGrid('images/player_walk.png', { 98 | cols: 4, 99 | rows: 4, 100 | limit: 6 101 | }); 102 | ``` 103 | Only the first 6 frames will be added. 104 | 105 | ### Skipping Frames (offset): 106 | ```javascript 107 | sprite.addCostumeGrid('images/player_walk.png', { 108 | cols: 4, 109 | rows: 4, 110 | offset: 4, 111 | limit: 4 112 | }); 113 | ``` 114 | We skip the first 4 frames and add the next 4. 115 | 116 | --- 117 | 118 | ## Creating a Costume with Code 119 | 120 | You can draw costumes manually: 121 | 122 | ```javascript 123 | sprite.drawCostume((ctx) => { 124 | ctx.fillStyle = 'red'; 125 | ctx.fillRect(0, 0, 50, 50); 126 | }, { width: 50, height: 50, name: 'red-square' }); 127 | ``` 128 | 129 | Use when you need a simple visual without loading images. 130 | 131 | ### Example with a Circle: 132 | ```javascript 133 | sprite.drawCostume((ctx) => { 134 | ctx.fillStyle = 'blue'; 135 | ctx.beginPath(); 136 | ctx.arc(25, 25, 20, 0, 2 * Math.PI); 137 | ctx.fill(); 138 | }, { width: 50, height: 50, name: 'blue-circle' }); 139 | ``` 140 | 141 | --- 142 | 143 | ## Managing Costumes 144 | 145 | ### Switching by Index: 146 | ```javascript 147 | sprite.switchCostume(1); 148 | ``` 149 | 150 | ### Switching by Name: 151 | ```javascript 152 | sprite.switchCostumeByName('walk3'); 153 | ``` 154 | 155 | ### Next Costume (Convenient for Animation): 156 | ```javascript 157 | sprite.nextCostume(); // next in the list 158 | ``` 159 | 160 | Specifying a range: 161 | ```javascript 162 | sprite.nextCostume(4, 7); // loop between 4 and 7 indices 163 | ``` 164 | 165 | ### Previous Costume, Specifying a Range: 166 | ```javascript 167 | sprite.prevCostume(4, 7); // loop between 7 and 4 indices 168 | ``` 169 | 170 | ### Removing a Costume: 171 | ```javascript 172 | sprite.removeCostume(0); // remove the first costume 173 | ``` 174 | 175 | --- 176 | 177 | ## Sprite Animation Example from Grid 178 | 179 | ```javascript 180 | const sprite = new Sprite(stage); 181 | sprite.addCostumeGrid('images/player_run.png', { 182 | cols: 6, 183 | rows: 1, 184 | name: 'run' 185 | }); 186 | 187 | sprite.forever(() => { 188 | sprite.nextCostume(); 189 | }, 100); // every 100 ms 190 | ``` 191 | 192 | --- 193 | 194 | ## Tips 195 | 196 | - Naming (`name`) is important for easily switching between costumes. 197 | - Use `addCostumeGrid` to load animations from sprite sheets. 198 | - Parameters `alphaColor` and `alphaTolerance` are especially useful when removing backgrounds from PNG/JPG. 199 | - Dynamic costumes (`drawCostume`) allow you to create unique images on the fly. 200 | 201 | --- 202 | 203 | > Full information about the sprite's costume properties and methods can be found in the [Sprite Game Object](sprite.md#costumess) section. 204 | -------------------------------------------------------------------------------- /docs/en/colliders.md: -------------------------------------------------------------------------------- 1 | # Colliders, touches, and tags 2 | 3 | This guide covers the system for handling interactions between objects through colliders, working with collisions, and grouping objects using tags. 4 | 5 | **Important:** If a collider is not added manually, it will be created automatically from the first added costume. 6 | 7 | If you need a sprite without a collider, remove it explicitly: 8 | ```javascript 9 | sprite.removeCollider(); 10 | ``` 11 | 12 | ## 1. Types of Colliders 13 | 14 | Colliders define the interaction zone of a sprite. There are four main types: 15 | 16 | ### Rectangular Collider 17 | ```javascript 18 | const platform = new Sprite(stage); 19 | platform.setRectCollider('main', 100, 30, 0, -15); 20 | // Width 100px, height 30px, offset 15px upwards 21 | ``` 22 | 23 | ### Circular Collider 24 | ```javascript 25 | const ball = new Sprite(stage); 26 | ball.setCircleCollider('hitbox', 25); 27 | // Radius 25px, collider name 'hitbox' 28 | ``` 29 | 30 | ### Polygonal Collider 31 | ```javascript 32 | const triangle = new Sprite(stage); 33 | triangle.setPolygonCollider('main', [ 34 | [0, 0], // Top left 35 | [50, 100], // Bottom 36 | [100, 0] // Top right 37 | ]); 38 | ``` 39 | 40 | ### Automatic Generation 41 | ```javascript 42 | const character = new Sprite(stage); 43 | character.addCostume('hero.png'); 44 | // A rectangular collider with the name 'main' will be created automatically from the first added costume 45 | ``` 46 | 47 | ### Disabling Collider Creation 48 | 49 | ```javascript 50 | const character = new Sprite(stage); 51 | character.addCostume('hero.png'); 52 | character.removeCollider(); 53 | // This will disable the automatic creation of a collider from the costume 54 | ``` 55 | 56 | > Detailed information about the properties and methods for managing sprite colliders can be found in the [Sprite Game Object](sprite.md#colliders) section. 57 | 58 | --- 59 | 60 | ## 2. Checking Collisions 61 | 62 | ### Basic Methods 63 | 64 | **Collision with another sprite:** 65 | ```javascript 66 | if (player.touchSprite(enemy)) { 67 | player.damage(); 68 | } 69 | ``` 70 | 71 | **Collision with a group of objects:** 72 | ```javascript 73 | const hazards = [spikes, fire, poison]; 74 | if (player.touchSprites(hazards)) { 75 | player.respawn(); 76 | } 77 | ``` 78 | 79 | **Interaction with the mouse:** 80 | ```javascript 81 | button.forever(() => { 82 | if (button.touchMouse()) { 83 | button.scale = 1.1; 84 | if (game.mouseDownOnce()) { 85 | startGame(); 86 | } 87 | } 88 | }); 89 | ``` 90 | 91 | ### Checking Stage Edges 92 | 93 | ```javascript 94 | // General edge check 95 | if (bullet.touchEdge()) { 96 | bullet.destroy(); 97 | } 98 | 99 | // Specific directions 100 | if (player.touchBottomEdge()) { 101 | player.jump(); 102 | } 103 | ``` 104 | 105 | ### Working with Coordinates 106 | 107 | ```javascript 108 | const clickPoint = new PointCollider(game.mouseX, game.mouseY); 109 | if (map.touchPoint(clickPoint)) { 110 | showTooltip(); 111 | } 112 | ``` 113 | 114 | ### Optimizing Checks 115 | ```javascript 116 | // Check only the main collider 117 | if (bullet.touchSprite(target, false)) { 118 | // checkChildren = false 119 | } 120 | ``` 121 | 122 | > Detailed information about the properties and methods for checking sprite collisions can be found in the [Sprite Game Object](sprite.md#collisions) section. 123 | 124 | --- 125 | 126 | ## 3. Tag System 127 | 128 | Tags allow grouping objects and checking collisions by categories. 129 | 130 | ### Basic Usage 131 | 132 | **Adding a tag:** 133 | ```javascript 134 | enemy.addTag('danger'); 135 | powerUp.addTag('bonus'); 136 | ``` 137 | 138 | **Checking a group:** 139 | ```javascript 140 | player.forever(() => { 141 | if (player.touchTag('danger')) { 142 | player.health -= 10; 143 | } 144 | 145 | const bonuses = player.touchTagAll('bonus'); 146 | if (bonuses) { 147 | bonuses.forEach(item => { 148 | player.money += 10; 149 | item.delete(); 150 | }); 151 | } 152 | }); 153 | ``` 154 | 155 | ### Inheriting Tags 156 | 157 | Child objects inherit tags from their parent: 158 | ```javascript 159 | const car = new Sprite(stage); 160 | car.addTag('vehicle'); 161 | 162 | const wheel = new Sprite(stage); 163 | wheel.setParent(car); 164 | 165 | console.log(wheel.hasTag('vehicle')); // true 166 | ``` 167 | 168 | ### Dynamic Tag Management 169 | ```javascript 170 | stage.forever(() => { 171 | if (player.touchTag('key')) { 172 | player.otherSprite.delete(); 173 | door.removeTag('locked'); 174 | } 175 | 176 | if (player.touchTag('locked')) { 177 | console.log('The door is locked!'); 178 | } 179 | }); 180 | ``` 181 | 182 | > Detailed information about the properties and methods for managing sprite tags can be found in the [Sprite Game Object](sprite.md#tags) section. 183 | 184 | --- 185 | 186 | ## 4. Working with Overlaps 187 | 188 | The `overlap` properties help implement realistic collision physics. 189 | 190 | **Example of handling a collision:** 191 | ```javascript 192 | if (player.touchSprite(wall)) { 193 | // Position correction 194 | player.x -= player.overlapX; 195 | player.y -= player.overlapY; 196 | 197 | // Visual feedback 198 | player.tint = '#FF0000'; 199 | setTimeout(() => player.tint = '#FFFFFF', 100); 200 | } 201 | ``` 202 | 203 | --- 204 | 205 | ## 5. Debugging Colliders 206 | 207 | **Visualization:** 208 | ```javascript 209 | // For the entire stage 210 | game.debugCollider = true; 211 | game.debugColor = '#00FF0077'; // RGBA 212 | ``` 213 | 214 | **Logging:** 215 | ```javascript 216 | player.forever(() => { 217 | if (player.touchAnySprite()) { 218 | console.log('Collided with:', player.otherSprite.name); 219 | console.log('Depth:', player.overlap); 220 | } 221 | }); 222 | ``` 223 | 224 | --- 225 | 226 | ## 6. Best Practices 227 | 228 | 1. Remove colliders where not needed: 229 | ```javascript 230 | const drop = new Sprite(); 231 | drop.removeCollider(); 232 | ``` 233 | 234 | 2. Disable checking child elements where not needed: 235 | ```javascript 236 | touchTag('group', false) 237 | ``` 238 | 239 | 3. Use composite sprites for different colliders: 240 | - Physics (`body`) 241 | - Interaction zones (`sensor`) 242 | - Attack (`attack`) 243 | 244 | 4. Group objects using tags: 245 | - `enemy`, `player`, `terrain` 246 | - `coin`, `gem` 247 | -------------------------------------------------------------------------------- /docs/en/composite_sprites.md: -------------------------------------------------------------------------------- 1 | # Composite Sprites 2 | 3 | ScrubJS allows creating **sprite hierarchies** where one sprite can be a "parent" and others can be "children". This is useful for building complex objects: characters with animations, machinery with rotating parts, and any structures consisting of multiple visual elements. 4 | 5 | ## 1. Creating Object Hierarchies 6 | 7 | You can connect sprites into composite groups using the `setParent()` or `addChild()` methods. Child sprites automatically follow the parent — both in position and in rotation and scale. 8 | 9 | ### Basic Example: Robot 10 | 11 | ```javascript 12 | const robot = new Sprite(stage); 13 | 14 | const body = new Sprite(stage); 15 | body.setParent(robot); 16 | 17 | const head = new Sprite(stage); 18 | head.setParent(body); 19 | 20 | const armLeft = new Sprite(stage); 21 | armLeft.setParent(body); 22 | 23 | const armRight = new Sprite(stage); 24 | armRight.setParent(body); 25 | 26 | // Equivalent variant using addChild(): 27 | // robot.addChild(body); 28 | // body.addChild(head); 29 | // body.addChild(armLeft); 30 | // body.addChild(armRight); 31 | 32 | // Setting positions relative to the parent 33 | body.y = 0; 34 | head.y = -30; 35 | armLeft.x = -40; 36 | armRight.x = 40; 37 | ``` 38 | 39 | ## 2. Synchronizing Transformations 40 | 41 | Child sprites **inherit all transformations** of the parent: movement, rotation, scale, transparency, etc. 42 | 43 | ### Coordinated Movement and Animation 44 | 45 | ```javascript 46 | robot.forever(() => { 47 | robot.x += 2; // The entire robot moves forward 48 | 49 | // Head rotation and arm waving 50 | head.rotation += 1; 51 | armLeft.rotation = Math.sin(Date.now() / 300) * 30; 52 | }); 53 | ``` 54 | 55 | ### Local and Global Coordinates 56 | 57 | If a child sprite needs to interact with the world, you can use global coordinates: 58 | 59 | ```javascript 60 | const gun = new Sprite(stage); 61 | gun.setParent(robot); 62 | gun.x = 20; 63 | gun.y = -10; 64 | 65 | gun.onClick(() => { 66 | const bullet = new Sprite(stage); 67 | bullet.addCostume('bullet.png'); 68 | 69 | bullet.setPosition(gun.globalX, gun.globalY); // Initial position 70 | bullet.direction = gun.parent.direction; // Parent's direction 71 | 72 | bullet.forever(() => bullet.move(5)); 73 | }); 74 | ``` 75 | 76 | ### Detecting Touch of a Composite Object 77 | 78 | Even if an object consists of multiple child sprites, you can check if **it touches another sprite** using `touchSprite()`. The method works **at any level of the hierarchy** — if at least one child touches, the touch will be counted. 79 | 80 | ### Example: Robot Touches Enemy 81 | 82 | ```javascript 83 | const robot = new Sprite(stage); 84 | const body = new Sprite(stage); 85 | const armLeft = new Sprite(stage); 86 | const armRight = new Sprite(stage); 87 | 88 | robot.addChild(body); 89 | body.addChild(armLeft); 90 | body.addChild(armRight); 91 | 92 | const enemy = new Sprite(stage); 93 | enemy.setPosition(400, 200); 94 | enemy.addCostume('enemy.png'); 95 | 96 | // Simple touch detection: 97 | stage.forever(() => { 98 | if (robot.touchSprite(enemy)) { 99 | console.log("Robot touches the enemy!"); 100 | } 101 | }); 102 | ``` 103 | 104 | `touchSprite()` automatically considers **all descendants** — the check will work even if only one of the robot's arms touches the enemy. 105 | 106 | ## 3. Tips for Working with Hierarchies 107 | 108 | - Use composite sprites for characters, interfaces, and vehicles. 109 | - The parent can be **invisible**, serving only as a "container". 110 | - When a parent is destroyed, all child sprites are automatically destroyed. 111 | - **Multi-level hierarchies** (nested groups) are supported. 112 | -------------------------------------------------------------------------------- /docs/en/debugging.md: -------------------------------------------------------------------------------- 1 | # Debugging and Performance 2 | 3 | ScrubJS provides built-in tools for debugging and performance analysis, which help in game development and testing. However, in the final version of the game, it is recommended to disable debug features to reduce browser load. 4 | 5 | --- 6 | 7 | ## 1. Displaying Errors 8 | 9 | During development, ScrubJS can display **errors and warnings**, including: 10 | 11 | - Typos in methods and properties 12 | - Attempts to access non-existent objects 13 | - API violations 14 | 15 | This is implemented through **proxy wrappers** that track object accesses and compare them with allowed properties. Such checks simplify debugging, especially in the early stages. 16 | 17 | ### Managing Error Display 18 | 19 | When creating a game, you can enable or disable error output: 20 | 21 | ```javascript 22 | const game = new Game( 23 | null, // width 24 | null, // height 25 | null, // canvasId 26 | true, // displayErrors — enabled 27 | 'ru' // message language 28 | ); 29 | ``` 30 | 31 | ### Recommendation 32 | 33 | In the production build, set `displayErrors = false` to remove debugging overhead and speed up execution. 34 | 35 | --- 36 | 37 | ## 2. Debugging Colliders 38 | 39 | For visual debugging of collisions, ScrubJS allows **highlighting active colliders** of sprites and scenes. 40 | 41 | ### Example of enabling collision debugging: 42 | 43 | ```javascript 44 | game.debugCollider = true; 45 | game.debugColor = '#00FF0077'; // Debug border color (green with transparency) 46 | ``` 47 | 48 | After this, each sprite with an active collider will display its **collision boundary** over the graphics. 49 | 50 | --- 51 | 52 | ## 3. `debugMode` Property 53 | 54 | The `debugMode` property controls the mode for displaying additional information about sprites (e.g., position, size, name, etc.). 55 | 56 | ### Possible values: 57 | 58 | - `'none'` — debug mode is disabled 59 | - `'hover'` — information appears when hovering over a sprite 60 | - `'forever'` — information is always displayed 61 | 62 | ### Example: 63 | 64 | ```javascript 65 | game.debugMode = 'hover'; // Display debug info on hover 66 | ``` 67 | 68 | Useful if you need to track the behavior of a specific object on the scene. 69 | 70 | --- 71 | 72 | ## 4. Improving Performance 73 | 74 | To ensure the game runs smoothly even on weak devices, follow several recommendations: 75 | 76 | ### Avoid redundant calculations in `forever()` 77 | 78 | ```javascript 79 | // Negative example: 80 | sprite.forever(() => { 81 | sprite.drawCostume(...); // New costume every frame — expensive 82 | }); 83 | ``` 84 | 85 | ```javascript 86 | // Good practice: 87 | if (!sprite.hasCostume('cached')) { 88 | sprite.drawCostume(...); 89 | } 90 | ``` 91 | 92 | ### Minimize the number of actively rendered sprites 93 | 94 | Use `sprite.hidden = true` for objects off-screen to reduce GPU load. 95 | 96 | ### Use `pen()` for drawing backgrounds and trails 97 | 98 | Instead of creating new objects every frame, draw static elements using `stage.pen()`. 99 | 100 | ### Disable debugging before publishing 101 | 102 | ```javascript 103 | game.debugMode = 'none' 104 | game.debugCollider = false; 105 | game.displayErrors = false; 106 | ``` 107 | -------------------------------------------------------------------------------- /docs/en/distance.md: -------------------------------------------------------------------------------- 1 | # Determining the Distance Between a Sprite and Another Object 2 | 3 | ## Method getDistanceTo() 4 | 5 | Returns the distance in pixels between the current sprite and the specified object, using their global coordinates. Useful for determining the distance between objects, controlling enemy AI, and activating events at a distance. 6 | 7 | ```javascript 8 | getDistanceTo(object: TransformableObject): number 9 | ``` 10 | 11 | ### Parameters: 12 | * `object` (`TransformableObject`) - The object to which the distance is measured. Can be: - A Sprite (`Sprite`) - An object with coordinates `{ x: number, y: number }` 13 | 14 | ### Return value: 15 | - `number` — the straight-line distance in pixels between the centers of the objects. 16 | 17 | ### How does it work? 18 | **Calculates the global coordinates** of the current sprite and the target object, taking into account nesting in parent elements. 19 | 20 | ### Usage Examples 21 | 22 | #### Basic example: 23 | ```javascript 24 | const player = new Sprite(stage); 25 | const enemy = new Sprite(stage); 26 | enemy.x = 100; 27 | enemy.y = 100; 28 | 29 | // Get the distance between the player and the enemy 30 | const distance = player.getDistanceTo(enemy); 31 | console.log(`Distance: ${distance}px`); 32 | ``` 33 | 34 | #### Using for enemy AI: 35 | ```javascript 36 | enemy.forever(() => { 37 | const dist = enemy.getDistanceTo(player); 38 | 39 | if (dist { 40 | const allEnemies = stage.getSprites().filter(s => s.hasTag('enemy')); 41 | 42 | allEnemies.forEach(enemy => { 43 | const dist = radar.getDistanceTo(enemy); 44 | if (dist < 300) { 45 | enemy.filter = 'brightness(1.5)'; // Highlight nearby enemies 46 | } 47 | }); 48 | }); 49 | ``` 50 | 51 | ### Features 52 | 53 | #### Global Coordinates: 54 | 55 | If objects are nested in parent sprites, the method automatically takes their transformations into account: 56 | 57 | ```javascript 58 | const parent = new Sprite(stage); 59 | parent.x = 50; 60 | 61 | const child = new Sprite(stage); 62 | child.setParent(parent); 63 | child.x = 30; // Global X coordinate = 50 + 30 = 80 64 | 65 | console.log(child.getDistanceTo({x: 0, y: 0})); // Will output 80 66 | ``` 67 | 68 | #### Does not consider shape and colliders: 69 | 70 | The distance is measured between the **geometric centers** of the objects, even if they have a complex shape: 71 | 72 | ```javascript 73 | // For precise collisions, use touchSprite() 74 | if (sprite.touchSprite(other)) { 75 | // Handling the touch 76 | } 77 | ``` 78 | -------------------------------------------------------------------------------- /docs/en/drawing.md: -------------------------------------------------------------------------------- 1 | # Drawing Capabilities in ScrubJS 2 | 3 | This section covers tools for creating graphics "on the fly", working with the canvas, and visual effects. ScrubJS provides a simple and flexible API for drawing using the built-in `CanvasRenderingContext2D` context. 4 | 5 | --- 6 | 7 | ## 1. Dynamic Costumes 8 | 9 | ### Method `drawCostume()` 10 | 11 | Allows creating custom costumes directly in the code using the Canvas API. This approach is ideal for generative graphics, UI elements, and effects. 12 | 13 | ```javascript 14 | const sprite = new Sprite(stage); 15 | sprite.drawCostume((context) => { 16 | context.fillStyle = '#FF5733'; 17 | context.beginPath(); 18 | context.arc(32, 32, 30, 0, Math.PI * 2); 19 | context.fill(); 20 | 21 | context.strokeStyle = 'white'; 22 | context.lineWidth = 3; 23 | context.stroke(); 24 | }, { 25 | width: 64, 26 | height: 64, 27 | name: 'custom_circle' 28 | }); 29 | ``` 30 | 31 | **Features:** 32 | - `context` is the standard Canvas 2D context. 33 | - `width` and `height` define the size of the costume canvas. 34 | - The costume can be reused by setting `name`. 35 | - The collider is automatically calculated based on the image boundaries but can be overridden manually. 36 | 37 | --- 38 | 39 | ## 2. Permanent Drawing 40 | 41 | Drawing can occur not only in costumes but also directly on the stage or following a sprite. 42 | 43 | ### Method `pen()` for Sprites 44 | 45 | Allows drawing trails, effects, animations, and complex ornaments while moving across the stage. 46 | 47 | ```javascript 48 | const brush = new Sprite(stage); 49 | brush.pen((context, sprite) => { 50 | context.fillStyle = `hsl(${Date.now() % 360}, 70%, 50%)`; 51 | context.beginPath(); 52 | context.arc(sprite.x, sprite.y, 10, 0, Math.PI * 2); 53 | context.fill(); 54 | }); 55 | 56 | brush.forever(() => { 57 | const mouse = game.getMousePoint(); 58 | brush.x = mouse.x; 59 | brush.y = mouse.y; 60 | }); 61 | ``` 62 | 63 | ### Drawing Directly on the Stage 64 | 65 | Suitable for creating effects, backgrounds, special effects, interfaces, and visualizations. 66 | 67 | ```javascript 68 | stage.pen((context, stage) => { 69 | // Gradient background 70 | const gradient = context.createLinearGradient(0, 0, stage.width, 0); 71 | gradient.addColorStop(0, '#1A2980'); 72 | gradient.addColorStop(1, '#26D0CE'); 73 | context.fillStyle = gradient; 74 | context.fillRect(0, 0, stage.width, stage.height); 75 | 76 | // Animated circles 77 | context.fillStyle = 'rgba(255, 255, 255, 0.1)'; 78 | context.beginPath(); 79 | context.arc( 80 | Math.sin(Date.now() / 1000) * 200 + 400, 81 | 300, 82 | 50, 83 | 0, 84 | Math.PI * 2 85 | ); 86 | context.fill(); 87 | }); 88 | ``` 89 | 90 | --- 91 | 92 | ## 3. Working with Stamps 93 | 94 | ### Static Impressions of Sprites 95 | 96 | The `stamp()` method leaves an image of the current costume of the sprite directly on the stage. 97 | 98 | ```javascript 99 | const stampSprite = new Sprite(stage); 100 | stampSprite.addCostume('icon.png'); 101 | 102 | stampSprite.onClick(() => { 103 | stampSprite.stamp(); // Normal stamp 104 | stampSprite.stamp(0, true); // With rotation 105 | }); 106 | ``` 107 | 108 | ### Stamping Images on the Stage 109 | 110 | You can directly apply any images to the stage without using sprites. 111 | 112 | ```javascript 113 | const image = new Image(); 114 | image.src = 'particle.png'; 115 | 116 | stage.forever(() => { 117 | if (game.mouseDown()) { 118 | stage.stampImage( 119 | image, 120 | game.getMousePoint().x, 121 | game.getMousePoint().y, 122 | game.getRandom(0, 360) // random rotation angle 123 | ); 124 | } 125 | }); 126 | ``` 127 | 128 | --- 129 | 130 | ## 4. Common Mistakes 131 | 132 | ### Memory Leaks with Dynamic Drawing 133 | 134 | If you create a new costume every frame, memory is quickly consumed: 135 | 136 | ```javascript 137 | // Incorrect: 138 | sprite.forever(() => { 139 | sprite.drawCostume(...); // A new object every frame 140 | }); 141 | 142 | // Correct: 143 | if (!sprite.hasCostume('dynamic')) { 144 | sprite.drawCostume(...); 145 | } 146 | ``` 147 | 148 | ### Ignoring the Layer System 149 | 150 | If you use `stage.pen()` and do not specify a layer, the drawing may overlap important elements. 151 | 152 | ```javascript 153 | // Overlapping sprites: 154 | stage.pen((context) => { 155 | context.fillStyle = 'black'; 156 | context.fillRect(0, 0, stage.width, stage.height); 157 | }, 999); // Explicitly on the topmost layer 158 | ``` 159 | 160 | ### Incorrect Coordinates 161 | 162 | Drawing can occur in local or global coordinates. It is important to distinguish: 163 | 164 | ```javascript 165 | sprite.pen((context) => { 166 | context.fillRect(0, 0, 50, 50); // locally (relative to the sprite) 167 | 168 | context.fillRect(sprite.globalX, sprite.globalY, 50, 50); // globally (on the stage) 169 | }); 170 | ``` 171 | -------------------------------------------------------------------------------- /docs/en/game_loop.md: -------------------------------------------------------------------------------- 1 | # Game Loop 2 | 3 | The game loop in ScrubJS ensures continuous code execution, similar to the `forever` block in Scratch. It "animates" sprites and scenes by defining actions that execute every frame. 4 | 5 | ## Core Concept 6 | 7 | Instead of directly using `requestAnimationFrame`, ScrubJS provides convenient methods `forever`, `repeat`, and `timeout` for managing the game loop. 8 | 9 | ```javascript 10 | sprite.forever(() => { 11 | sprite.x += 1; // The sprite moves to the right every frame 12 | }); 13 | ``` 14 | 15 | This function will be automatically called on every screen update. 16 | 17 | ## Managing Game Loops 18 | 19 | The `forever`, `repeat`, and `timeout` methods are available for both stages (`Stage`) and sprites (`Sprite`), enabling flexible logic configuration. 20 | 21 | ### Forever: Continuous Execution 22 | 23 | `forever` runs a function continuously while the stage is active or the sprite exists. Ideal for persistent actions like movement, animation, or condition checks. 24 | 25 | **Parameters:** 26 | 27 | * `callback`: Function to execute on each iteration. 28 | * `interval` (optional): Interval in milliseconds between executions. If omitted, the function runs every frame (as fast as possible). 29 | * `timeout` (optional): Delay in milliseconds before the *first* execution. 30 | * `finishCallback` (optional): Function called *after* the loop stops (e.g., when the sprite is deleted). 31 | 32 | ### Repeat: Fixed Iteration Loop 33 | 34 | `repeat` runs a function a specified number of times. Suitable for repeated actions like animation sequences or effects. 35 | 36 | **Parameters:** 37 | 38 | * `callback`: Function to execute on each iteration. 39 | * `repeat`: Number of repetitions. 40 | * `interval` (optional): Interval in milliseconds between iterations. If omitted, the function runs as fast as possible. 41 | * `timeout` (optional): Delay before the *first* execution. 42 | * `finishCallback` (optional): Function called after the loop completes. 43 | 44 | ### Timeout: Single Execution with Delay 45 | 46 | `timeout` executes a function *once* after a specified delay. Useful for delayed actions like displaying messages or triggering effects after a pause. 47 | 48 | **Parameters:** 49 | 50 | * `callback`: Function to execute after the delay. 51 | * `timeout`: Delay in milliseconds before execution. 52 | 53 | ### Where to Use: 54 | 55 | * **Stage:** For global game logic like background management, effects, or input handling. 56 | * **Sprite:** For sprite-specific logic like movement, animation, or interactions. 57 | 58 | **Important Notes:** 59 | 60 | * All loops automatically stop when the scene changes. They resume upon returning to the scene. 61 | * Deleting a sprite stops all its active loops. 62 | 63 | ### Examples: 64 | 65 | ```javascript 66 | stage.forever(() => { 67 | console.log("It runs continuously with a frequency of 100 ms, the first run after 2 seconds."); 68 | }, 100, 2000); 69 | 70 | stage.repeat(() => { 71 | console.log("It will run 10 times with a frequency of 200 ms. First launch in 1 second."); 72 | }, 10, 200, 1000, () => { 73 | console.log("Will be executed at the end of the cycle."); 74 | }); 75 | 76 | stage.timeout(() => { 77 | console.log("It will run once in 3 seconds."); 78 | }, 3000); 79 | ``` 80 | 81 | ## Context and Loop State Management 82 | 83 | ### Loop Context 84 | 85 | Inside `forever`, `repeat`, and `timeout` functions, you can access: 86 | 87 | * `this`: The object the loop is bound to (stage or sprite). 88 | * `ScheduledState` (for `forever` and `repeat`): Object for loop control. 89 | 90 | ### ScheduledState: Internal and External Loop Control 91 | 92 | `ScheduledState` is returned when creating `forever` and `repeat` loops, providing properties for loop management. 93 | 94 | **Properties:** 95 | 96 | * `interval`: Interval between iterations in milliseconds. 97 | * `maxIterations`: Total iterations for `repeat` loops. 98 | * `currentIteration`: Current iteration for `repeat` loops. 99 | 100 | ### Loop Control Examples: 101 | 102 | 103 | ```javascript 104 | // Stopping the loop 105 | stage.forever((stage, state) => { 106 | return false; 107 | }); 108 | 109 | // Loop acceleration 110 | stage.forever((stage, state) => { 111 | state.interval -= 1; 112 | }); 113 | 114 | // Passing parameters to the loop 115 | const animationState = stage.forever((stage, state) => { 116 | player.nextCostume(); 117 | 118 | if (state.control === 'fast') { 119 | state.interval = 100; 120 | } 121 | 122 | if (state.control === 'slow') { 123 | state.interval = 250; 124 | } 125 | 126 | if (state.control === 'stop') { 127 | return false; 128 | } 129 | }); 130 | 131 | stage.forever(() => { 132 | if (game.keyPressed('d')) { 133 | animationState.control = 'fast'; 134 | } 135 | 136 | if (game.keyPressed('a')) { 137 | animationState.control = 'slow'; 138 | } 139 | 140 | if (game.keyPressed('space')) { 141 | animationState.control = 'stop'; 142 | } 143 | }); 144 | ``` 145 | 146 | ## Multiple Loops per Object 147 | 148 | You can create multiple loops for a single object. They run independently, enabling complex behaviors. 149 | 150 | ```javascript 151 | sprite.forever(() => { 152 | sprite.x += 1; // Move right 153 | }); 154 | 155 | sprite.forever(() => { 156 | sprite.direction += 1; // Rotate 157 | }); 158 | ``` 159 | 160 | ## Example: Basic Player Control 161 | 162 | ```javascript 163 | stage.forever(() => { 164 | if (game.keyPressed('d')) { 165 | sprite.x += 5; // Move right on 'd' press 166 | } 167 | 168 | if (game.keyPressed('a')) { 169 | sprite.x -= 5; // Move left on 'a' press 170 | } 171 | }); 172 | ``` 173 | 174 | ## Stopping All Object Loops 175 | 176 | To forcibly stop all loops associated with an object (sprite or stage), use the `stop()` method: 177 | 178 | ```javascript 179 | sprite.stop(); // Stop all sprite loops 180 | ``` 181 | 182 | ## How It Works 183 | 184 | All loops (`forever`, `repeat`, `timeout`) are registered in ScrubJS's timing system. Each game frame, the system invokes their callbacks. You don’t need to manage this manually—everything happens automatically. 185 | -------------------------------------------------------------------------------- /docs/en/layers.md: -------------------------------------------------------------------------------- 1 | # Layer Management 2 | 3 | Each sprite in ScrubJS can be drawn on a specific layer. Layers allow precise control over the order in which objects are displayed on the screen: what is in the foreground and what is in the background. This is especially important for creating a scene with a background, characters, effects, and UI elements. 4 | 5 | ## How the Layer System Works 6 | 7 | Each sprite can be assigned a numerical priority through the `.layer` property. The **higher the value**, the **higher** it will be in the rendering hierarchy. 8 | 9 | ```javascript 10 | const background = new Sprite(stage); 11 | const player = new Sprite(stage); 12 | const effects = new Sprite(stage); 13 | 14 | background.layer = 0; // The farthest background 15 | player.layer = 1; // The main character 16 | effects.layer = 2; // Visual effects on top of the player 17 | ``` 18 | 19 | By default, all sprites have `layer = 0`, so it is recommended to explicitly set the order when creating a scene. 20 | 21 | ## Dynamic Layer Changes 22 | 23 | Layers can be changed at any time, for example, to create the effect of "jumping to the foreground" or "appearing a pop-up window". 24 | 25 | ### Example: Player jumps higher — layer increases 26 | 27 | ```javascript 28 | player.forever(() => { 29 | if (player.y < 200) { 30 | player.layer = 3; // Raise to the foreground 31 | } else { 32 | player.layer = 1; // Return to the normal level 33 | } 34 | }); 35 | ``` 36 | 37 | This is especially useful for simulating depth in 2D — for example, when a character can "go behind" an object. 38 | 39 | ## Important Notes 40 | 41 | - When changing `.layer`, you do not need to recreate the sprite — the changes take effect immediately. 42 | - If multiple sprites are on the same layer, the order in which they are created determines the order in which they are drawn. 43 | - `stage.render()` automatically sorts sprites by `layer` before drawing each frame. 44 | 45 | ## Summary 46 | 47 | | Layer Value | Purpose | 48 | |-------------|-----------------------------| 49 | | 0 | Background, decorations | 50 | | 1–2 | Players, enemies | 51 | | 3–4 | Foreground objects | 52 | | 5–9 | Visual effects, flashes | 53 | | 10+ | Interface, menu | 54 | 55 | -------------------------------------------------------------------------------- /docs/en/main_objects.md: -------------------------------------------------------------------------------- 1 | # Core Game Objects 2 | 3 | In ScrubJS, everything starts with three main entities: `Game`, `Stage`, and `Sprite`. They form the foundation of a project's architecture and are used in all games. 4 | 5 | ## Game Class 6 | 7 | The `Game` class initializes the game. It handles canvas creation, stage management, and game startup. 8 | 9 | [Learn more about the Game object](game.md) 10 | 11 | ## Stage Class 12 | 13 | The `Stage` class represents a scene in the game—equivalent to a screen or level. It contains sprites, backgrounds, and manages their rendering. 14 | 15 | [Learn more about Stages](stage.md) 16 | 17 | ## Sprite Class 18 | 19 | The `Sprite` class represents a game object that can move, change appearance, and interact with other objects. Each sprite can have multiple costumes, sounds, and child elements. 20 | 21 | [Learn more about the Sprite object](../en/sprite.md) 22 | 23 | ## Relationships Between Game, Stage, and Sprite 24 | 25 | - `Game` manages all `Stage` instances. 26 | - Each `Stage` contains multiple `Sprite` objects. 27 | - Each `Sprite` knows its parent `Stage` and can access the `Game` through it. 28 | 29 | ## General Recommendations 30 | 31 | The `Game`, `Stage`, and `Sprite` classes offer extensive customization for object behavior and appearance. However, some methods and properties become available only after full loading (`ready`). For example, you cannot select a costume or play a sound before loading completes. 32 | 33 | Game loop methods (`forever`, `repeat`) only start working after the game is fully ready. A game is considered ready when all stages are completely loaded. A stage becomes ready when all images and sounds are loaded, including resources for all its sprites. 34 | -------------------------------------------------------------------------------- /docs/en/movement.md: -------------------------------------------------------------------------------- 1 | # Coordinates, Rotations, Movements 2 | 3 | This guide explains the full cycle of working with positioning, orientation, and moving sprites in ScrubJS. 4 | 5 | ## Contents 6 | * [Coordinate System](#coordinate-system) 7 | * [Sprite Boundaries](#sprite-boundaries) 8 | * [Rotations and Direction](#rotations-and-direction) 9 | * [Basic Movement](#basic-movement) 10 | 11 | --- 12 | 13 | ## Coordinate System 14 | 15 | ### Basic Principles 16 | - The coordinates `(x, y)` define the **center of the sprite**. 17 | - The X-axis: increases to the right 18 | - The Y-axis: increases downwards 19 | 20 | Coordinates can be modified directly: 21 | 22 | ```javascript 23 | sprite.x += 5; // movement to the right 24 | sprite.y -= 3; // movement upwards 25 | ``` 26 | 27 | ### Local vs Global Coordinates 28 | 29 | For child sprites, global and local coordinates differ. For sprites without parents, they are equal. 30 | 31 | ```javascript 32 | const parent = new Sprite(stage); 33 | parent.x = 200; 34 | 35 | const child = new Sprite(stage); 36 | child.setParent(parent); 37 | child.x = 50; 38 | 39 | console.log(child.globalX); // 250 (200 + 50) 40 | ``` 41 | 42 | **Properties:** 43 | - `x`, `y` - local coordinates 44 | - `globalX`, `globalY` - global coordinates considering parents 45 | 46 | ### Practical Examples 47 | 48 | #### Centering a Sprite: 49 | ```javascript 50 | sprite.onReady(() => { 51 | sprite.x = stage.width/2 - sprite.width/2; 52 | sprite.y = stage.height/2 - sprite.height/2; 53 | }); 54 | ``` 55 | 56 | #### Group Movement: 57 | ```javascript 58 | const parent = new Sprite(stage); 59 | const child = new Sprite(stage); 60 | child.setParent(parent); 61 | 62 | parent.forever(() => { 63 | parent.x += 2; 64 | // Child sprites move with the parent 65 | }); 66 | ``` 67 | 68 | #### Smooth Movement: 69 | ```javascript 70 | const targetX = 400; 71 | sprite.forever(() => { 72 | sprite.x += (targetX - sprite.x) * 0.1; // Smooth approach 73 | }); 74 | ``` 75 | 76 | --- 77 | 78 | ## Sprite Boundaries 79 | 80 | ### Boundary Properties 81 | | Property | Formula | Description | 82 | |------------|-----------------------------------|-----------------------------| 83 | | `rightX` | `x + width/2 + colliderOffsetX` | Right boundary | 84 | | `leftX` | `x - width/2 + colliderOffsetX` | Left boundary | 85 | | `topY` | `y - height/2 + colliderOffsetY` | Top boundary | 86 | | `bottomY` | `y + height/2 + colliderOffsetY` | Bottom boundary | 87 | 88 | ### Usage Examples 89 | 90 | #### Handling Screen Edges 91 | ```javascript 92 | // Keeping the sprite within the stage bounds 93 | sprite.forever(() => { 94 | sprite.leftX = Math.max(sprite.leftX, 0); 95 | sprite.rightX = Math.min(sprite.rightX, stage.width); 96 | sprite.topY = Math.max(sprite.topY, 0); 97 | sprite.bottomY = Math.min(sprite.bottomY, stage.height); 98 | }); 99 | ``` 100 | 101 | #### Platformer: Ground Check 102 | ```javascript 103 | const isGrounded = () => { 104 | return sprite.bottomY >= ground.topY - 1; 105 | }; 106 | ``` 107 | 108 | #### Alignment: 109 | ```javascript 110 | // Aligning with the top of a platform 111 | player.bottomY = platform.topY; 112 | ``` 113 | 114 | ### Features: 115 | 116 | - Consider the active collider: 117 | 118 | ```javascript 119 | sprite.setRectCollider('main', 100, 50, 20, 0); 120 | console.log(sprite.rightX); // x + 50 + 20 121 | ``` 122 | 123 | - For polygonal colliders, values are approximate 124 | - Do not depend on the visual pivot (`pivotOffset`) 125 | 126 | --- 127 | 128 | ## Rotations and Direction 129 | 130 | ### Direction Property 131 | 132 | Sprite direction is controlled through the `direction` property. 133 | 134 | ```javascript 135 | // Smooth rotation 136 | sprite.forever(() => { 137 | sprite.direction += 1.5; // 1.5° per frame 138 | }); 139 | 140 | // Sharp turn 141 | sprite.direction += 180; 142 | ``` 143 | 144 | **Features:** 145 | 146 | * Angles are specified in degrees, clockwise. 147 | * 0° — up, 90° — right, 180° — down, 270° — left. 148 | * Works with fractional numbers (e.g., move(1.5)). 149 | 150 | ### `pointForward()` Method 151 | 152 | Automatic rotation towards a target. Works with any object having `x` and `y` coordinates. 153 | 154 | ```javascript 155 | const target = new Sprite(stage); // or const target = {x: 300, y: 200}; 156 | target.x = 300; 157 | 158 | sprite.forever(() => { 159 | sprite.pointForward(target); 160 | sprite.move(3); // Move towards the target 161 | }); 162 | ``` 163 | 164 | **Tip:** For smooth rotation, use linear interpolation: 165 | ```javascript 166 | const targetAngle = Math.atan2(target.y - sprite.y, target.x - sprite.x) * 180/Math.PI; 167 | sprite.direction += (targetAngle - sprite.direction) * 0.1; 168 | ``` 169 | 170 | --- 171 | 172 | ## Basic Movement 173 | 174 | ### `move()` Method 175 | 176 | Moves the sprite in its current direction. 177 | 178 | ```javascript 179 | sprite.direction = 60; // Angle of 60° 180 | sprite.move(10); // 10 pixels in that direction 181 | ``` 182 | 183 | Uses trigonometry to calculate displacement, equivalent to: 184 | ```javascript 185 | const radians = sprite.direction * Math.PI / 180; 186 | sprite.x += Math.cos(radians) * 10; 187 | sprite.y += Math.sin(radians) * 10; 188 | ``` 189 | 190 | ### Example: Movement with Bounces 191 | 192 | ```javascript 193 | sprite.forever(() => { 194 | sprite.move(5); 195 | sprite.bounceOnEdge(); 196 | }); 197 | ``` 198 | 199 | --- 200 | 201 | > Full information about all sprite geometry properties and methods can be found in the [Sprite Object](sprite.md#geometry) section. 202 | -------------------------------------------------------------------------------- /docs/en/multi_scene.md: -------------------------------------------------------------------------------- 1 | # Game with Multiple Scenes 2 | 3 | ScrubJS supports creating games with multiple scenes (stages), such as a main menu, game level, victory screen, etc. Each scene can contain its own objects, logic, and interface. This section describes how to create, switch, and manage scenes within a single game process. 4 | 5 | ## Creating and Managing Scenes 6 | 7 | ### Basic Example 8 | 9 | In this example, two scenes are created: **main menu** and **game scene**. A start button is located in the menu, and when clicked, it switches the game to the main game level. 10 | 11 | ```javascript 12 | const game = new Game(); 13 | const menuStage = new Stage(); 14 | const gameStage = new Stage(); 15 | 16 | // Start button 17 | const startButton = new Sprite(menuStage); 18 | startButton.addCostume('start.png'); 19 | 20 | // Player 21 | const player = new Sprite(gameStage); 22 | player.addCostume('player.png'); 23 | 24 | // Transition to the game scene on button click 25 | menuStage.forever(() => { 26 | if (startButton.touchMouse() && game.mouseDownOnce()) { 27 | game.run(gameStage); 28 | } 29 | }); 30 | 31 | // Start the game with the menu 32 | game.run(menuStage); 33 | ``` 34 | -------------------------------------------------------------------------------- /docs/en/pivot.md: -------------------------------------------------------------------------------- 1 | # Pivot Point 2 | 3 | ## Method setPivotOffset() 4 | 5 | Allows you to change the point around which the sprite rotates and scales. 6 | 7 | ```javascript 8 | setPivotOffset(x: number = 0, y: number = 0): this 9 | ``` 10 | 11 | **Parameters:** 12 | 13 | * `x` (`number`, optional, default `0`) - offset of the center along the X-axis. 14 | * `y` (`number`, optional, default `0`) - offset of the center along the Y-axis. 15 | 16 | ### How does it work? 17 | - **By default,** the sprite rotates around its geometric center. 18 | - **The offset** is specified relative to the original center of the sprite: 19 | - Positive `x` — shift to the right 20 | - Negative `x` — shift to the left 21 | - Positive `y` — shift downwards 22 | - Negative `y` — shift upwards 23 | 24 | ### Usage Examples 25 | 26 | #### Rotation around the edge of the sprite 27 | ```javascript 28 | const windmill = new Sprite(stage); 29 | windmill.setPivotOffset(-50, 0); // The center is shifted 50px to the left 30 | 31 | windmill.forever(() => { 32 | windmill.direction += 2; // Rotation around the left edge 33 | }); 34 | ``` 35 | 36 | #### Orbital motion 37 | ```javascript 38 | const planet = new Sprite(stage); 39 | const star = new Sprite(stage); 40 | 41 | planet.setPivotOffset(100, 0); // The center is shifted 100px to the right 42 | planet.setParent(star); // The planet becomes a child of the star 43 | 44 | star.forever(() => { 45 | star.direction += 1; // The planet rotates around the star 46 | }); 47 | ``` 48 | 49 | #### Swinging lantern 50 | ```javascript 51 | const lantern = new Sprite(stage); 52 | lantern.setPivotOffset(0, -30); // The center is 30px above the sprite 53 | 54 | lantern.forever(() => { 55 | lantern.direction = Math.sin(Date.now() / 300) * 30; // -30° to +30° 56 | }); 57 | ``` 58 | 59 | ### Features 60 | 61 | #### Impact on child objects 62 | ```javascript 63 | const car = new Sprite(stage); 64 | const wheel = new Sprite(stage); 65 | 66 | car.setPivotOffset(0, 20); // Shift the center of the car 67 | car.addChild(wheel); // The wheel inherits the transformations of the parent 68 | 69 | // The wheel will rotate relative to the new center of the car 70 | ``` 71 | 72 | #### Interaction with colliders 73 | The pivot point **does not affect** colliders. The physical boundaries remain the same: 74 | ```javascript 75 | sprite.setPivotOffset(50, 0); 76 | sprite.setRectCollider('main', 100, 100); // The collider remains centered 77 | ``` 78 | 79 | ### Common mistakes 80 | 81 | #### Confusion with the coordinate system 82 | ```javascript 83 | // Incorrect: offset by 50px down 84 | sprite.setPivotOffset(0, 50); 85 | 86 | // Correct: to shift down, use positive Y 87 | // (Y increases downwards in the canvas coordinate system) 88 | ``` 89 | 90 | #### Ignoring parent transformations 91 | ```javascript 92 | const parent = new Sprite(stage); 93 | parent.x = 100; 94 | 95 | const child = new Sprite(stage); 96 | child.setParent(parent); 97 | child.setPivotOffset(50, 0); // Offset relative to the parent 98 | ``` 99 | 100 | --- 101 | 102 | > Full information about all sprite geometry properties and methods can be found in the [Sprite Object](sprite.md#geometry) section. 103 | 104 | -------------------------------------------------------------------------------- /docs/en/sounds.md: -------------------------------------------------------------------------------- 1 | # Sounds in the Game 2 | 3 | This section describes how to add, play, and manage sounds and music in ScrubJS. The library provides a simple interface for working with sound effects. 4 | 5 | ## 1. Basic Sound Management 6 | 7 | ### Adding and Playing 8 | 9 | To start, you need to load a sound and assign it a name by which it can be played: 10 | 11 | ```javascript 12 | const player = new Sprite(stage); 13 | player.addSound('jump.wav', 'jump'); 14 | 15 | stage.forever(() => { 16 | if (game.keyPressed('space')) { 17 | player.playSoundByName('jump'); 18 | } 19 | }); 20 | ``` 21 | 22 | ### Using Sound Indices 23 | 24 | You can add a sound without specifying a name; in this case, you will need to use its index to play it: 25 | 26 | ```javascript 27 | const player = new Sprite(stage); 28 | player.addSound('jump.wav'); 29 | player.playSound(0); 30 | ``` 31 | 32 | ### Dynamic Volume 33 | 34 | The volume of a sound can depend, for example, on the distance to the player: 35 | 36 | ```javascript 37 | const enemy = new Sprite(stage); 38 | enemy.forever(() => { 39 | const distance = player.getDistanceTo(enemy); 40 | const volume = Math.max(0, 1 - distance / 500); 41 | enemy.sounds.get('alert').volume = volume; 42 | }); 43 | ``` 44 | 45 | ### Sounds on the Stage 46 | 47 | The stage object also supports the same sound management capabilities. 48 | 49 | Example of playing a sound by the stage with specified volume and starting position (in seconds): 50 | 51 | ```javascript 52 | const stage = new Stage(); 53 | stage.addSound('background.mp3', 'bg_music'); 54 | stage.playSoundByName('bg_music', 0.3, 5.0); // Volume at 30%, starting from the 5th second 55 | ``` 56 | 57 | --- 58 | 59 | > Full information about the properties and methods for managing sprite sounds can be found in the [Sprite Game Object](sprite.md#sounds) section. 60 | 61 | -------------------------------------------------------------------------------- /docs/en/visual_effects.md: -------------------------------------------------------------------------------- 1 | # Visual Effects: Transparency and CSS Filters in ScrubJS 2 | 3 | This section explains how to work with transparency and CSS filters to create complex visual effects. 4 | 5 | ## 1. Managing Transparency 6 | 7 | ### Basic Use of the `opacity` Property 8 | ```javascript 9 | const ghost = new Sprite(stage); 10 | ghost.opacity = 0.5; // 50% transparency 11 | ``` 12 | 13 | ### Smooth Disappearance of an Object 14 | ```javascript 15 | ghost.forever(() => { 16 | ghost.opacity -= 0.01; 17 | 18 | if(ghost.opacity { 19 | if (player.touchSprite(enemy)) { 20 | enemy.delete(); 21 | 22 | player.repeat((sprite, state) => { 23 | sprite.opacity = state.currentIteration % 2 ? 0.3 : 1; 24 | }, 6, 100); // 6 blinks with a 100ms interval 25 | } 26 | }); 27 | ``` 28 | 29 | ## 2. Working with CSS Filters 30 | 31 | ### Basic Filter Types 32 | ```javascript 33 | // Blur 34 | sprite.filter = 'blur(5px)'; 35 | 36 | // Black and white mode 37 | sprite.filter = 'grayscale(100%)'; 38 | 39 | // Color shift 40 | sprite.filter = 'hue-rotate(90deg)'; 41 | 42 | // Shadow 43 | sprite.filter = 'drop-shadow(5px 5px 5px rgba(0,0,0,0.5))'; 44 | ``` 45 | 46 | ### Dynamic Effects 47 | ```javascript 48 | // Smooth color change 49 | sprite.forever(() => { 50 | sprite.filter = `hue-rotate(${Date.now() % 360}deg)`; 51 | }); 52 | 53 | // "Breathing" effect with blur 54 | sprite.forever(() => { 55 | const blur = Math.abs(Math.sin(Date.now()/500)) * 10; 56 | sprite.filter = `blur(${blur}px)`; 57 | }); 58 | ``` 59 | 60 | ## 3. Combining Filters 61 | 62 | ### Multiple Effects 63 | ```javascript 64 | boss.filter = ` 65 | drop-shadow(0 0 10px #FF0000) 66 | contrast(150%) 67 | brightness(0.8) 68 | `; 69 | ``` 70 | 71 | ### Animated Aura Effect 72 | ```javascript 73 | let auraPhase = 0; 74 | boss.forever(() => { 75 | auraPhase += 0.1; 76 | const glowSize = Math.sin(auraPhase) * 5 + 10; 77 | boss.filter = ` 78 | drop-shadow(0 0 ${glowSize}px rgba(255, 0, 0, 0.7)) 79 | brightness(${1 + Math.abs(Math.sin(auraPhase)) * 0.3}) 80 | `; 81 | }); 82 | ``` 83 | 84 | ## 4. Specific Effects 85 | 86 | ### Freeze Effect 87 | ```javascript 88 | const freezeEffect = () => { 89 | sprite.filter = ` 90 | grayscale(80%) 91 | blur(2px) 92 | contrast(120%) 93 | `; 94 | sprite.opacity = 0.8; 95 | }; 96 | ``` 97 | 98 | ### Teleportation Effect 99 | ```javascript 100 | sprite.repeat((s, state) => { 101 | s.opacity = Math.random(); 102 | s.filter = `hue-rotate(${Math.random() * 360}deg)`; 103 | }, 20, 50, 0, () => { 104 | s.filter = 'none'; 105 | }); 106 | ``` 107 | 108 | ## 5. Common Mistakes 109 | 110 | ### Incorrect Filter Syntax 111 | ```javascript 112 | // Incorrect: 113 | sprite.filter = 'blur 5px'; 114 | 115 | // Correct: 116 | sprite.filter = 'blur(5px)'; 117 | ``` 118 | 119 | ### Conflicting Filters 120 | ```javascript 121 | // Unpredictable result: 122 | sprite.filter = 'brightness(2)'; 123 | sprite.filter = 'grayscale(100%)'; // Overwrites the previous 124 | 125 | // Correctly combine: 126 | sprite.filter = 'brightness(2) grayscale(100%)'; 127 | ``` 128 | 129 | ### Ignoring Filter Order 130 | ```javascript 131 | // Different result: 132 | sprite.filter = 'blur(5px) grayscale(100%)'; 133 | sprite.filter = 'grayscale(100%) blur(5px)'; 134 | ``` 135 | -------------------------------------------------------------------------------- /docs/ru/animations.md: -------------------------------------------------------------------------------- 1 | # Костюмы и анимация 2 | 3 | Костюмы — это визуальные образы спрайта, которые могут переключаться, создавая эффект анимации. ScrubJS предоставляет гибкие инструменты для загрузки, настройки и управления костюмами, включая поддержку изображений, спрайт-листов (сеток), динамического рисования и работы с прозрачностью и обрезкой. 4 | 5 | ## Добавление одного костюма с настройками 6 | 7 | Метод `addCostume()` позволяет загрузить изображение и задать параметры отображения. 8 | 9 | ### Простой пример: 10 | ```javascript 11 | sprite.addCostume('images/player_idle.png'); 12 | ``` 13 | 14 | ### Добавление с именем и поворотом: 15 | ```javascript 16 | sprite.addCostume('images/player_idle.png', { 17 | name: 'idle', 18 | rotate: 90 19 | }); 20 | ``` 21 | Здесь спрайт будет повёрнут на 90° при отображении. 22 | 23 | ### Вырезка части изображения: 24 | ```javascript 25 | sprite.addCostume('images/player_sheet.png', { 26 | name: 'head', 27 | x: 32, 28 | y: 0, 29 | width: 32, 30 | height: 32 31 | }); 32 | ``` 33 | Это позволит взять только фрагмент изображения (32×32 пикселя), начиная с координаты (32, 0). 34 | 35 | ### Отражение по горизонтали: 36 | ```javascript 37 | sprite.addCostume('images/player_sheet.png', { 38 | flipX: true 39 | }); 40 | ``` 41 | Это позволит отразить изображение по горизонтали. 42 | 43 | ### Отражение по горизонтали: 44 | ```javascript 45 | sprite.addCostume('images/player_sheet.png', { 46 | flipY: true 47 | }); 48 | ``` 49 | Это позволит отразить изображение по вертикали. 50 | 51 | ### Прозрачность по цвету: 52 | ```javascript 53 | sprite.addCostume('images/enemy.png', { 54 | alphaColor: '#FF00FF', // Цвет фона, который станет прозрачным 55 | alphaTolerance: 15 // Допуск (насколько близкий цвет тоже станет прозрачным) 56 | }); 57 | ``` 58 | Удобно для удаления фонового цвета, например, пурпурного (#FF00FF). 59 | 60 | ### Обрезка костюма: 61 | ```javascript 62 | sprite.addCostume('images/player.png', { 63 | crop: 10 64 | }); 65 | ``` 66 | Удалит по 10 пикселей с каждой стороны изображения. 67 | 68 | ### Обрезка костюма, более точная настройка: 69 | ```javascript 70 | sprite.addCostume('images/player.png', { 71 | cropTop: 5, 72 | cropRight: 10, 73 | cropBottom: 5, 74 | cropLeft: 10 75 | }); 76 | ``` 77 | Удалит сверху и снизу по 5 пикселей, слева и справа по 10. 78 | 79 | --- 80 | 81 | ## Загрузка костюмов из сетки (спрайт-листа) 82 | 83 | Если у вас есть изображение с множеством кадров, например, 4×4: 84 | 85 | ```javascript 86 | sprite.addCostumeGrid('images/player_walk.png', { 87 | cols: 4, 88 | rows: 4, 89 | name: 'walk' 90 | }); 91 | ``` 92 | 93 | Это создаст 16 костюмов с именами `walk0`, `walk1`, ..., `walk15`. 94 | 95 | ### С ограничением количества кадров: 96 | ```javascript 97 | sprite.addCostumeGrid('images/player_walk.png', { 98 | cols: 4, 99 | rows: 4, 100 | limit: 6 101 | }); 102 | ``` 103 | Только первые 6 кадров будут добавлены. 104 | 105 | ### Пропуск кадров (offset): 106 | ```javascript 107 | sprite.addCostumeGrid('images/player_walk.png', { 108 | cols: 4, 109 | rows: 4, 110 | offset: 4, 111 | limit: 4 112 | }); 113 | ``` 114 | Пропускаем первые 4 кадра и добавляем следующие 4. 115 | 116 | --- 117 | 118 | ## Создание костюма с помощью кода 119 | 120 | Можно рисовать костюмы вручную: 121 | 122 | ```javascript 123 | sprite.drawCostume((ctx) => { 124 | ctx.fillStyle = 'red'; 125 | ctx.fillRect(0, 0, 50, 50); 126 | }, { width: 50, height: 50, name: 'red-square' }); 127 | ``` 128 | 129 | Используйте, когда нужен простой визуал без загрузки изображений. 130 | 131 | ### Пример с кругом: 132 | ```javascript 133 | sprite.drawCostume((ctx) => { 134 | ctx.fillStyle = 'blue'; 135 | ctx.beginPath(); 136 | ctx.arc(25, 25, 20, 0, 2 * Math.PI); 137 | ctx.fill(); 138 | }, { width: 50, height: 50, name: 'blue-circle' }); 139 | ``` 140 | 141 | --- 142 | 143 | ## Управление костюмами 144 | 145 | ### Переключение по индексу: 146 | ```javascript 147 | sprite.switchCostume(1); 148 | ``` 149 | 150 | ### Переключение по имени: 151 | ```javascript 152 | sprite.switchCostumeByName('walk3'); 153 | ``` 154 | 155 | ### Следующий костюм (удобно для анимации): 156 | ```javascript 157 | sprite.nextCostume(); // следующий в списке 158 | ``` 159 | 160 | С указанием диапазона: 161 | ```javascript 162 | sprite.nextCostume(4, 7); // цикл между 4 и 7 индексами 163 | ``` 164 | 165 | ### Предыдущий костюм, с указанием диапазона: 166 | ```javascript 167 | sprite.prevCostume(4, 7); // цикл между 7 и 4 индексами 168 | ``` 169 | 170 | ### Удаление костюма: 171 | ```javascript 172 | sprite.removeCostume(0); // удаляем первый костюм 173 | ``` 174 | 175 | --- 176 | 177 | ## Пример анимации из сетки 178 | 179 | ```javascript 180 | const sprite = new Sprite(stage); 181 | sprite.addCostumeGrid('images/player_run.png', { 182 | cols: 6, 183 | rows: 1, 184 | name: 'run' 185 | }); 186 | 187 | sprite.forever(() => { 188 | sprite.nextCostume(); 189 | }, 100); // каждые 100 мс 190 | ``` 191 | 192 | --- 193 | 194 | ## Советы 195 | 196 | - Именование `name` важно для удобства переключения между костюмами. 197 | - Используйте `addCostumeGrid` для загрузки анимаций из спрайт-листов. 198 | - Параметры `alphaColor` и `alphaTolerance` особенно полезны при удалении фонов у PNG/JPG. 199 | - Динамические костюмы (`drawCostume`) позволяют создать уникальные образы на лету. 200 | 201 | --- 202 | 203 | > Полную информацию о свойствах и методах управления костюмами спрайта можно найти в разделе [Игровой объект Sprite](sprite.md#костюмы). 204 | -------------------------------------------------------------------------------- /docs/ru/colliders.md: -------------------------------------------------------------------------------- 1 | # Коллайдеры, касания и теги 2 | 3 | В этом руководстве рассмотрим систему обработки взаимодействий между объектами через коллайдеры, работу с касаниями и группировку объектов с помощью тегов. 4 | 5 | **Важно:** если коллайдер не был добавлен вручную, то он будет создан автоматически из первого добавленного костюма. 6 | 7 | Если вам нужен спрайт без коллайдера удалите его явно: 8 | ```javascript 9 | sprite.removeCollider(); 10 | ``` 11 | 12 | ## 1. Типы коллайдеров 13 | 14 | Коллайдеры определяют зону взаимодействия спрайта. Доступно 4 основных типа: 15 | 16 | ### Прямоугольный коллайдер 17 | ```javascript 18 | const platform = new Sprite(stage); 19 | platform.setRectCollider('main', 100, 30, 0, -15); 20 | // Ширина 100px, высота 30px, смещение на 15px вверх 21 | ``` 22 | 23 | ### Круглый коллайдер 24 | ```javascript 25 | const ball = new Sprite(stage); 26 | ball.setCircleCollider('hitbox', 25); 27 | // Радиус 25px, имя коллайдера 'hitbox' 28 | ``` 29 | 30 | ### Полигональный коллайдер 31 | ```javascript 32 | const triangle = new Sprite(stage); 33 | triangle.setPolygonCollider('main', [ 34 | [0, 0], // Левый верх 35 | [50, 100], // Низ 36 | [100, 0] // Правый верх 37 | ]); 38 | ``` 39 | 40 | ### Автоматическая генерация 41 | ```javascript 42 | const character = new Sprite(stage); 43 | character.addCostume('hero.png'); 44 | // Будет создан прямоугольный коллайдер с именем main автоматически из первого добавленного костюма 45 | ``` 46 | 47 | ### Отключение создания коллайдера 48 | 49 | ```javascript 50 | const character = new Sprite(stage); 51 | character.addCostume('hero.png'); 52 | character.removeCollider(); 53 | // Это отключит автоматическое содание коллайдера из костюма 54 | ``` 55 | 56 | > Подробную информацию о свойствах и методах управления коллайдерам спрайта можно найти в разделе [Игровой объект Sprite](sprite.md#коллайдеры). 57 | 58 | --- 59 | 60 | ## 2. Проверка касаний 61 | 62 | ### Основные методы 63 | 64 | **Касание другого спрайта:** 65 | ```javascript 66 | if (player.touchSprite(enemy)) { 67 | player.damage(); 68 | } 69 | ``` 70 | 71 | **Касание группы объектов:** 72 | ```javascript 73 | const hazards = [spikes, fire, poison]; 74 | if (player.touchSprites(hazards)) { 75 | player.respawn(); 76 | } 77 | ``` 78 | 79 | **Взаимодействие с мышью:** 80 | ```javascript 81 | button.forever(() => { 82 | if (button.touchMouse()) { 83 | button.scale = 1.1; 84 | if (game.mouseDownOnce()) { 85 | startGame(); 86 | } 87 | } 88 | }); 89 | ``` 90 | 91 | ### Проверка границ сцены 92 | 93 | ```javascript 94 | // Общая проверка краев 95 | if (bullet.touchEdge()) { 96 | bullet.destroy(); 97 | } 98 | 99 | // Конкретные направления 100 | if (player.touchBottomEdge()) { 101 | player.jump(); 102 | } 103 | ``` 104 | 105 | ### Работа с координатами 106 | 107 | ```javascript 108 | const clickPoint = new PointCollider(game.mouseX, game.mouseY); 109 | if (map.touchPoint(clickPoint)) { 110 | showTooltip(); 111 | } 112 | ``` 113 | 114 | ### Оптимизация проверок 115 | ```javascript 116 | // Проверка только по главному коллайдеру 117 | if (bullet.touchSprite(target, false)) { 118 | // checkChildren = false 119 | } 120 | ``` 121 | 122 | > Подробную информацию о свойствах и методах проверке касаний спрайта можно найти в разделе [Игровой объект Sprite](sprite.md#касания). 123 | 124 | --- 125 | 126 | ## 3. Система тегов 127 | 128 | Теги позволяют группировать объекты и проверять касания по категориям. 129 | 130 | ### Базовое использование 131 | 132 | **Добавление тега:** 133 | ```javascript 134 | enemy.addTag('danger'); 135 | powerUp.addTag('bonus'); 136 | ``` 137 | 138 | **Проверка группы:** 139 | ```javascript 140 | player.forever(() => { 141 | if (player.touchTag('danger')) { 142 | player.health -= 10; 143 | } 144 | 145 | const bonuses = player.touchTagAll('bonus'); 146 | if (bonuses) { 147 | bonuses.forEach(item => { 148 | player.money += 10; 149 | item.delete(); 150 | }); 151 | } 152 | }); 153 | ``` 154 | 155 | ### Наследование тегов 156 | 157 | Дочерние объекты наследуют теги родителя: 158 | ```javascript 159 | const car = new Sprite(stage); 160 | car.addTag('vehicle'); 161 | 162 | const wheel = new Sprite(stage); 163 | wheel.setParent(car); 164 | 165 | console.log(wheel.hasTag('vehicle')); // true 166 | ``` 167 | 168 | ### Динамическое управление тегами 169 | ```javascript 170 | stage.forever(() => { 171 | if (player.touchTag('key')) { 172 | player.otherSprite.delete(); 173 | door.removeTag('locked'); 174 | } 175 | 176 | if (player.touchTag('locked')) { 177 | console.log('Дверь заперта!'); 178 | } 179 | }); 180 | ``` 181 | 182 | > Подробную информацию о свойствах и методах управления тегам спрайта можно найти в разделе [Игровой объект Sprite](sprite.md#теги). 183 | 184 | --- 185 | 186 | ## 4. Работа с перекрытиями 187 | 188 | Свойства `overlap` помогают реализовать реалистичную физику столкновений. 189 | 190 | **Пример обработки столкновения:** 191 | ```javascript 192 | if (player.touchSprite(wall)) { 193 | // Коррекция позиции 194 | player.x -= player.overlapX; 195 | player.y -= player.overlapY; 196 | 197 | // Визуальная обратная связь 198 | player.tint = '#FF0000'; 199 | setTimeout(() => player.tint = '#FFFFFF', 100); 200 | } 201 | ``` 202 | 203 | --- 204 | 205 | ## 5. Отладка коллайдеров 206 | 207 | **Визуализация:** 208 | ```javascript 209 | // Для всей сцены 210 | game.debugCollider = true; 211 | game.debugColor = '#00FF0077'; // RGBA 212 | ``` 213 | 214 | **Логирование:** 215 | ```javascript 216 | player.forever(() => { 217 | if (player.touchAnySprite()) { 218 | console.log('Столкнулся с:', player.otherSprite.name); 219 | console.log('Глубина:', player.overlap); 220 | } 221 | }); 222 | ``` 223 | 224 | --- 225 | 226 | ## 6. Лучшие практики 227 | 228 | 1. Удаляйте коллайдеры там где они не нужны: 229 | ```javascript 230 | const drop = new Sprite(); 231 | drop.removeCollider(); 232 | ``` 233 | 234 | 2. Отключайте проверку дочерних элементов где не нужно: 235 | ```javascript 236 | touchTag('group', false) 237 | ``` 238 | 239 | 3. Используйте составные спрайты для разных коллайдеров: 240 | - Физики (`body`) 241 | - Зон взаимодействия (`sensor`) 242 | - Атак (`attack`) 243 | 244 | 4. Группируйте объекты через теги: 245 | - `enemy`, `player`, `terrain` 246 | - `coin`, `gem` 247 | 248 | 249 | -------------------------------------------------------------------------------- /docs/ru/composite_sprites.md: -------------------------------------------------------------------------------- 1 | # Составные спрайты 2 | 3 | ScrubJS позволяет создавать **иерархии спрайтов**, в которых один спрайт может быть "родителем", а другие — "дочерними". Это удобно при построении сложных объектов: персонажей с анимацией, техники с вращающимися деталями и любых структур, состоящих из нескольких визуальных элементов. 4 | 5 | ## 1. Создание иерархии объектов 6 | 7 | Вы можете соединять спрайты в составные группы с помощью методов `setParent()` или `addChild()`. Дочерние спрайты автоматически следуют за родительским — как по позиции, так и по повороту и масштабу. 8 | 9 | ### Базовый пример: робот 10 | 11 | ```javascript 12 | const robot = new Sprite(stage); 13 | 14 | const body = new Sprite(stage); 15 | body.setParent(robot); 16 | 17 | const head = new Sprite(stage); 18 | body.setParent(body); 19 | 20 | const armLeft = new Sprite(stage); 21 | body.setParent(body); 22 | 23 | const armRight = new Sprite(stage); 24 | body.setParent(body); 25 | 26 | // Равносильный вариант с addChild(): 27 | // robot.addChild(body); 28 | // body.addChild(head); 29 | // body.addChild(armLeft); 30 | // body.addChild(armRight); 31 | 32 | // Задание позиций относительно родителя 33 | body.y = 0; 34 | head.y = -30; 35 | armLeft.x = -40; 36 | armRight.x = 40; 37 | ``` 38 | 39 | ## 2. Синхронизация преобразований 40 | 41 | Дочерние спрайты **наследуют все трансформации** родителя: движение, поворот, масштаб, прозрачность и т. д. 42 | 43 | ### Согласованное движение и анимация 44 | 45 | ```javascript 46 | robot.forever(() => { 47 | robot.x += 2; // Двигается весь робот вперёд 48 | 49 | // Вращение головы и "махание" рукой 50 | head.rotation += 1; 51 | armLeft.rotation = Math.sin(Date.now() / 300) * 30; 52 | }); 53 | ``` 54 | 55 | ### Локальные и глобальные координаты 56 | 57 | Если дочерний спрайт должен взаимодействовать с миром, можно использовать глобальные координаты: 58 | 59 | ```javascript 60 | const gun = new Sprite(stage); 61 | gun.setParent(robot); 62 | gun.x = 20; 63 | gun.y = -10; 64 | 65 | gun.onClick(() => { 66 | const bullet = new Sprite(stage); 67 | bullet.addCostume('bullet.png'); 68 | 69 | bullet.setPosition(gun.globalX, gun.globalY); // Начальная позиция 70 | bullet.direction = gun.parent.direction; // Направление родителя 71 | 72 | bullet.forever(() => bullet.move(5)); 73 | }); 74 | ``` 75 | 76 | ### Обнаружение касания составного объекта 77 | 78 | Даже если объект состоит из нескольких дочерних спрайтов, вы можете проверить, **касается ли он другого спрайта**, используя `touchSprite()`. Метод работает **на любом уровне иерархии** — если касается хотя бы один из дочерних элементов, касание будет засчитано. 79 | 80 | ### Пример: робот касается врага 81 | 82 | ```javascript 83 | const robot = new Sprite(stage); 84 | const body = new Sprite(stage); 85 | const armLeft = new Sprite(stage); 86 | const armRight = new Sprite(stage); 87 | 88 | robot.addChild(body); 89 | body.addChild(armLeft); 90 | body.addChild(armRight); 91 | 92 | const enemy = new Sprite(stage); 93 | enemy.setPosition(400, 200); 94 | enemy.addCostume('enemy.png'); 95 | 96 | // Простейшее обнаружение касания: 97 | stage.forever(() => { 98 | if (robot.touchSprite(enemy)) { 99 | console.log("Робот касается врага!"); 100 | } 101 | }); 102 | ``` 103 | 104 | `touchSprite()` автоматически учитывает **всех потомков** — проверка будет работать, даже если только один из "рукавов" робота соприкоснётся с врагом. 105 | 106 | ## 3. Советы по работе с иерархией 107 | 108 | - Используйте составные спрайты для персонажей, интерфейсов и транспорта. 109 | - Родитель может быть **невидимым**, выполняя лишь роль "контейнера". 110 | - При уничтожении родителя все дочерние спрайты уничтожаются автоматически. 111 | - Поддерживается **многоуровневая иерархия** (вложенные группы). 112 | -------------------------------------------------------------------------------- /docs/ru/debugging.md: -------------------------------------------------------------------------------- 1 | # Отладка и производительность 2 | 3 | ScrubJS предоставляет встроенные средства для отладки и анализа производительности, которые помогают в разработке и тестировании игр. Однако в финальной версии игры рекомендуется отключать отладочные функции, чтобы снизить нагрузку на браузер. 4 | 5 | --- 6 | 7 | ## 1. Отображение ошибок 8 | 9 | Во время разработки ScrubJS может отображать **ошибки и предупреждения**, включая: 10 | 11 | - Опечатки в методах и свойствах 12 | - Попытки доступа к несуществующим объектам 13 | - Нарушения API 14 | 15 | Это реализуется через **прокси-обёртки**, которые отслеживают обращения к объектам и сравнивают их с допустимыми свойствами. Такая проверка облегчает отладку, особенно на ранних этапах. 16 | 17 | ### Управление отображением ошибок 18 | 19 | При создании игры можно включать или отключать вывод ошибок: 20 | 21 | ```javascript 22 | const game = new Game( 23 | null, // ширина 24 | null, // высота 25 | null, // canvasId 26 | true, // displayErrors — включено 27 | 'ru' // язык сообщений 28 | ); 29 | ``` 30 | 31 | ### Рекомендация 32 | 33 | В продакшн-сборке выставляйте `displayErrors = false`, чтобы убрать накладные расходы на отладку и ускорить выполнение. 34 | 35 | --- 36 | 37 | ## 2. Отладка коллайдеров 38 | 39 | Для визуальной отладки столкновений ScrubJS позволяет **подсвечивать активные коллайдеры** спрайтов и сцен. 40 | 41 | ### Пример включения отладки коллизий: 42 | 43 | ```javascript 44 | game.debugCollider = true; 45 | game.debugColor = '#00FF0077'; // Цвет отладочной рамки (зеленый с прозрачностью) 46 | ``` 47 | 48 | После этого каждый спрайт с активным коллайдером будет отображать свою **границу столкновения** поверх графики. 49 | 50 | --- 51 | 52 | ## 3. Свойство `debugMode` 53 | 54 | Свойство `debugMode` управляет режимом отображения дополнительной информации о спрайтах (например, положение, размеры, имя и т.д.). 55 | 56 | ### Возможные значения: 57 | 58 | - `'none'` — режим отладки отключён 59 | - `'hover'` — информация появляется при наведении мыши на спрайт 60 | - `'forever'` — информация отображается всегда 61 | 62 | ### Пример: 63 | 64 | ```javascript 65 | game.debugMode = 'hover'; // Отображать отладку при наведении 66 | ``` 67 | 68 | Полезно, если нужно отследить поведение конкретного объекта на сцене. 69 | 70 | --- 71 | 72 | ## 4. Повышение производительности 73 | 74 | Чтобы игра работала плавно даже на слабых устройствах, следуйте нескольким рекомендациям: 75 | 76 | ### Избегайте избыточных вычислений в `forever()` 77 | 78 | ```javascript 79 | // Негативный пример: 80 | sprite.forever(() => { 81 | sprite.drawCostume(...); // Новый костюм каждый кадр — затратно 82 | }); 83 | ``` 84 | 85 | ```javascript 86 | // Хорошо: 87 | if (!sprite.hasCostume('cached')) { 88 | sprite.drawCostume(...); 89 | } 90 | ``` 91 | 92 | ### Минимизируйте количество активно отрисовываемых спрайтов 93 | 94 | Используйте `sprite.hidden = true` для объектов вне экрана, чтобы сократить нагрузку на GPU. 95 | 96 | ### Используйте `pen()` для рисования фона и следов 97 | 98 | Вместо создания новых объектов каждый кадр, рисуйте статичные элементы через `stage.pen()`. 99 | 100 | ### Отключайте отладку перед публикацией 101 | 102 | ```javascript 103 | game.debugMode = 'none' 104 | game.debugCollider = false; 105 | game.displayErrors = false; 106 | ``` 107 | -------------------------------------------------------------------------------- /docs/ru/distance.md: -------------------------------------------------------------------------------- 1 | # Определение расстояния между спрайтом и другим объектом 2 | 3 | ## Метод getDistanceTo() 4 | 5 | Возвращает расстояние в пикселях между текущим спрайтом и указанным объектом, используя их глобальные координаты. Полезен для определения дистанции между объектами, управления ИИ врагов, активации событий на расстоянии. 6 | 7 | ```javascript 8 | getDistanceTo(object: TransformableObject): number 9 | ``` 10 | 11 | ### Параметры: 12 | * `object` (`TransformableObject`) - Объект, до которого измеряется расстояние. Может быть:
• Спрайтом (`Sprite`)
• Объектом с координатами `{ x: number, y: number }` | 13 | 14 | ### Возвращаемое значение: 15 | - `number` — расстояние в пикселях по прямой между центрами объектов. 16 | 17 | ### Как работает? 18 | **Вычисляет глобальные координаты** текущего спрайта и целевого объекта, учитывая вложенность в родительские элементы. 19 | 20 | ### Примеры использования 21 | 22 | #### Базовый пример: 23 | ```javascript 24 | const player = new Sprite(stage); 25 | const enemy = new Sprite(stage); 26 | enemy.x = 100; 27 | enemy.y = 100; 28 | 29 | // Получить расстояние между игроком и врагом 30 | const distance = player.getDistanceTo(enemy); 31 | console.log(`Дистанция: ${distance}px`); 32 | ``` 33 | 34 | #### Использование для ИИ врага: 35 | ```javascript 36 | enemy.forever(() => { 37 | const dist = enemy.getDistanceTo(player); 38 | 39 | if (dist < 200) { 40 | // Начать преследование 41 | enemy.pointForward(player); 42 | enemy.move(2); 43 | } 44 | }); 45 | ``` 46 | 47 | #### Пример: Радар для игрока 48 | ```javascript 49 | const radar = new Sprite(stage); 50 | radar.forever(() => { 51 | const allEnemies = stage.getSprites().filter(s => s.hasTag('enemy')); 52 | 53 | allEnemies.forEach(enemy => { 54 | const dist = radar.getDistanceTo(enemy); 55 | if (dist < 300) { 56 | enemy.filter = 'brightness(1.5)'; // Подсветить ближайших врагов 57 | } 58 | }); 59 | }); 60 | ``` 61 | 62 | ### Особенности 63 | 64 | #### Глобальные координаты: 65 | 66 | Если объекты вложены в родительские спрайты, метод автоматически учитывает их трансформации: 67 | 68 | ```javascript 69 | const parent = new Sprite(stage); 70 | parent.x = 50; 71 | 72 | const child = new Sprite(stage); 73 | child.setParent(parent); 74 | child.x = 30; // Глобальная X-координата = 50 + 30 = 80 75 | 76 | console.log(child.getDistanceTo({x: 0, y: 0})); // Выведет 80 77 | ``` 78 | 79 | #### Не учитывает форму и коллайдеры: 80 | 81 | Расстояние измеряется между **геометрическими центрами** объектов, даже если они имеют сложную форму: 82 | 83 | ```javascript 84 | // Для точных столкновений используйте touchSprite() 85 | if (sprite.touchSprite(other)) { 86 | // Обработка касания 87 | } 88 | ``` 89 | -------------------------------------------------------------------------------- /docs/ru/drawing.md: -------------------------------------------------------------------------------- 1 | # Возможности рисования в ScrubJS 2 | 3 | Этот раздел охватывает инструменты для создания графики "на лету", работы с холстом и визуальных эффектов. ScrubJS предоставляет простой и гибкий API для рисования с использованием встроенного контекста `CanvasRenderingContext2D`. 4 | 5 | --- 6 | 7 | ## 1. Динамические костюмы 8 | 9 | ### Метод `drawCostume()` 10 | 11 | Позволяет создавать кастомные костюмы прямо в коде, используя Canvas API. Такой подход идеально подходит для генеративной графики, UI-элементов и эффектов. 12 | 13 | ```javascript 14 | const sprite = new Sprite(stage); 15 | sprite.drawCostume((context) => { 16 | context.fillStyle = '#FF5733'; 17 | context.beginPath(); 18 | context.arc(32, 32, 30, 0, Math.PI * 2); 19 | context.fill(); 20 | 21 | context.strokeStyle = 'white'; 22 | context.lineWidth = 3; 23 | context.stroke(); 24 | }, { 25 | width: 64, 26 | height: 64, 27 | name: 'custom_circle' 28 | }); 29 | ``` 30 | 31 | **Особенности:** 32 | - `context` — это стандартный Canvas 2D контекст. 33 | - `width` и `height` определяют размер холста костюма. 34 | - Костюм можно переиспользовать, задавая `name`. 35 | - Коллайдер автоматически рассчитывается по границам изображения, но можно переопределить вручную. 36 | 37 | --- 38 | 39 | ## 2. Перманентное рисование 40 | 41 | Рисование может происходить не только в костюмах, но и напрямую на сцене или следом за спрайтом. 42 | 43 | ### Метод `pen()` для спрайтов 44 | 45 | Позволяет рисовать следы, эффекты, анимации и сложные орнаменты, двигаясь по сцене. 46 | 47 | ```javascript 48 | const brush = new Sprite(stage); 49 | brush.pen((context, sprite) => { 50 | context.fillStyle = `hsl(${Date.now() % 360}, 70%, 50%)`; 51 | context.beginPath(); 52 | context.arc(sprite.x, sprite.y, 10, 0, Math.PI * 2); 53 | context.fill(); 54 | }); 55 | 56 | brush.forever(() => { 57 | const mouse = game.getMousePoint(); 58 | brush.x = mouse.x; 59 | brush.y = mouse.y; 60 | }); 61 | ``` 62 | 63 | ### Рисование непосредственно на сцене 64 | 65 | Подходит для создания эффектов, фонов, спецэффектов, интерфейсов и визуализаций. 66 | 67 | ```javascript 68 | stage.pen((context, stage) => { 69 | // Градиентный фон 70 | const gradient = context.createLinearGradient(0, 0, stage.width, 0); 71 | gradient.addColorStop(0, '#1A2980'); 72 | gradient.addColorStop(1, '#26D0CE'); 73 | context.fillStyle = gradient; 74 | context.fillRect(0, 0, stage.width, stage.height); 75 | 76 | // Анимированные круги 77 | context.fillStyle = 'rgba(255, 255, 255, 0.1)'; 78 | context.beginPath(); 79 | context.arc( 80 | Math.sin(Date.now() / 1000) * 200 + 400, 81 | 300, 82 | 50, 83 | 0, 84 | Math.PI * 2 85 | ); 86 | context.fill(); 87 | }); 88 | ``` 89 | 90 | --- 91 | 92 | ## 3. Работа со штампами 93 | 94 | ### Статичные отпечатки спрайтов 95 | 96 | `stamp()` — это метод, который оставляет изображение текущего костюма спрайта прямо на сцене. 97 | 98 | ```javascript 99 | const stampSprite = new Sprite(stage); 100 | stampSprite.addCostume('icon.png'); 101 | 102 | stampSprite.onClick(() => { 103 | stampSprite.stamp(); // Обычный отпечаток 104 | stampSprite.stamp(0, true); // С поворотом 105 | }); 106 | ``` 107 | 108 | ### Штампование изображений на сцене 109 | 110 | Можно напрямую наносить любые изображения на сцену без использования спрайтов. 111 | 112 | ```javascript 113 | const image = new Image(); 114 | image.src = 'particle.png'; 115 | 116 | stage.forever(() => { 117 | if (game.mouseDown()) { 118 | stage.stampImage( 119 | image, 120 | game.getMousePoint().x, 121 | game.getMousePoint().y, 122 | game.getRandom(0, 360) // случайный угол поворота 123 | ); 124 | } 125 | }); 126 | ``` 127 | 128 | --- 129 | 130 | ## 4. Распространенные ошибки 131 | 132 | ### Утечки памяти при динамическом рисовании 133 | 134 | Если каждый кадр создавать новый костюм, память быстро расходуется: 135 | 136 | ```javascript 137 | // Неправильно: 138 | sprite.forever(() => { 139 | sprite.drawCostume(...); // Каждый кадр — новый объект 140 | }); 141 | 142 | // Правильно: 143 | if (!sprite.hasCostume('dynamic')) { 144 | sprite.drawCostume(...); 145 | } 146 | ``` 147 | 148 | ### Игнорирование системы слоёв 149 | 150 | Если вы используете `stage.pen()` и не указываете слой, рисунок может перекрыть важные элементы. 151 | 152 | ```javascript 153 | // Перекрытие спрайтов: 154 | stage.pen((context) => { 155 | context.fillStyle = 'black'; 156 | context.fillRect(0, 0, stage.width, stage.height); 157 | }, 999); // Явно на самый верхний слой 158 | ``` 159 | 160 | ### Некорректные координаты 161 | 162 | Рисование может происходить в локальных или глобальных координатах. Важно различать: 163 | 164 | ```javascript 165 | sprite.pen((context) => { 166 | context.fillRect(0, 0, 50, 50); // локально (относительно спрайта) 167 | 168 | context.fillRect(sprite.globalX, sprite.globalY, 50, 50); // глобально (на сцене) 169 | }); 170 | ``` 171 | -------------------------------------------------------------------------------- /docs/ru/game_loop.md: -------------------------------------------------------------------------------- 1 | # Игровой цикл 2 | 3 | Игровой цикл в ScrubJS обеспечивает непрерывное выполнение кода, подобно блоку `повторять всегда` в Scratch. Он позволяет "оживить" спрайты и сцены, задавая действия, которые будут выполняться с каждым кадром. 4 | 5 | ## Основная идея 6 | 7 | Вместо прямого использования `requestAnimationFrame`, ScrubJS предоставляет удобные методы `forever`, `repeat` и `timeout` для управления игровым циклом. 8 | 9 | ```javascript 10 | sprite.forever(() => { 11 | sprite.x += 1; // Спрайт двигается вправо каждый кадр 12 | }); 13 | ``` 14 | Эта функция будет автоматически вызываться при каждом обновлении экрана. 15 | 16 | ## Управление игровыми циклами 17 | 18 | Методы `forever`, `repeat` и `timeout` доступны как для сцен (`Stage`), так и для спрайтов (`Sprite`), позволяя гибко настраивать логику обновления объектов. 19 | 20 | ### Forever: Непрерывное выполнение 21 | 22 | `forever` запускает функцию, которая выполняется непрерывно, пока активна сцена или существует спрайт. Этот цикл идеально подходит для постоянных действий, таких как движение, анимация или проверка условий. 23 | 24 | **Параметры:** 25 | 26 | * `callback`: Функция, выполняемая на каждой итерации цикла. 27 | * `interval` (необязательный): Интервал в миллисекундах между выполнениями `callback`. Если не указан, функция выполняется при каждом кадре (максимально быстро). 28 | * `timeout` (необязательный): Задержка в миллисекундах перед *первым* запуском цикла. 29 | * `finishCallback` (необязательный): Функция, вызываемая *после* остановки цикла (например, при удалении спрайта). 30 | 31 | ### Repeat: Цикл с заданным количеством повторений 32 | 33 | `repeat` запускает функцию, которая выполняется указанное количество раз. Этот цикл подходит для действий, которые нужно выполнить несколько раз, например, для последовательности анимаций или повторяющихся эффектов. 34 | 35 | **Параметры:** 36 | 37 | * `callback`: Функция, выполняемая на каждой итерации. 38 | * `repeat`: Количество повторений цикла. 39 | * `interval` (необязательный): Интервал в миллисекундах между итерациями. Если не указан, функция выполняется максимально быстро. 40 | * `timeout` (необязательный): Задержка перед *первым* запуском цикла. 41 | * `finishCallback` (необязательный): Функция, вызываемая после завершения цикла. 42 | 43 | ### Timeout: Однократное выполнение с задержкой 44 | 45 | `timeout` выполняет функцию *один раз* после указанной задержки. Этот цикл полезен для отложенных действий, например, для показа сообщения через некоторое время или для запуска эффекта после паузы. 46 | 47 | **Параметры:** 48 | 49 | * `callback`: Функция, выполняемая после задержки. 50 | * `timeout`: Задержка в миллисекундах перед выполнением `callback`. 51 | 52 | ### Где использовать: 53 | 54 | * **Stage (Сцена):** Для глобальной логики игры, такой как управление фоном, создание эффектов или обработка ввода. 55 | * **Sprite (Спрайт):** Для логики, специфичной для конкретного спрайта, например, для его движения, анимации или взаимодействия с другими объектами. 56 | 57 | **Важно:** 58 | 59 | * Все циклы автоматически останавливаются при смене сцены. При возврате на сцену циклы возобновляются. 60 | * При удалении спрайта все его активные циклы также автоматически останавливаются. 61 | 62 | ### Примеры: 63 | 64 | ```javascript 65 | stage.forever(() => { 66 | console.log("Выполняется постоянно с частотой 100мс, первый запуск через 2 секунды."); 67 | }, 100, 2000); 68 | 69 | stage.repeat(() => { 70 | console.log("Выполнится 10 раз с частотой 200 мс. Первый запуск через 1 секунду."); 71 | }, 10, 200, 1000, () => { 72 | console.log("Выполнится при завершении цикла."); 73 | }); 74 | 75 | stage.timeout(() => { 76 | console.log("Выполнится один раз через 3 секунды."); 77 | }, 3000); 78 | ``` 79 | 80 | ## Контекст и управление состоянием цикла 81 | 82 | ### Контекст внутри цикла 83 | 84 | Внутри функций, передаваемых в `forever`, `repeat` и `timeout`, доступны: 85 | 86 | * `this`: объект, к которому привязан цикл (сцена или спрайт); 87 | * `ScheduledState` (только для `forever` и `repeat`): объект позволяющий управлять циклом. 88 | 89 | ### ScheduledState: Управление циклом изнутри и снаружи 90 | 91 | `ScheduledState` - это объект, возвращаемый при создании циклов `forever` и `repeat`, который предоставляет свойства для управления их выполнением. 92 | 93 | **Свойства:** 94 | 95 | * `interval`: Интервал между итерациями в миллисекундах. 96 | * `maxIterations`: Общее количество итераций для циклов `repeat`. 97 | * `currentIteration`: Текущая итерация для циклов `repeat`. 98 | 99 | ### Примеры управления циклом: 100 | 101 | ```javascript 102 | // Остановка цикла 103 | stage.forever((stage, state) => { 104 | return false; 105 | }); 106 | 107 | // Ускорение цикла 108 | stage.forever((stage, state) => { 109 | state.interval -= 1; 110 | }); 111 | 112 | // Передача параметров в цикл 113 | const animationState = stage.forever((stage, state) => { 114 | player.nextCostume(); 115 | 116 | if (state.control === 'fast') { 117 | state.interval = 100; 118 | } 119 | 120 | if (state.control === 'slow') { 121 | state.interval = 250; 122 | } 123 | 124 | if (state.control === 'stop') { 125 | return false; 126 | } 127 | }); 128 | 129 | stage.forever(() => { 130 | if (game.keyPressed('d')) { 131 | animationState.control = 'fast'; 132 | } 133 | 134 | if (game.keyPressed('a')) { 135 | animationState.control = 'slow'; 136 | } 137 | 138 | if (game.keyPressed('space')) { 139 | animationState.control = 'stop'; 140 | } 141 | }); 142 | ``` 143 | 144 | ## Несколько циклов на один объект 145 | 146 | Можно создавать несколько циклов для одного спрайта. Они будут выполняться независимо друг от друга, позволяя реализовать сложное поведение. 147 | 148 | ```javascript 149 | sprite.forever(() => { 150 | sprite.x += 1; // Движение вправо 151 | }); 152 | 153 | sprite.forever(() => { 154 | sprite.direction += 1; // Поворот 155 | }); 156 | ``` 157 | 158 | ## Пример: Базовое управление игроком 159 | 160 | ```javascript 161 | stage.forever(() => { 162 | if (game.keyPressed('d')) { 163 | sprite.x += 5; // Движение вправо при нажатии 'd' 164 | } 165 | 166 | if (game.keyPressed('a')) { 167 | sprite.x -= 5; // Движение влево при нажатии 'a' 168 | } 169 | }); 170 | ``` 171 | 172 | ## Остановка всех циклов объекта 173 | 174 | Для принудительной остановки всех циклов, связанных с объектом (спрайтом или сценой), используйте метод `stop()`: 175 | 176 | ```javascript 177 | sprite.stop(); // Останавливает все циклы спрайта 178 | ``` 179 | 180 | ## Как это работает 181 | 182 | Все циклы (`forever`, `repeat` и `timeout`) регистрируются в системе управления временем ScrubJS. В каждом кадре игры система вызывает функции обратного вызова (callback) этих циклов. Вам не нужно управлять этим процессом вручную – всё происходит автоматически. 183 | -------------------------------------------------------------------------------- /docs/ru/layers.md: -------------------------------------------------------------------------------- 1 | # Управление слоями 2 | 3 | Каждый спрайт в ScrubJS может быть отрисован на определённом слое. Слои позволяют точно контролировать порядок отображения объектов на экране: что находится на переднем плане, а что — позади. Это особенно важно для создания сцены с фоном, персонажами, эффектами и UI-элементами. 4 | 5 | ## Как работает система слоёв 6 | 7 | Каждому спрайту можно задать числовой приоритет через свойство `.layer`. Чем **выше значение**, тем **выше** он окажется в иерархии отрисовки. 8 | 9 | ```javascript 10 | const background = new Sprite(stage); 11 | const player = new Sprite(stage); 12 | const effects = new Sprite(stage); 13 | 14 | background.layer = 0; // Самый задний план 15 | player.layer = 1; // Основной персонаж 16 | effects.layer = 2; // Визуальные эффекты поверх игрока 17 | ``` 18 | 19 | По умолчанию все спрайты имеют `layer = 0`, поэтому рекомендуется явно задавать порядок при создании сцены. 20 | 21 | ## Динамическое изменение слоёв 22 | 23 | Слои можно менять в любое время, например, для создания эффекта "прыжка на передний план" или "появления всплывающего окна". 24 | 25 | ### Пример: игрок прыгает выше — слой увеличивается 26 | ```javascript 27 | player.forever(() => { 28 | if (player.y < 200) { 29 | player.layer = 3; // Поднимаем на передний план 30 | } else { 31 | player.layer = 1; // Возвращаем на обычный уровень 32 | } 33 | }); 34 | ``` 35 | 36 | Это особенно удобно при имитации глубины в 2D — например, когда персонаж может "зайти" за объект. 37 | 38 | ## Важные замечания 39 | 40 | - При изменении `.layer` не нужно пересоздавать спрайт — изменения вступают в силу сразу. 41 | - Если несколько спрайтов находятся на одном слое, порядок их создания определяет порядок отрисовки. 42 | - `stage.render()` автоматически сортирует спрайты по `layer` перед отрисовкой каждого кадра. 43 | 44 | ## Резюме 45 | 46 | | Значение layer | Назначение | 47 | |----------------|-----------------------------| 48 | | 0 | Фон, декорации | 49 | | 1–2 | Игроки, враги | 50 | | 3–4 | Объекты на переднем плане | 51 | | 5–9 | Визуальные эффекты, вспышки | 52 | | 10+ | Интерфейс, меню | 53 | -------------------------------------------------------------------------------- /docs/ru/main_objects.md: -------------------------------------------------------------------------------- 1 | # Основные объекты игры 2 | 3 | В ScrubJS всё начинается с трёх главных сущностей: `Game`, `Stage` и `Sprite`. Они формируют базовую архитектуру проекта и используются во всех играх. 4 | 5 | ## Класс Game 6 | 7 | Класс `Game` является основным классом, который инициализирует игру. Он отвечает за создание холста, управление сценами и запуск игры. 8 | 9 | [Подробнее про объект игры Game](game.md) 10 | 11 | ## Класс Stage 12 | 13 | Класс `Stage` представляет сцену в игре — аналог экрана или уровня. Он содержит спрайты, фоны и управляет их отображением. 14 | 15 | [Подробнее про сцены Stage](stage.md) 16 | 17 | ## Класс Sprite 18 | 19 | Класс `Sprite` представляет собой игровой объект, который может перемещаться, изменять внешний вид и взаимодействовать с другими объектами. Каждый спрайт может иметь несколько костюмов, звуков и дочерних элементов. 20 | 21 | [Подробнее про игровой объект Sprite](../en/sprite.md) 22 | 23 | ## Связи между объектами Game, Stage и Sprite 24 | 25 | - `Game` управляет всеми сценами (`Stage`). 26 | - Каждая `Stage` содержит множество `Sprite`. 27 | - Каждый `Sprite` знает свою `Stage` и может через неё получить доступ к `Game`. 28 | 29 | ## Общие рекомендации 30 | 31 | Классы `Game`, `Stage` и `Sprite` обладают широкими возможностями настройки поведения и внешнего вида объектов. Однако некоторые методы и свойства становятся доступными только после полной загрузки (`ready`). Например, нельзя выбрать костюм или воспроизвести звук до завершения загрузки. 32 | 33 | Методы игрового цикла (`forever`, `repeat`) начинают работу только после полной готовности игры. Игра считается готовой, когда все сцены полностью загружены. Сцена готова, когда загружены все изображения и звуки, включая ресурсы всех спрайтов. 34 | -------------------------------------------------------------------------------- /docs/ru/movement.md: -------------------------------------------------------------------------------- 1 | # Координаты, повороты, движения 2 | 3 | Это руководство объясняет полный цикл работы с позиционированием, ориентацией и перемещением спрайтов в ScrubJS. 4 | 5 | ## Содержание 6 | * [Система координат](#система-координат) 7 | * [Границы спрайта](#границы-спрайта) 8 | * [Повороты и направление](#повороты-и-направление) 9 | * [Базовое перемещение](#базовое-перемещение) 10 | 11 | --- 12 | 13 | ## Система координат 14 | 15 | ### Базовые принципы 16 | - Координаты `(x, y)` определяют **центр спрайта**. 17 | - Ось X: увеличивается вправо 18 | - Ось Y: увеличивается вниз\ 19 | 20 | Координаты можно изменять напрямую: 21 | 22 | ```javascript 23 | sprite.x += 5; // движение вправо 24 | sprite.y -= 3; // движение вверх 25 | ``` 26 | 27 | ### Локальные vs Глобальные координаты 28 | 29 | Для дочерних спрайтов различают глобальные и локальные координаты. Для спрайтов не имеющих родителей они равны. 30 | 31 | ```javascript 32 | const parent = new Sprite(stage); 33 | parent.x = 200; 34 | 35 | const child = new Sprite(stage); 36 | child.setParent(parent); 37 | child.x = 50; 38 | 39 | console.log(child.globalX); // 250 (200 + 50) 40 | ``` 41 | 42 | **Свойства:** 43 | - `x`, `y` - локальные координаты 44 | - `globalX`, `globalY` - глобальные координаты с учетом родителей 45 | 46 | ### Практические примеры 47 | 48 | #### Центрирование спрайта: 49 | ```javascript 50 | sprite.onReady(() => { 51 | sprite.x = stage.width/2 - sprite.width/2; 52 | sprite.y = stage.height/2 - sprite.height/2; 53 | }); 54 | ``` 55 | 56 | #### Групповое движение: 57 | ```javascript 58 | const parent = new Sprite(stage); 59 | const child = new Sprite(stage); 60 | child.setParent(parent); 61 | 62 | parent.forever(() => { 63 | parent.x += 2; 64 | // Дочерние спрайты двигаются вместе с родителем 65 | }); 66 | ``` 67 | 68 | #### Плавное перемещение: 69 | ```javascript 70 | const targetX = 400; 71 | sprite.forever(() => { 72 | sprite.x += (targetX - sprite.x) * 0.1; // Плавное приближение 73 | }); 74 | ``` 75 | 76 | --- 77 | 78 | ## Границы спрайта 79 | 80 | ### Свойства границ 81 | | Свойство | Формула | Описание | 82 | |------------|-----------------------------------|--------------------------| 83 | | `rightX` | `x + width/2 + colliderOffsetX` | Правая граница | 84 | | `leftX` | `x - width/2 + colliderOffsetX` | Левая граница | 85 | | `topY` | `y - height/2 + colliderOffsetY` | Верхняя граница | 86 | | `bottomY` | `y + height/2 + colliderOffsetY` | Нижняя граница | 87 | 88 | ### Примеры использования 89 | 90 | #### Обработка краёв экрана 91 | ```javascript 92 | // Удержание спрайта в пределах сцены 93 | sprite.forever(() => { 94 | sprite.leftX = Math.max(sprite.leftX, 0); 95 | sprite.rightX = Math.min(sprite.rightX, stage.width); 96 | sprite.topY = Math.max(sprite.topY, 0); 97 | sprite.bottomY = Math.min(sprite.bottomY, stage.height); 98 | }); 99 | ``` 100 | 101 | #### Платформер: проверка земли 102 | ```javascript 103 | const isGrounded = () => { 104 | return sprite.bottomY >= ground.topY - 1; 105 | }; 106 | ``` 107 | 108 | #### Используйте для выравнивания объектов: 109 | ```javascript 110 | // Выравнивание по верху платформы 111 | player.bottomY = platform.topY; 112 | ``` 113 | 114 | ### Особенности: 115 | 116 | - Учитывают активный коллайдер: 117 | 118 | ```javascript 119 | sprite.setRectCollider('main', 100, 50, 20, 0); 120 | console.log(sprite.rightX); // x + 50 + 20 121 | ``` 122 | 123 | - Для полигональных коллайдеров значения приблизительные 124 | - Не зависят от визуального центра вращения (`pivotOffset`) 125 | 126 | --- 127 | 128 | ## Повороты и направление 129 | 130 | ### Свойство direction 131 | 132 | Управление направлением спрайта осуществляется через свойство `direction`. 133 | 134 | ```javascript 135 | // Плавное вращение 136 | sprite.forever(() => { 137 | sprite.direction += 1.5; // 1.5° за кадр 138 | }); 139 | 140 | // Резкий разворот 141 | sprite.direction += 180; 142 | ``` 143 | 144 | **Особенности:** 145 | 146 | * Углы указываются в градусах, по часовой стрелке. 147 | * 0° — вверх, 90° — вправо, 180° — вниз, 270° — влево. 148 | * Работает с дробными числами (например, move(1.5)). 149 | 150 | ### Метод `pointForward()` 151 | 152 | Автоматический поворот к цели. Работает с любыми объектами, имеющими координаты `x` и `y` 153 | 154 | ```javascript 155 | const target = new Sprite(stage); // или const target = {x: 300, y: 200}; 156 | target.x = 300; 157 | 158 | sprite.forever(() => { 159 | sprite.pointForward(target); 160 | sprite.move(3); // Движение к цели 161 | }); 162 | ``` 163 | 164 | **Совет:** Для плавного поворота используйте линейную интерполяцию: 165 | ```javascript 166 | const targetAngle = Math.atan2(target.y - sprite.y, target.x - sprite.x) * 180/Math.PI; 167 | sprite.direction += (targetAngle - sprite.direction) * 0.1; 168 | ``` 169 | 170 | --- 171 | 172 | ## Базовое перемещение 173 | 174 | ### Метод move() 175 | 176 | Перемещает спрайт в текущем направлении. 177 | 178 | ```javascript 179 | sprite.direction = 60; // Угол 60° 180 | sprite.move(10); // 10 пикселей в направлении 181 | ``` 182 | 183 | Использует тригонометрию для расчета смещения, эквивалентный расчет 184 | ```javascript 185 | const radians = sprite.direction * Math.PI / 180; 186 | sprite.x += Math.cos(radians) * 10; 187 | sprite.y += Math.sin(radians) * 10; 188 | ``` 189 | 190 | ### Пример: движение с отскоками 191 | 192 | ```javascript 193 | sprite.forever(() => { 194 | sprite.move(5); 195 | sprite.bounceOnEdge(); 196 | }); 197 | ``` 198 | 199 | --- 200 | 201 | > Полную информацию обо всех свойствах и методах геометрии спрайта можно найти в разделе [Игровой объект Sprite](sprite.md#геометрия). 202 | -------------------------------------------------------------------------------- /docs/ru/multi_scene.md: -------------------------------------------------------------------------------- 1 | # Игра с несколькими сценами 2 | 3 | ScrubJS поддерживает создание игр с несколькими сценами (stages), такими как главное меню, уровень игры, экран победы и т. д. Каждая сцена может содержать свои объекты, логику и интерфейс. В этом разделе описано, как создавать, переключать и управлять сценами в рамках одного игрового процесса. 4 | 5 | ## Создание и управление сценами 6 | 7 | ### Базовый пример 8 | 9 | В этом примере создаются две сцены: **главное меню** и **игровая сцена**. Кнопка запуска находится в меню и при нажатии переключает игру на основной игровой уровень. 10 | 11 | ```javascript 12 | const game = new Game(); 13 | const menuStage = new Stage(); 14 | const gameStage = new Stage(); 15 | 16 | // Кнопка старта 17 | const startButton = new Sprite(menuStage); 18 | startButton.addCostume('start.png'); 19 | 20 | // Игрок 21 | const player = new Sprite(gameStage); 22 | player.addCostume('player.png'); 23 | 24 | // Переход к игровой сцене по нажатию 25 | menuStage.forever(() => { 26 | if (startButton.touchMouse() && game.mouseDownOnce()) { 27 | game.run(gameStage); 28 | } 29 | }); 30 | 31 | // Запуск игры с меню 32 | game.run(menuStage); 33 | ``` 34 | -------------------------------------------------------------------------------- /docs/ru/overview.md: -------------------------------------------------------------------------------- 1 | # ScrubJS — библиотека для создания HTML5 игр с фокусом на простоте изучения. 2 | 3 | [English version](../../README.md) 4 | 5 | Архитектура и система именования вдохновлены Scratch, популярной визуальной средой программирования, 6 | что делает ScrubJS интуитивно понятным для начинающих разработчиков. 7 | 8 | ## Предназначение 9 | 10 | Библиотека создана для того, чтобы предоставить простой и доступный способ изучения основ разработки игр, таких как: 11 | 12 | * Игровой цикл 13 | * Управление спрайтами 14 | * Обработка событий 15 | * Анимация 16 | * Коллизии 17 | 18 | ## Преимущества 19 | 20 | * Поддержка нескольких сцен 21 | * Встроенная система коллайдеров и обработки касаний 22 | * Простая работа со звуками, костюмами, слоями 23 | * Инструменты для отладки и визуализации коллайдеров 24 | * Отображение ошибок с подсказками 25 | 26 | ## Быстрый старт: 27 | ```javascript 28 | const game = new Game(800, 600); 29 | const stage = new Stage(); 30 | 31 | const cat = new Sprite(); 32 | cat.addCostume("cat.png"); 33 | 34 | stage.forever(() => { 35 | cat.move(5); 36 | cat.bounceOnEdge(); 37 | }); 38 | 39 | game.run(); 40 | ``` 41 | 42 | ## Документация: 43 | 44 | ### Архитектура: 45 | 46 | * [Основные объекты игры](main_objects.md) 47 | * [Объект игры Game](game.md) 48 | * [Сцена Stage](stage.md) 49 | * [Игровой объект Sprite](sprite.md) 50 | * [Игровой цикл](game_loop.md) 51 | 52 | ### Примеры и практики: 53 | 54 | * [Координаты, повороты, движения](movement.md) 55 | * [Центр вращения](pivot.md) 56 | * [Определение расстояния между спрайтом и другим объектом](distance.md) 57 | * [Костюмы и анимация](animations.md) 58 | * [Слои](layers.md) 59 | * [Возможности рисования](drawing.md) 60 | * [Звуки в игре](sounds.md) 61 | * [Коллайдеры, касания и теги](colliders.md) 62 | * [Игра с несколькими сценами](multi_scene.md) 63 | * [Визуальные эффекты: прозрачность и css-фильтры](visual_effects.md) 64 | * [Составные спрайты](composite_sprites.md) 65 | * [Отладка и производительность](debugging.md) 66 | -------------------------------------------------------------------------------- /docs/ru/pivot.md: -------------------------------------------------------------------------------- 1 | # Центр вращения 2 | 3 | ## Метод setPivotOffset() 4 | 5 | Позволяет изменить точку, вокруг которой происходит вращение и масштабирование спрайта. 6 | 7 | ```javascript 8 | setPivotOffset(x: number = 0, y: number = 0): this 9 | ``` 10 | 11 | **Параметры:** 12 | 13 | * `x` (`number`, необязательно, по умолчанию `0`) - смещение центра по оси X. 14 | * `y` (`number`, необязательно, по умолчанию `0`) - смещение центра по оси Y. 15 | 16 | 17 | ### Как это работает? 18 | - **По умолчанию** спрайт вращается вокруг своего геометрического центра. 19 | - **Смещение** задается относительно исходного центра спрайта: 20 | - Положительный `x` — сдвиг вправо 21 | - Отрицательный `x` — сдвиг влево 22 | - Положительный `y` — сдвиг вниз 23 | - Отрицательный `y` — сдвиг вверх 24 | 25 | ### Примеры использования 26 | 27 | #### Вращение вокруг края спрайта 28 | ```javascript 29 | const windmill = new Sprite(stage); 30 | windmill.setPivotOffset(-50, 0); // Центр смещен на 50px влево 31 | 32 | windmill.forever(() => { 33 | windmill.direction += 2; // Вращение вокруг левого края 34 | }); 35 | ``` 36 | 37 | #### Орбитальное движение 38 | ```javascript 39 | const planet = new Sprite(stage); 40 | const star = new Sprite(stage); 41 | 42 | planet.setPivotOffset(100, 0); // Центр смещен на 100px вправо 43 | planet.setParent(star); // Планета становится дочерней к звезде 44 | 45 | star.forever(() => { 46 | star.direction += 1; // Планета вращается вокруг звезды 47 | }); 48 | ``` 49 | 50 | #### Качающийся фонарь 51 | ```javascript 52 | const lantern = new Sprite(stage); 53 | lantern.setPivotOffset(0, -30); // Центр на 30px выше спрайта 54 | 55 | lantern.forever(() => { 56 | lantern.direction = Math.sin(Date.now() / 300) * 30; // -30° до +30° 57 | }); 58 | ``` 59 | 60 | ### Особенности 61 | 62 | #### Влияние на дочерние объекты 63 | ```javascript 64 | const car = new Sprite(stage); 65 | const wheel = new Sprite(stage); 66 | 67 | car.setPivotOffset(0, 20); // Смещение центра машины 68 | car.addChild(wheel); // Колесо наследует трансформации родителя 69 | 70 | // Колесо будет вращаться относительно нового центра машины 71 | ``` 72 | 73 | #### Взаимодействие с коллайдерами 74 | Центр вращения **не влияет** на коллайдеры. Физические границы остаются прежними: 75 | ```javascript 76 | sprite.setPivotOffset(50, 0); 77 | sprite.setRectCollider('main', 100, 100); // Коллайдер остается центрированным 78 | ``` 79 | 80 | ### Распространенные ошибки 81 | 82 | #### Путаница с системой координат 83 | ```javascript 84 | // Неправильно: смещение на 50px вниз 85 | sprite.setPivotOffset(0, 50); 86 | 87 | // Правильно: для смещения вниз используйте положительный Y 88 | // (Y увеличивается вниз в системе координат canvas) 89 | ``` 90 | 91 | #### Игнорирование родительских преобразований 92 | ```javascript 93 | const parent = new Sprite(stage); 94 | parent.x = 100; 95 | 96 | const child = new Sprite(stage); 97 | child.setParent(parent); 98 | child.setPivotOffset(50, 0); // Смещение относительно родителя 99 | ``` 100 | 101 | --- 102 | 103 | > Полную информацию обо всех свойствах и методах геометрии спрайта можно найти в разделе [Игровой объект Sprite](sprite.md#геометрия). 104 | -------------------------------------------------------------------------------- /docs/ru/sounds.md: -------------------------------------------------------------------------------- 1 | # Звуки в игре 2 | 3 | Этот раздел описывает, как добавлять, воспроизводить и управлять звуками и музыкой в ScrubJS. Библиотека предоставляет простой интерфейс для работы со звуковыми эффектами. 4 | 5 | ## 1. Базовое управление звуками 6 | 7 | ### Добавление и воспроизведение 8 | 9 | Для начала нужно загрузить звук и назначить ему имя, по которому его можно воспроизводить: 10 | 11 | ```javascript 12 | const player = new Sprite(stage); 13 | player.addSound('jump.wav', 'jump'); 14 | 15 | stage.forever(() => { 16 | if (game.keyPressed('space')) { 17 | player.playSoundByName('jump'); 18 | } 19 | }); 20 | ``` 21 | 22 | ### Использование индексов звука 23 | 24 | Вы можете добавить звук без указания имени, в этом случае, для воспроизведения, необходимо будет использовать его индекс: 25 | 26 | ```javascript 27 | const player = new Sprite(stage); 28 | player.addSound('jump.wav'); 29 | player.playSound(0); 30 | ``` 31 | 32 | ### Динамическая громкость 33 | 34 | Громкость звука может зависеть, например, от расстояния до игрока: 35 | 36 | ```javascript 37 | const enemy = new Sprite(stage); 38 | enemy.forever(() => { 39 | const distance = player.getDistanceTo(enemy); 40 | const volume = Math.max(0, 1 - distance / 500); 41 | enemy.sounds.get('alert').volume = volume; 42 | }); 43 | ``` 44 | 45 | ### Звуки на сцене 46 | 47 | Все те же возможности работы со звуками поддерживает и объект сцены (Stage). 48 | 49 | Пример воспроизведения звука сценой с заданной громкостью и начальной позицией (в секундах): 50 | 51 | ```javascript 52 | const stage = new Stage(); 53 | stage.addSound('background.mp3', 'bg_music'); 54 | stage.playSoundByName('bg_music', 0.3, 5.0); // Громкость 30%, начало с 5-й секунды 55 | ``` 56 | 57 | --- 58 | 59 | > Полную информацию о свойствах и методах управления звукам спрайта можно найти в разделе [Игровой объект Sprite](sprite.md#звуки). 60 | -------------------------------------------------------------------------------- /docs/ru/visual_effects.md: -------------------------------------------------------------------------------- 1 | # Визуальные эффекты: прозрачность и CSS-фильтры в ScrubJS 2 | 3 | Этот раздел объясняет, как работать с прозрачностью и CSS-фильтрами для создания сложных визуальных эффектов. 4 | 5 | ## 1. Управление прозрачностью 6 | 7 | ### Базовое использование свойства `opacity` 8 | ```javascript 9 | const ghost = new Sprite(stage); 10 | ghost.opacity = 0.5; // 50% прозрачности 11 | ``` 12 | 13 | ### Плавное исчезновение объекта 14 | ```javascript 15 | ghost.forever(() => { 16 | ghost.opacity -= 0.01; 17 | 18 | if(ghost.opacity <= 0) { 19 | ghost.delete(); 20 | } 21 | }); 22 | ``` 23 | 24 | ### Мигание при получении урона 25 | ```javascript 26 | stage.forever(() => { 27 | if (player.touchSprite(emeny)) { 28 | emeny.delete(); 29 | 30 | player.repeat((sprite, state) => { 31 | sprite.opacity = state.currentIteration % 2 ? 0.3 : 1; 32 | }, 6, 100); // 6 миганий с интервалом 100ms 33 | } 34 | }); 35 | ``` 36 | 37 | ## 2. Работа с CSS-фильтрами 38 | 39 | ### Основные типы фильтров 40 | ```javascript 41 | // Размытие 42 | sprite.filter = 'blur(5px)'; 43 | 44 | // Чёрно-белый режим 45 | sprite.filter = 'grayscale(100%)'; 46 | 47 | // Смещение цвета 48 | sprite.filter = 'hue-rotate(90deg)'; 49 | 50 | // Тень 51 | sprite.filter = 'drop-shadow(5px 5px 5px rgba(0,0,0,0.5))'; 52 | ``` 53 | 54 | ### Динамические эффекты 55 | ```javascript 56 | // Плавное изменение цвета 57 | sprite.forever(() => { 58 | sprite.filter = `hue-rotate(${Date.now() % 360}deg)`; 59 | }); 60 | 61 | // Эффект "дыхания" с размытием 62 | sprite.forever(() => { 63 | const blur = Math.abs(Math.sin(Date.now()/500)) * 10; 64 | sprite.filter = `blur(${blur}px)`; 65 | }); 66 | ``` 67 | 68 | ## 3. Комбинирование фильтров 69 | 70 | ### Множественные эффекты 71 | ```javascript 72 | boss.filter = ` 73 | drop-shadow(0 0 10px #FF0000) 74 | contrast(150%) 75 | brightness(0.8) 76 | `; 77 | ``` 78 | 79 | ### Анимированный эффект ауры 80 | ```javascript 81 | let auraPhase = 0; 82 | boss.forever(() => { 83 | auraPhase += 0.1; 84 | const glowSize = Math.sin(auraPhase) * 5 + 10; 85 | boss.filter = ` 86 | drop-shadow(0 0 ${glowSize}px rgba(255, 0, 0, 0.7)) 87 | brightness(${1 + Math.abs(Math.sin(auraPhase)) * 0.3}) 88 | `; 89 | }); 90 | ``` 91 | 92 | ## 4. Специфические эффекты 93 | 94 | ### Эффект "заморозки" 95 | ```javascript 96 | const freezeEffect = () => { 97 | sprite.filter = ` 98 | grayscale(80%) 99 | blur(2px) 100 | contrast(120%) 101 | `; 102 | sprite.opacity = 0.8; 103 | }; 104 | ``` 105 | 106 | ### Эффект телепортации 107 | ```javascript 108 | sprite.repeat((s, state) => { 109 | s.opacity = Math.random(); 110 | s.filter = `hue-rotate(${Math.random() * 360}deg)`; 111 | }, 20, 50, 0, () => { 112 | s.filter = 'none'; 113 | }); 114 | ``` 115 | 116 | ## 5. Распространённые ошибки 117 | 118 | ### Неправильный синтаксис фильтров 119 | ```javascript 120 | // Неправильно: 121 | sprite.filter = 'blur 5px'; 122 | 123 | // Правильно: 124 | sprite.filter = 'blur(5px)'; 125 | ``` 126 | 127 | ### Конфликтующие фильтры 128 | ```javascript 129 | // Непредсказуемый результат: 130 | sprite.filter = 'brightness(2)'; 131 | sprite.filter = 'grayscale(100%)'; // Перезаписывает предыдущий 132 | 133 | // Правильно комбинировать: 134 | sprite.filter = 'brightness(2) grayscale(100%)'; 135 | ``` 136 | 137 | ### Игнорирование порядка фильтров 138 | ```javascript 139 | // Разный результат: 140 | sprite.filter = 'blur(5px) grayscale(100%)'; 141 | sprite.filter = 'grayscale(100%) blur(5px)'; 142 | ``` 143 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jetcode-scrubjs", 3 | "private": false, 4 | "version": "2.3.4", 5 | "description": "HTML5 Game Library with a Focus on Ease of Learning", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/jetcode-org/scrub.js.git" 9 | }, 10 | "author": "Andrey Nilov", 11 | "license": "MIT", 12 | "bugs": { 13 | "url": "https://github.com/jetcode-org/scrub.js/issues" 14 | }, 15 | "homepage": "https://github.com/jetcode-org/scrub.js#readme", 16 | "keywords": ["HTML5", "game", "2d", "library", "canvas", "javascript", "typescript"], 17 | "devDependencies": { 18 | "tsup": "^8.4.0", 19 | "typescript": "^5.8.3" 20 | }, 21 | "scripts": { 22 | "build": "tsup", 23 | "watch": "tsup --watch" 24 | }, 25 | "directories": { 26 | "doc": "docs" 27 | }, 28 | "files": ["dist"], 29 | "main": "./dist/scrub.mjs", 30 | "types": "./dist/scrub.d.ts", 31 | "browser": "./dist/scrub.js", 32 | "exports": { 33 | "import": "./dist/scrub.mjs", 34 | "require": "./dist/scrub.cjs", 35 | "default": "./dist/scrub.mjs" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /public/polyfills.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | var lastTime = 0; 3 | var vendors = ['ms', 'moz', 'webkit', 'o']; 4 | for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { 5 | window.requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame']; 6 | window.cancelAnimationFrame = window[vendors[x] + 'CancelAnimationFrame'] 7 | || window[vendors[x] + 'CancelRequestAnimationFrame']; 8 | } 9 | 10 | if (!window.requestAnimationFrame) { 11 | window.requestAnimationFrame = function (callback, element) { 12 | var currTime = new Date().getTime(); 13 | var timeToCall = Math.max(0, 16 - (currTime - lastTime)); 14 | var id = window.setTimeout(function () { 15 | callback(currTime + timeToCall); 16 | }, timeToCall); 17 | lastTime = currTime + timeToCall; 18 | 19 | return id; 20 | }; 21 | } 22 | 23 | if (!window.cancelAnimationFrame) { 24 | window.cancelAnimationFrame = function (id) { 25 | clearTimeout(id); 26 | }; 27 | } 28 | }()); 29 | -------------------------------------------------------------------------------- /src/Camera.ts: -------------------------------------------------------------------------------- 1 | import { Stage } from './Stage'; 2 | import { CameraChanges } from './CameraChanges'; 3 | 4 | export class Camera { 5 | 6 | stage: Stage; 7 | 8 | changes: CameraChanges; 9 | 10 | private _x: number; 11 | private _y: number; 12 | private _direction: number = 0; 13 | private _renderRadius: number; 14 | private _zoom = 1; 15 | 16 | constructor(stage:Stage) { 17 | this.stage = stage; 18 | this._x = this.stage.width / 2; 19 | this._y = this.stage.height / 2; 20 | this.updateRenderRadius(); 21 | this.changes = new CameraChanges(); 22 | } 23 | 24 | set direction(value){ 25 | if (this.changes.direction == 0) { 26 | let direction = value % 360; 27 | direction = direction < 0 ? direction + 360 : direction; 28 | 29 | this.changes.direction = direction - this._direction; 30 | 31 | this._direction = direction; 32 | } 33 | } 34 | 35 | get direction(){ 36 | return this._direction; 37 | } 38 | 39 | get angleDirection() { 40 | return this._direction * Math.PI / 180; 41 | } 42 | 43 | get width(){ 44 | return this.stage.width / this._zoom; 45 | } 46 | 47 | get height(){ 48 | return this.stage.height / this._zoom; 49 | } 50 | 51 | set x(value){ 52 | this.changes.x = value - this._x; 53 | } 54 | 55 | get x(){ 56 | return this._x + this.changes.x; 57 | } 58 | 59 | set y(value){ 60 | this.changes.y = value - this._y; 61 | } 62 | 63 | get y(){ 64 | return this._y + this.changes.y; 65 | } 66 | 67 | get startCornerX (): number { 68 | return this._x - this.stage.width / 2; 69 | } 70 | 71 | get startCornerY (): number { 72 | return this._y - this.stage.height / 2; 73 | } 74 | 75 | get renderRadius() { 76 | return this._renderRadius; 77 | } 78 | 79 | set zoom(value){ 80 | if (this.changes.zoom == 1){ 81 | const newZoom = value < 0.1 ? 0.1 : value 82 | 83 | this.changes.zoom = newZoom / this._zoom; 84 | 85 | this._zoom = newZoom; 86 | this.updateRenderRadius() 87 | } 88 | } 89 | 90 | get zoom(){ 91 | return this._zoom; 92 | } 93 | 94 | stop(){ 95 | this.stage.context.translate(this._x - this.stage.width / 2, this._y - this.stage.height / 2); 96 | 97 | this.stage.context.translate(this.stage.width / 2, this.stage.height / 2); 98 | this.stage.context.scale(1 / this._zoom, 1 / this._zoom); 99 | // this.context.rotate(-this._direction * Math.PI / 180); 100 | this.stage.context.translate(-this.stage.width / 2, -this.stage.height / 2); 101 | this._renderRadius = Math.hypot(this.stage.width, this.stage.height) / 1.5; 102 | } 103 | 104 | run(){ 105 | this.stage.context.translate(-this._x + this.stage.width / 2, -this._y + this.stage.height / 2); 106 | 107 | this.stage.context.translate(this._x, this._y); 108 | this.stage.context.scale(this._zoom, this._zoom); 109 | // this.context.rotate(this._direction * Math.PI / 180); 110 | this.stage.context.translate(-this._x, -this._y); 111 | 112 | this.updateRenderRadius(); 113 | 114 | this.changes.reset(); 115 | } 116 | 117 | update() { 118 | this._x += this.changes.x; 119 | this._y += this.changes.y; 120 | } 121 | 122 | private updateRenderRadius(){ 123 | this._renderRadius = Math.hypot(this.width, this.height) / 1.5; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/CameraChanges.ts: -------------------------------------------------------------------------------- 1 | export class CameraChanges { 2 | x = 0; 3 | 4 | y = 0; 5 | 6 | zoom = 1; 7 | 8 | direction = 0; 9 | 10 | reset() { 11 | this.x = 0; 12 | this.y = 0; 13 | this.zoom = 1; 14 | this.direction = 0; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Costume.ts: -------------------------------------------------------------------------------- 1 | export class Costume { 2 | image: HTMLCanvasElement; 3 | ready = false; 4 | 5 | get width(): number { 6 | if (this.image instanceof HTMLCanvasElement) { 7 | return this.image.width; 8 | } 9 | 10 | return 0; 11 | } 12 | 13 | get height(): number { 14 | if (this.image instanceof HTMLCanvasElement) { 15 | return this.image.height; 16 | } 17 | 18 | return 0; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/EventEmitter.ts: -------------------------------------------------------------------------------- 1 | export class EventEmitter { 2 | private eventTarget: EventTarget; 3 | private callbacksMap = new Map(); 4 | 5 | constructor() { 6 | this.eventTarget = new EventTarget(); 7 | } 8 | 9 | once(name: string, type: string, callback: EventListenerOrEventListenerObject): boolean { 10 | if (this.callbacksMap.get(name)) { 11 | return false; 12 | } 13 | 14 | const wrapper: EventListener = (event) => { 15 | if (typeof callback === 'function') { 16 | callback(event); 17 | 18 | } else { 19 | callback.handleEvent(event); 20 | } 21 | 22 | this.eventTarget.removeEventListener(type, wrapper); 23 | this.remove(name); 24 | }; 25 | 26 | this.eventTarget.addEventListener(type, wrapper); 27 | this.callbacksMap.set(name, {type, callback: wrapper}); 28 | 29 | return true; 30 | } 31 | 32 | on(name: string, type: string, callback: EventListenerOrEventListenerObject): boolean { 33 | if (this.callbacksMap.get(name)) { 34 | return false; 35 | } 36 | 37 | this.eventTarget.addEventListener(type, callback); 38 | this.callbacksMap.set(name, {type, callback}); 39 | 40 | return true; 41 | } 42 | 43 | emit(type: string, detail: any): void { 44 | this.eventTarget.dispatchEvent(new CustomEvent(type, {detail})); 45 | } 46 | 47 | remove(name: string): boolean { 48 | const item = this.callbacksMap.get(name); 49 | 50 | if (!item) { 51 | return false; 52 | } 53 | 54 | this.eventTarget.removeEventListener(item.type, item.callback); 55 | this.callbacksMap.delete(name); 56 | 57 | return true; 58 | } 59 | 60 | removeByType(type: string): void { 61 | this.callbacksMap.forEach((item, itemName) => { 62 | if (type === item.type) { 63 | this.eventTarget.removeEventListener(item.type, item.callback); 64 | this.callbacksMap.delete(itemName); 65 | } 66 | }); 67 | } 68 | 69 | clearAll(): void { 70 | this.callbacksMap.forEach(item => { 71 | this.eventTarget.removeEventListener(item.type, item.callback); 72 | }); 73 | 74 | this.callbacksMap.clear(); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Game.ts: -------------------------------------------------------------------------------- 1 | import { ErrorMessages, Keyboard, Mouse, Registry, Styles, ValidatorFactory } from './utils'; 2 | import { Stage } from './Stage'; 3 | import { Sprite } from './Sprite'; 4 | import { ScheduledState } from './ScheduledState'; 5 | import { EventEmitter } from './EventEmitter'; 6 | import { PointCollider } from './collisions'; 7 | 8 | export type DrawingCallbackFunction = (context: CanvasRenderingContext2D, object: Stage | Sprite) => void; 9 | export type ScheduledCallbackFunction = (context: Stage | Sprite, state: ScheduledState) => boolean | void; 10 | export type Locale = 'ru' | 'en'; 11 | 12 | export type TransformableObject = { 13 | x: number, 14 | y: number, 15 | globalX?: number, 16 | globalY?: number 17 | }; 18 | 19 | export type GridCostumeOptions = { 20 | cols: number, 21 | rows: number, 22 | limit?: number, 23 | offset?: number, 24 | name?: string, 25 | rotate?: number, 26 | flipX?: boolean, 27 | flipY?: boolean, 28 | x?: number, 29 | y?: number, 30 | width?: number, 31 | height?: number, 32 | alphaColor?: string | { r: number; g: number; b: number }, 33 | alphaTolerance?: number, 34 | crop?: number, 35 | cropTop?: number, 36 | cropRight?: number, 37 | cropBottom?: number, 38 | cropLeft?: number 39 | }; 40 | 41 | export type CostumeOptions = { 42 | name?: string, 43 | rotate?: number, 44 | flipX?: boolean, 45 | flipY?: boolean, 46 | x?: number, 47 | y?: number, 48 | width?: number, 49 | height?: number, 50 | alphaColor?: string | { r: number; g: number; b: number }, 51 | alphaTolerance?: number, 52 | crop?: number, 53 | cropTop?: number, 54 | cropRight?: number, 55 | cropBottom?: number, 56 | cropLeft?: number 57 | }; 58 | 59 | export type SoundOptions = { 60 | volume?: number, 61 | currentTime?: number, 62 | loop?: boolean 63 | }; 64 | 65 | export class Game { 66 | id: Symbol; 67 | eventEmitter: EventEmitter; 68 | validatorFactory: ValidatorFactory; 69 | canvas: HTMLCanvasElement; 70 | context: CanvasRenderingContext2D; 71 | keyboard: Keyboard; 72 | mouse: Mouse; 73 | 74 | debugMode = 'none'; // none, hover, forever; 75 | debugCollider = false; 76 | debugColor = 'red'; 77 | 78 | static readonly STAGE_READY_EVENT = 'scrubjs.stage.ready'; 79 | static readonly STAGE_BACKGROUND_READY_EVENT = 'scrubjs.stage.background_ready'; 80 | static readonly SPRITE_READY_EVENT = 'scrubjs.sprite.ready'; 81 | 82 | private stages: Stage[] = []; 83 | private activeStage: Stage = null 84 | private styles = null; 85 | private loadedStages = 0; 86 | private onReadyCallbacks = []; 87 | private onUserInteractedCallbacks = []; 88 | private onReadyPending = true; 89 | protected running = false; 90 | private pendingRun = false; 91 | private reportedError = false; 92 | private _displayErrors = true; 93 | private _locale = 'ru'; 94 | private _userInteracted = false; 95 | private userInteractionPromise: Promise; 96 | 97 | constructor(width: number = null, 98 | height: number = null, 99 | canvasId: string = null, 100 | displayErrors = true, 101 | locale: Locale = 'ru', 102 | smoothingEnabled = false 103 | ) { 104 | this._displayErrors = displayErrors; 105 | this._locale = locale; 106 | this.validatorFactory = new ValidatorFactory(this); 107 | 108 | let game = this; 109 | if (this.displayErrors) { 110 | game = this.validatorFactory.createValidator(this, 'Game'); 111 | } 112 | 113 | window.onerror = () => { 114 | game.reportError(ErrorMessages.getMessage(ErrorMessages.SCRIPT_ERROR, game._locale)); 115 | }; 116 | 117 | game.id = Symbol(); 118 | game.eventEmitter = new EventEmitter(); 119 | game.keyboard = new Keyboard(); 120 | 121 | if (canvasId) { 122 | const element = document.getElementById(canvasId); 123 | 124 | if (element instanceof HTMLCanvasElement) { 125 | game.canvas = element; 126 | } 127 | 128 | } else { 129 | game.canvas = document.createElement('canvas'); 130 | document.body.appendChild(game.canvas); 131 | } 132 | 133 | game.canvas.width = width; 134 | game.canvas.height = height; 135 | game.styles = new Styles(game.canvas, width, height); 136 | game.mouse = new Mouse(game); 137 | game.context = game.canvas.getContext('2d'); 138 | game.context.imageSmoothingEnabled = smoothingEnabled; 139 | 140 | Registry.getInstance().set('game', game); 141 | 142 | game.addListeners(); 143 | 144 | return game; 145 | } 146 | 147 | addStage(stage: Stage): this { 148 | this.stages.push(stage); 149 | 150 | return this; 151 | } 152 | 153 | getLastStage(): Stage | null { 154 | if (!this.stages.length) { 155 | return null; 156 | } 157 | 158 | return this.stages[this.stages.length - 1]; 159 | } 160 | 161 | getActiveStage(): Stage | null { 162 | if (this.activeStage) { 163 | return this.activeStage; 164 | } 165 | 166 | return null; 167 | } 168 | 169 | run(stage: Stage = null): void { 170 | if (this.activeStage && this.activeStage == stage) { 171 | return; 172 | } 173 | 174 | if (!stage && this.stages.length) { 175 | stage = this.stages[0]; 176 | } 177 | 178 | if (!stage) { 179 | this.throwError(ErrorMessages.NEED_STAGE_BEFORE_RUN_GAME); 180 | } 181 | 182 | if (!this.running) { // only first run 183 | for (const inStage of this.stages) { 184 | inStage.ready(); 185 | } 186 | } 187 | 188 | if (this.activeStage && this.activeStage.running) { 189 | this.activeStage.stop(); 190 | } 191 | 192 | this.running = false; 193 | this.pendingRun = true; 194 | this.activeStage = stage; 195 | 196 | this.tryDoRun(); 197 | } 198 | 199 | isReady(): boolean { 200 | return this.loadedStages == this.stages.length; 201 | } 202 | 203 | onReady(callback: CallableFunction): void { 204 | this.onReadyCallbacks.push(callback); 205 | } 206 | 207 | onUserInteracted(callback: CallableFunction): void { 208 | this.onUserInteractedCallbacks.push(callback); 209 | } 210 | 211 | stop(): void { 212 | if (this.activeStage && this.activeStage.running) { 213 | this.activeStage.stop(); 214 | } 215 | 216 | this.running = false; 217 | } 218 | 219 | get displayErrors(): boolean { 220 | return this._displayErrors; 221 | } 222 | 223 | get locale(): string { 224 | return this._locale; 225 | } 226 | 227 | get width(): number { 228 | return this.canvas.width; 229 | } 230 | 231 | get height(): number { 232 | return this.canvas.height; 233 | } 234 | 235 | get userInteracted(): boolean { 236 | return this._userInteracted; 237 | } 238 | 239 | isInsideGame(x: number, y: number): boolean { 240 | return x >= 0 && x <= this.width && y >= 0 && y <= this.height; 241 | } 242 | 243 | correctMouseX(mouseX: number): number { 244 | const cameraOffsetX = this.activeStage ? this.activeStage.camera.startCornerX : 0; 245 | 246 | return mouseX - this.styles.canvasRect.left + cameraOffsetX; 247 | } 248 | 249 | correctMouseY(mouseY: number): number { 250 | const cameraOffsetY = this.activeStage ? this.activeStage.camera.startCornerY : 0; 251 | 252 | return mouseY - this.styles.canvasRect.top + cameraOffsetY; 253 | } 254 | 255 | keyPressed(char: string | string[]): boolean { 256 | if (Array.isArray(char)) { 257 | for (const oneChar of char) { 258 | const pressed = this.keyboard.keyPressed(oneChar); 259 | 260 | if (pressed) { 261 | return true; 262 | } 263 | } 264 | 265 | return false; 266 | } 267 | 268 | return this.keyboard.keyPressed(char); 269 | } 270 | 271 | keyDown(char: string, callback: CallableFunction): void { 272 | this.keyboard.keyDown(char, callback); 273 | } 274 | 275 | keyUp(char: string, callback: CallableFunction): void { 276 | this.keyboard.keyUp(char, callback); 277 | } 278 | 279 | mouseDown(): boolean { 280 | return this.mouse.isMouseDown(this.activeStage); 281 | } 282 | 283 | mouseDownOnce(): boolean { 284 | const isMouseDown = this.mouse.isMouseDown(this.activeStage); 285 | this.mouse.clearMouseDown(); 286 | 287 | return isMouseDown; 288 | } 289 | 290 | getMousePoint(): PointCollider { 291 | return this.mouse.getPoint(); 292 | } 293 | 294 | getRandom(min: number, max: number): number { 295 | return Math.floor(Math.random() * (max - min + 1)) + min; 296 | } 297 | 298 | throwError(messageId: string, variables: {} | null = null, reportError = true): void { 299 | const message = ErrorMessages.getMessage(messageId, this.locale, variables); 300 | 301 | this.throwErrorRaw(message, reportError); 302 | } 303 | 304 | throwErrorRaw(message: string, reportError = true): void { 305 | if (reportError) { 306 | this.reportError(message); 307 | } 308 | 309 | throw new Error(message); 310 | } 311 | 312 | private reportError(message): void { 313 | if (this._displayErrors && !this.reportedError) { 314 | alert(message); 315 | 316 | this.reportedError = true; 317 | } 318 | } 319 | 320 | private addListeners(): void { 321 | this.eventEmitter.on(Game.STAGE_READY_EVENT, Game.STAGE_READY_EVENT, (event: CustomEvent) => { 322 | this.loadedStages++; 323 | this.tryDoOnReady(); 324 | }); 325 | 326 | document.addEventListener('visibilitychange', () => { 327 | if (document.hidden) { 328 | if (this.activeStage && this.activeStage.running) { 329 | this.activeStage.stop(); 330 | } 331 | 332 | } else { 333 | if (this.activeStage && this.activeStage.stopped) { 334 | this.activeStage.run(); 335 | } 336 | } 337 | }); 338 | 339 | this.userInteractionPromise = new Promise((resolve) => { 340 | document.addEventListener('click', resolve, { once: true }); 341 | 342 | document.addEventListener('keydown', (event) => { 343 | const excludedKeys = ['Control', 'Shift', 'CapsLock', 'NumLock', 'Alt', 'Meta']; 344 | if (!excludedKeys.includes(event.key)) { 345 | resolve(true); 346 | } 347 | }, { once: true }); 348 | }); 349 | } 350 | 351 | private tryDoOnReady(): void { 352 | if (this.isReady() && this.onReadyPending) { 353 | this.onReadyPending = false; 354 | 355 | if (this.onReadyCallbacks.length) { 356 | for (const callback of this.onReadyCallbacks) { 357 | callback(); 358 | } 359 | this.onReadyCallbacks = []; 360 | } 361 | 362 | this.userInteractionPromise.then(() => { 363 | this._userInteracted = true; 364 | 365 | this.onUserInteractedCallbacks.filter(callback => { 366 | callback(this); 367 | 368 | return false 369 | }); 370 | }); 371 | 372 | this.tryDoRun(); 373 | } 374 | } 375 | 376 | private tryDoRun(): void { 377 | if (this.pendingRun && !this.running && this.isReady()) { 378 | this.running = true; 379 | this.pendingRun = false; 380 | 381 | this.activeStage.run(); 382 | } 383 | } 384 | } 385 | -------------------------------------------------------------------------------- /src/MultiplayerControl.ts: -------------------------------------------------------------------------------- 1 | import { MultiplayerGame } from './MultiplayerGame'; 2 | import { JetcodeSocket, JetcodeSocketConnection } from './jmp'; 3 | import { SyncObjectInterface } from './SyncObjectInterface'; 4 | import { Player } from './Player'; 5 | import { PointCollider } from './collisions'; 6 | import { KeyboardMap } from './utils'; 7 | 8 | 9 | export class MultiplayerControl { 10 | private game: MultiplayerGame; 11 | private connection: JetcodeSocketConnection; 12 | private trackedKeys = []; 13 | private receiveDataConnections: CallableFunction[] = []; 14 | private keydownCallback: (event: KeyboardEvent) => void; 15 | private mousedownCallback: (event: MouseEvent) => void; 16 | private userKeydownCallbacks = new Map(); 17 | private systemLockedChars = {}; 18 | private userLockedChars = {}; 19 | private userMousedownCallback: [CallableFunction, string, SyncObjectInterface[]]; 20 | private systemMouseLocked: boolean = false; 21 | private userMouseLocked: boolean = false; 22 | 23 | constructor(player: Player, game: MultiplayerGame, connection: JetcodeSocketConnection, isMe: boolean) { 24 | this.game = game; 25 | this.connection = connection; 26 | 27 | if (isMe) { 28 | this.defineListeners(); 29 | } 30 | 31 | const keydownConnection = connection.connect(JetcodeSocket.RECEIVE_DATA, (dataJson: any, parameters) => { 32 | const data = JSON.parse(dataJson); 33 | const char = data['char']; 34 | 35 | if (!parameters.SendTime || parameters.Keydown != 'true' || parameters.MemberId != player.id || !this.trackedKeys.includes(char)) { 36 | return; 37 | } 38 | 39 | if (this.userKeydownCallbacks.has(char)) { 40 | const callback = this.userKeydownCallbacks.get(char)[0]; 41 | 42 | const block = (isBlock: boolean, chars = [char], mouse = false) => { 43 | if (mouse) { 44 | this.userMouseLocked = isBlock; 45 | } 46 | 47 | for (const char of chars) { 48 | this.userLockedChars[char.toUpperCase()] = isBlock; 49 | } 50 | }; 51 | 52 | let attempts = 0; 53 | const handler = () => { 54 | if (this.userLockedChars[char] !== true || attempts > 999) { 55 | const syncData = data['sync']; 56 | if (syncData) { 57 | game.syncObjects(syncData, this.game.calcDeltaTime(parameters.SendTime)); 58 | } 59 | 60 | callback(player, block); 61 | 62 | } else { 63 | attempts++; 64 | setTimeout(handler, 50); 65 | } 66 | }; 67 | 68 | handler(); 69 | } 70 | 71 | this.systemLockedChars[char] = false; 72 | }); 73 | this.receiveDataConnections.push(keydownConnection); 74 | 75 | const mousedownConnection = connection.connect(JetcodeSocket.RECEIVE_DATA, (dataJson: any, parameters) => { 76 | if (!parameters.SendTime || parameters.Mousedown != 'true' || parameters.MemberId != player.id) { 77 | return; 78 | } 79 | 80 | if (this.userMousedownCallback) { 81 | const callback = this.userMousedownCallback[0]; 82 | const data = JSON.parse(dataJson); 83 | const mouseX = data['mouseX']; 84 | const mouseY = data['mouseY']; 85 | const syncData = data['sync']; 86 | 87 | const block = (isBlock: boolean, chars = [], mouse = true) => { 88 | if (mouse) { 89 | this.userMouseLocked = isBlock; 90 | } 91 | 92 | for (const char of chars) { 93 | this.userLockedChars[char.toUpperCase()] = isBlock; 94 | } 95 | }; 96 | 97 | let attempts = 0; 98 | const handler = () => { 99 | if (this.userMouseLocked !== true || attempts > 999) { 100 | if (syncData) { 101 | game.syncObjects(syncData, this.game.calcDeltaTime(parameters.SendTime)); 102 | } 103 | 104 | const mousePoint = new PointCollider(mouseX, mouseY); 105 | callback(mousePoint, player, block); 106 | 107 | } else { 108 | attempts++; 109 | setTimeout(handler, 50); 110 | } 111 | }; 112 | 113 | handler(); 114 | } 115 | 116 | this.systemMouseLocked = false; 117 | }); 118 | this.receiveDataConnections.push(mousedownConnection); 119 | } 120 | 121 | private defineListeners() { 122 | this.keydownCallback = (event: KeyboardEvent) => { 123 | const char = KeyboardMap.getChar(event.keyCode); 124 | 125 | if ( 126 | !this.userKeydownCallbacks.has(char) || 127 | this.systemLockedChars[char] === true || 128 | this.userLockedChars[char] === true || 129 | !this.trackedKeys.includes(char) 130 | ) { 131 | return; 132 | } 133 | 134 | this.systemLockedChars[char] = true; 135 | 136 | const syncPackName = this.userKeydownCallbacks.get(char)[1]; 137 | const syncData = this.userKeydownCallbacks.get(char)[2]; 138 | const syncDataPacked = this.game.packSyncData(syncPackName, syncData); 139 | 140 | this.connection.sendData(JSON.stringify({ 141 | 'char': char, 142 | 'sync': syncDataPacked 143 | }), { 144 | Keydown: 'true' 145 | }); 146 | }; 147 | 148 | this.mousedownCallback = (event: MouseEvent) => { 149 | if (!this.userMousedownCallback || this.systemMouseLocked || this.userMouseLocked) { 150 | return; 151 | } 152 | 153 | const mouseX = this.game.correctMouseX(event.clientX); 154 | const mouseY = this.game.correctMouseY(event.clientY); 155 | 156 | if (!this.game.isInsideGame(mouseX, mouseY)) { 157 | return; 158 | } 159 | 160 | this.systemMouseLocked = true; 161 | 162 | const syncPackName = this.userMousedownCallback[1]; 163 | const syncData = this.userMousedownCallback[2]; 164 | const syncDataPacked = this.game.packSyncData(syncPackName, syncData); 165 | 166 | this.connection.sendData(JSON.stringify({ 167 | 'mouseX': mouseX, 168 | 'mouseY': mouseY, 169 | 'sync': syncDataPacked 170 | }), { 171 | Mousedown: 'true' 172 | }); 173 | }; 174 | 175 | document.addEventListener('keydown', this.keydownCallback); 176 | document.addEventListener('mousedown', this.mousedownCallback); 177 | } 178 | 179 | stop() { 180 | if (this.keydownCallback) { 181 | document.removeEventListener('keydown', this.keydownCallback); 182 | } 183 | 184 | for (const connection of this.receiveDataConnections) { 185 | this.connection.disconnect(JetcodeSocket.RECEIVE_DATA, connection); 186 | } 187 | } 188 | 189 | keyDown(char: string, callback, syncPackName: string, syncData: SyncObjectInterface[] = []) { 190 | char = char.toUpperCase(); 191 | 192 | if (!this.trackedKeys.includes(char)) { 193 | this.trackedKeys.push(char); 194 | } 195 | 196 | this.userKeydownCallbacks.set(char, [callback, syncPackName, syncData]); 197 | } 198 | 199 | removeKeyDownHandler(char) { 200 | char = char.toUpperCase(); 201 | 202 | this.userKeydownCallbacks.delete(char); 203 | } 204 | 205 | mouseDown(callback: CallableFunction, syncPackName: string, syncData: SyncObjectInterface[] = []): void { 206 | this.userMousedownCallback = [callback, syncPackName, syncData]; 207 | } 208 | 209 | removeMouseDownHandler() { 210 | this.userMousedownCallback = null; 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/MultiplayerGame.ts: -------------------------------------------------------------------------------- 1 | import { JetcodeSocket, JetcodeSocketConnection } from './jmp'; 2 | import { SharedData } from './SharedData'; 3 | import { Player } from './Player'; 4 | import { Game, Locale } from './Game'; 5 | import { MultiplayerSprite } from './MultiplayerSprite'; 6 | import { SyncObjectInterface } from './SyncObjectInterface'; 7 | 8 | export class MultiplayerGame extends Game { 9 | connection: any; 10 | 11 | private autoSyncGameTimeout: number = 0; 12 | private onConnectionCallback: (connection: JetcodeSocketConnection) => void; 13 | private onReceiveCallback: (data: any, parameters: any, isMe: boolean) => void; 14 | private onMemberJoinedCallback: (parameters: any, isMe: boolean) => void; 15 | private onMemberLeftCallback: (parameters: any, isMe: boolean) => void; 16 | private onGameStartedCallback: (players: Player[], parameters: any) => void; 17 | private onGameStoppedCallback: (parameters: any) => void; 18 | private onMultiplayerErrorCallback: (parameters: any) => void; 19 | private players: Player[] = []; 20 | private sharedObjects: SharedData[] = []; 21 | private isHost: boolean; 22 | 23 | constructor( 24 | socketUrl: string, 25 | gameToken: string, 26 | width: number, 27 | height: number, 28 | canvasId: string = null, 29 | displayErrors = true, 30 | locale: Locale = 'ru', 31 | lobbyId: string | number = 0, 32 | autoSyncGame: number = 0, 33 | multiplayerOptions: any = {} 34 | ) { 35 | super(width, height, canvasId, displayErrors, locale); 36 | 37 | this.autoSyncGameTimeout = autoSyncGame; 38 | 39 | this.initializeConnection(socketUrl, gameToken, lobbyId, multiplayerOptions); 40 | } 41 | 42 | send(userData: any, parameters: any = {}, syncPackName: string, syncData: SyncObjectInterface[] = []): void { 43 | if (!this.connection) { 44 | throw new Error('Connection to the server is not established.'); 45 | } 46 | 47 | const data = { 48 | 'data': userData, 49 | 'sync': this.packSyncData(syncPackName, syncData) 50 | } 51 | 52 | this.connection.sendData(JSON.stringify(data), parameters); 53 | } 54 | 55 | sync(syncPackName: string, syncData: SyncObjectInterface[] = [], parameters: any = {}): void { 56 | if (!syncData.length) { 57 | return; 58 | } 59 | 60 | parameters.SyncGame = 'true'; 61 | const data = this.packSyncData(syncPackName, syncData); 62 | 63 | this.sendData(JSON.stringify(data), parameters); 64 | } 65 | 66 | syncGame() { 67 | const syncObjects = this.getSyncObjects(); 68 | const syncData = this.packSyncData('game', syncObjects); 69 | 70 | this.sendData(JSON.stringify(syncData), { 71 | SyncGame: 'true' 72 | }); 73 | } 74 | 75 | onConnection(callback): void { 76 | this.onConnectionCallback = callback; 77 | } 78 | 79 | removeConnectionHandler(callback): void { 80 | this.onConnectionCallback = null; 81 | } 82 | 83 | onReceive(callback): void { 84 | this.onReceiveCallback = callback; 85 | } 86 | 87 | removeReceiveHandler(callback): void { 88 | this.onReceiveCallback = null; 89 | } 90 | 91 | onMemberJoined(callback): void { 92 | this.onMemberJoinedCallback = callback; 93 | } 94 | 95 | removeMemberJoinedHandler(callback): void { 96 | this.onMemberJoinedCallback = null; 97 | } 98 | 99 | onMemberLeft(callback): void { 100 | this.onMemberLeftCallback = callback; 101 | } 102 | 103 | removeMemberLeftHandler(callback): void { 104 | this.onMemberLeftCallback = null; 105 | } 106 | 107 | onGameStarted(callback): void { 108 | this.onGameStartedCallback = callback; 109 | } 110 | 111 | removeGameStartedHandler(callback): void { 112 | this.onGameStartedCallback = null; 113 | } 114 | 115 | onGameStopped(callback): void { 116 | this.onGameStoppedCallback = callback; 117 | } 118 | 119 | removeGameStoppedHandler(callback): void { 120 | this.onGameStoppedCallback = null; 121 | } 122 | 123 | onMultiplayerError(callback): void { 124 | this.onMultiplayerErrorCallback = callback; 125 | } 126 | 127 | removeMultiplayerErrorHandler(callback): void { 128 | this.onMultiplayerErrorCallback = null; 129 | } 130 | 131 | run() { 132 | super.run(); 133 | 134 | if (this.isHost && this.autoSyncGameTimeout) { 135 | this.autoSyncGame(this.autoSyncGameTimeout); 136 | } 137 | } 138 | 139 | stop(): void { 140 | super.stop(); 141 | 142 | for (const player of this.players) { 143 | player.delete(); 144 | } 145 | 146 | this.players = []; 147 | } 148 | 149 | getPlayers(): Player[] { 150 | return this.players; 151 | } 152 | 153 | addSharedObject(sharedObject: SharedData): void { 154 | this.sharedObjects.push(sharedObject); 155 | } 156 | 157 | removeSharedObject(sharedObject: SharedData): void { 158 | const index = this.sharedObjects.indexOf(sharedObject); 159 | 160 | if (index > -1) { 161 | this.sharedObjects.splice(index, 1); 162 | } 163 | } 164 | 165 | getSharedObjects(): SharedData[] { 166 | return this.sharedObjects; 167 | } 168 | 169 | getMultiplayerSprites(): MultiplayerSprite[] { 170 | if (!this.getActiveStage()) { 171 | return []; 172 | } 173 | 174 | return this.getActiveStage().getSprites().filter((sprite) => { 175 | return sprite instanceof MultiplayerSprite; 176 | }) as MultiplayerSprite[]; 177 | } 178 | 179 | getSyncObjects(): SyncObjectInterface[] { 180 | const multiplayerSprites = this.getMultiplayerSprites(); 181 | const players = this.getPlayers(); 182 | const sharedObjects = this.getSharedObjects(); 183 | 184 | return [...multiplayerSprites, ...players, ...sharedObjects]; 185 | } 186 | 187 | syncObjects(syncData: any, deltaTime: number) { 188 | const gameAllSyncObjects = this.getSyncObjects(); 189 | 190 | for (const [syncPackName, syncObjectsData] of Object.entries(syncData)) { 191 | for (const syncObject of gameAllSyncObjects) { 192 | if (syncObjectsData[syncObject.getMultiplayerName()]) { 193 | const syncPackData = syncObjectsData[syncObject.getMultiplayerName()]; 194 | syncObject.setSyncData(syncPackName, syncPackData, deltaTime); 195 | } 196 | } 197 | } 198 | } 199 | 200 | packSyncData(packName: string, syncObjects: SyncObjectInterface[]): any { 201 | const syncObjectsData = {}; 202 | 203 | for (const syncObject of syncObjects) { 204 | syncObjectsData[syncObject.getMultiplayerName()] = syncObject.getSyncData(); 205 | syncObjectsData[syncObject.getMultiplayerName()]['syncId'] = syncObject.increaseSyncId(); 206 | } 207 | 208 | const result = {}; 209 | result[packName] = syncObjectsData; 210 | 211 | return result; 212 | } 213 | 214 | private sendData(data: any, parameters: any = {}): void { 215 | if (!this.connection) { 216 | throw new Error('Connection to the server is not established.'); 217 | } 218 | 219 | this.connection.sendData(data, parameters); 220 | } 221 | 222 | calcDeltaTime(sendTime: number): number { 223 | return Date.now() - sendTime - this.connection.deltaTime; 224 | } 225 | 226 | extrapolate(callback: CallableFunction, deltaTime: number, timeout: number): void { 227 | const times = Math.round((deltaTime / timeout) * 0.75); 228 | 229 | for (let i = 0; i < times; i++) { 230 | callback(); 231 | } 232 | } 233 | 234 | private async initializeConnection(socketUrl: string, gameToken: string, lobbyId: string | number, multiplayerOptions: any = {}) { 235 | const socket = new JetcodeSocket(socketUrl); 236 | 237 | try { 238 | this.connection = await socket.connect(gameToken, lobbyId, multiplayerOptions); 239 | 240 | if (this.onConnectionCallback) { 241 | this.onConnectionCallback(this.connection); 242 | } 243 | 244 | this.connection.connect(JetcodeSocket.RECEIVE_DATA, (data: any, parameters: any, isMe: boolean) => { 245 | if (!data || !this.running || !parameters.SendTime) { 246 | return; 247 | } 248 | 249 | if (parameters.SyncGame === 'true') { 250 | const syncObjectsData = JSON.parse(data); 251 | this.syncObjects(syncObjectsData, this.calcDeltaTime(parameters.SendTime)); 252 | 253 | } else if (parameters.Keydown !== 'true' && parameters.Mousedown !== 'true' && this.onReceiveCallback) { 254 | data = JSON.parse(data); 255 | 256 | const userData = data['userData']; 257 | const syncSpritesData = data['sync']; 258 | 259 | this.syncObjects(syncSpritesData, this.calcDeltaTime(parameters.SendTime)); 260 | 261 | this.onReceiveCallback(userData, parameters, isMe); 262 | } 263 | }); 264 | 265 | this.connection.connect(JetcodeSocket.MEMBER_JOINED, (parameters: any, isMe: boolean) => { 266 | if (this.onMemberJoinedCallback) { 267 | this.onMemberJoinedCallback(parameters, isMe); 268 | } 269 | }); 270 | 271 | this.connection.connect(JetcodeSocket.MEMBER_LEFT, (parameters: any, isMe: boolean) => { 272 | if (this.onMemberLeftCallback) { 273 | this.onMemberLeftCallback(parameters, isMe); 274 | } 275 | }); 276 | 277 | this.connection.connect(JetcodeSocket.GAME_STARTED, (parameters: any) => { 278 | const hostId = parameters.HostId; 279 | const playerIds = parameters.Members?.split(',') ?? []; 280 | 281 | this.players = playerIds.map((playerId) => { 282 | return new Player(playerId, playerId === this.connection.memberId, this); 283 | }); 284 | 285 | this.isHost = hostId === this.connection.memberId; 286 | 287 | if (this.onGameStartedCallback) { 288 | this.onGameStartedCallback(this.players, parameters); 289 | } 290 | 291 | // if (this.isHost) { 292 | // this.syncGame(); 293 | // } 294 | }); 295 | 296 | this.connection.connect(JetcodeSocket.GAME_STOPPED, (parameters: any) => { 297 | if (this.onGameStoppedCallback) { 298 | this.onGameStoppedCallback(parameters); 299 | } 300 | }); 301 | 302 | this.connection.connect(JetcodeSocket.ERROR, (parameters: any) => { 303 | if (this.onMultiplayerError) { 304 | this.onMultiplayerError(parameters); 305 | } 306 | }); 307 | 308 | } catch (error) { 309 | console.error(error); 310 | } 311 | } 312 | 313 | private autoSyncGame(timeout: number) { 314 | const hander = () => { 315 | this.syncGame(); 316 | }; 317 | 318 | setInterval(hander, timeout); 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /src/MultiplayerSprite.ts: -------------------------------------------------------------------------------- 1 | import { SyncObjectInterface } from './SyncObjectInterface'; 2 | import { OrphanSharedData } from './OrphanSharedData'; 3 | import { Stage } from './Stage'; 4 | import { Sprite } from './Sprite'; 5 | 6 | export class MultiplayerSprite extends Sprite implements SyncObjectInterface { 7 | private multiplayerName: string; 8 | private syncId: number; 9 | private reservedProps: string[]; 10 | private syncCallback: CallableFunction; 11 | 12 | constructor(multiplayerName: string, stage: Stage = null, layer = 1, costumePaths = []) { 13 | super(stage, layer, costumePaths) 14 | 15 | this.multiplayerName = 'sprite_' + multiplayerName; 16 | this.syncId = 1; 17 | 18 | this.reservedProps = Object.keys(this); 19 | this.reservedProps.push('body'); 20 | this.reservedProps.push('reservedProps'); 21 | } 22 | 23 | generateUniqueId(): string { 24 | return Math.random().toString(36).slice(2) + '-' + Math.random().toString(36).slice(2); 25 | } 26 | 27 | getCustomerProperties() { 28 | const data = {}; 29 | 30 | for (const key of Object.keys(this)) { 31 | if (this.reservedProps.includes(key)) { 32 | continue; 33 | } 34 | 35 | data[key] = this[key]; 36 | } 37 | 38 | return data; 39 | } 40 | 41 | getMultiplayerName(): string { 42 | return this.multiplayerName; 43 | } 44 | 45 | getSyncId(): number { 46 | return this.syncId; 47 | } 48 | 49 | increaseSyncId(): number { 50 | this.syncId++; 51 | 52 | return this.syncId; 53 | } 54 | 55 | getSyncData() { 56 | return Object.assign({}, this.getCustomerProperties(), { 57 | size: this.size, 58 | rotateStyle: this.rotateStyle, 59 | costumeIndex: this.costumeIndex, 60 | deleted: this._deleted, 61 | x: this.x, 62 | y: this.y, 63 | direction: this.direction, 64 | hidden: this.hidden, 65 | stopped: this.stopped, 66 | }); 67 | } 68 | 69 | setSyncData(packName: string, data: any, deltaTime: number) { 70 | const oldData = {}; 71 | 72 | for (const key in data) { 73 | if (data.hasOwnProperty(key) && !this.reservedProps.includes(key)) { 74 | oldData[key] = this[key]; 75 | 76 | this[key] = data[key]; 77 | } 78 | } 79 | 80 | if (this.syncCallback) { 81 | this.syncCallback(this, packName, data, oldData, deltaTime); 82 | } 83 | } 84 | 85 | onSync(callback: CallableFunction): void { 86 | this.syncCallback = callback; 87 | } 88 | 89 | removeSyncHandler(): void { 90 | this.syncCallback = null; 91 | } 92 | 93 | only(...properties): OrphanSharedData { 94 | return new OrphanSharedData(this, properties); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/OrphanSharedData.ts: -------------------------------------------------------------------------------- 1 | import { SyncObjectInterface } from './SyncObjectInterface'; 2 | 3 | export class OrphanSharedData implements SyncObjectInterface { 4 | private parent: SyncObjectInterface; 5 | private properties: string[]; 6 | 7 | constructor(parent: SyncObjectInterface, properties: string[]) { 8 | this.parent = parent; 9 | this.properties = properties; 10 | } 11 | 12 | getMultiplayerName(): string { 13 | return this.parent.getMultiplayerName(); 14 | } 15 | 16 | getSyncId(): number { 17 | return this.parent.getSyncId(); 18 | } 19 | 20 | increaseSyncId(): number { 21 | return this.parent.increaseSyncId(); 22 | } 23 | 24 | getSyncData() { 25 | const syncData = {}; 26 | for (const property of this.properties) { 27 | if (this.parent[property]) { 28 | syncData[property] = this.parent[property]; 29 | } 30 | } 31 | 32 | return syncData; 33 | } 34 | 35 | setSyncData(packName: string, data: any, deltaTime: number) { 36 | this.parent.setSyncData(packName, data, deltaTime); 37 | } 38 | 39 | onSync(callback: CallableFunction) { 40 | throw new Error('Not implemented.') 41 | } 42 | 43 | removeSyncHandler() { 44 | throw new Error('Not implemented.') 45 | } 46 | 47 | only(...properties): OrphanSharedData { 48 | throw new Error('Not implemented.') 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Player.ts: -------------------------------------------------------------------------------- 1 | import { SyncObjectInterface } from './SyncObjectInterface'; 2 | import { MultiplayerControl } from './MultiplayerControl'; 3 | import { MultiplayerGame } from './MultiplayerGame'; 4 | import { OrphanSharedData } from './OrphanSharedData'; 5 | 6 | export class Player implements SyncObjectInterface { 7 | control: MultiplayerControl; 8 | id: string; 9 | 10 | private _isMe: boolean; 11 | private game: MultiplayerGame; 12 | private deleted = false; 13 | private reservedProps: string[]; 14 | private multiplayerName: string; 15 | private syncId: number; 16 | private syncCallback: CallableFunction; 17 | 18 | constructor(id: string, isMe: boolean, game: MultiplayerGame) { 19 | this.id = id; 20 | this._isMe = isMe; 21 | this.game = game; 22 | this.multiplayerName = 'player_' + id; 23 | this.syncId = 1; 24 | 25 | this.control = new MultiplayerControl(this, this.game, game.connection, isMe); 26 | 27 | this.reservedProps = Object.keys(this); 28 | this.reservedProps.push('reservedProps'); 29 | } 30 | 31 | keyDown(char: string, callback: CallableFunction, syncPackName: string, syncData: SyncObjectInterface[] = []): void { 32 | this.control.keyDown(char, callback, syncPackName, syncData); 33 | } 34 | 35 | removeKeyDownHandler(char) { 36 | this.control.removeKeyDownHandler(char); 37 | } 38 | 39 | mouseDown(callback: CallableFunction, syncPackName: string, syncData: SyncObjectInterface[] = []): void { 40 | this.control.mouseDown(callback, syncPackName, syncData); 41 | } 42 | 43 | removeMouseDownHandler() { 44 | this.control.removeMouseDownHandler(); 45 | } 46 | 47 | isMe() { 48 | return this._isMe; 49 | } 50 | 51 | delete(): void { 52 | if (this.deleted) { 53 | return; 54 | } 55 | 56 | this.control.stop(); 57 | 58 | let props = Object.keys(this); 59 | for (let i = 0; i < props.length; i++) { 60 | delete this[props[i]]; 61 | } 62 | 63 | this.deleted = true; 64 | } 65 | 66 | repeat(i: number, callback: CallableFunction, timeout, finishCallback: CallableFunction) { 67 | if (this.deleted) { 68 | finishCallback(); 69 | 70 | return; 71 | } 72 | 73 | if (i < 1) { 74 | finishCallback(); 75 | 76 | return; 77 | } 78 | 79 | const result = callback(this); 80 | if (result === false) { 81 | finishCallback(); 82 | 83 | return; 84 | } 85 | 86 | if (result > 0) { 87 | timeout = result; 88 | } 89 | 90 | i--; 91 | if (i < 1) { 92 | finishCallback(); 93 | 94 | return; 95 | } 96 | 97 | setTimeout(() => { 98 | requestAnimationFrame(() => this.repeat(i, callback, timeout, finishCallback)); 99 | }, timeout); 100 | } 101 | 102 | forever(callback, timeout = null): void { 103 | if (this.deleted) { 104 | return; 105 | } 106 | 107 | const result = callback(this); 108 | if (result === false) { 109 | return; 110 | } 111 | 112 | if (result > 0) { 113 | timeout = result; 114 | } 115 | 116 | if (timeout) { 117 | setTimeout(() => { 118 | requestAnimationFrame(() => this.forever(callback, timeout)); 119 | }, timeout); 120 | 121 | } else { 122 | requestAnimationFrame(() => this.forever(callback)); 123 | } 124 | } 125 | 126 | timeout(callback, timeout: number): void { 127 | setTimeout(() => { 128 | if (this.deleted) { 129 | return; 130 | } 131 | 132 | requestAnimationFrame(() => callback(this)); 133 | }, timeout); 134 | } 135 | 136 | getMultiplayerName(): string { 137 | return this.multiplayerName; 138 | } 139 | 140 | getSyncId(): number { 141 | return this.syncId; 142 | } 143 | 144 | increaseSyncId(): number { 145 | this.syncId++; 146 | 147 | return this.syncId; 148 | } 149 | 150 | getSyncData() { 151 | const data = {}; 152 | 153 | for (const key of Object.keys(this)) { 154 | if (this.reservedProps.includes(key)) { 155 | continue; 156 | } 157 | 158 | data[key] = this[key]; 159 | } 160 | 161 | return data; 162 | } 163 | 164 | setSyncData(packName: string, data: any, deltaTime: number) { 165 | const oldData = {}; 166 | 167 | for (const key in data) { 168 | if (data.hasOwnProperty(key) && !this.reservedProps.includes(key)) { 169 | oldData[key] = this[key]; 170 | 171 | this[key] = data[key]; 172 | } 173 | } 174 | 175 | if (this.syncCallback) { 176 | this.syncCallback(this, packName, data, oldData, deltaTime); 177 | } 178 | } 179 | 180 | onSync(callback: CallableFunction): void { 181 | this.syncCallback = callback; 182 | } 183 | 184 | removeSyncHandler(): void { 185 | this.syncCallback = null; 186 | } 187 | 188 | only(...properties): OrphanSharedData { 189 | return new OrphanSharedData(this, properties); 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/ScheduledCallbackExecutor.ts: -------------------------------------------------------------------------------- 1 | import { Stage } from './Stage'; 2 | import { Sprite } from './Sprite'; 3 | import { ScheduledCallbackItem } from './ScheduledCallbackItem'; 4 | 5 | export class ScheduledCallbackExecutor { 6 | constructor(private context: Stage | Sprite) { 7 | } 8 | 9 | execute(now: number, diffTime: number) { 10 | return (item: ScheduledCallbackItem) => { 11 | const state = item.state; 12 | 13 | if (this.context instanceof Sprite) { 14 | if (this.context.deleted) { 15 | return false; 16 | } 17 | 18 | if (this.context.stopped) { 19 | return true; 20 | } 21 | } 22 | 23 | if (item.timeout && diffTime) { 24 | item.timeout += diffTime; 25 | } 26 | 27 | if (!item.timeout || item.timeout <= now) { 28 | const result = item.callback.bind(this.context)(this.context, state); 29 | 30 | if (state.maxIterations) { 31 | state.currentIteration++; 32 | } 33 | 34 | const isFinished = 35 | result === false || 36 | (item.timeout && !state.interval && !state.maxIterations) || 37 | (state.maxIterations && state.currentIteration >= state.maxIterations) 38 | ; 39 | 40 | if (isFinished) { 41 | if (item.finishCallback) { 42 | item.finishCallback(this.context, state); 43 | } 44 | 45 | return false; 46 | } 47 | 48 | if (state.interval) { 49 | item.timeout = now + state.interval; 50 | } 51 | } 52 | 53 | return true; 54 | }; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/ScheduledCallbackItem.ts: -------------------------------------------------------------------------------- 1 | import { ScheduledCallbackFunction } from './Game'; 2 | import { ScheduledState } from './ScheduledState'; 3 | 4 | export class ScheduledCallbackItem { 5 | callback: ScheduledCallbackFunction; 6 | state: ScheduledState; 7 | timeout?: number; 8 | finishCallback?: ScheduledCallbackFunction; 9 | control: any; 10 | 11 | constructor(callback: ScheduledCallbackFunction, state: ScheduledState, timeout?: number, finishCallback?: ScheduledCallbackFunction) { 12 | this.callback = callback; 13 | this.state = state; 14 | this.timeout = timeout; 15 | this.finishCallback = finishCallback; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/ScheduledState.ts: -------------------------------------------------------------------------------- 1 | export class ScheduledState { 2 | interval: number; 3 | maxIterations?: number; 4 | currentIteration?: number; 5 | 6 | constructor(interval: number, maxIterations?: number, currentIteration?: number) { 7 | this.interval = interval; 8 | this.maxIterations = maxIterations; 9 | this.currentIteration = currentIteration; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/SharedData.ts: -------------------------------------------------------------------------------- 1 | import { SyncObjectInterface } from './SyncObjectInterface'; 2 | import { OrphanSharedData } from './OrphanSharedData'; 3 | import { Registry } from './utils'; 4 | 5 | export class SharedData implements SyncObjectInterface { 6 | private multiplayerName: string; 7 | private syncId: number; 8 | private syncCallback: CallableFunction; 9 | 10 | constructor(multiplayerName: string) { 11 | this.multiplayerName = 'data_' + multiplayerName; 12 | this.syncId = 1; 13 | 14 | if (!Registry.getInstance().has('game')) { 15 | throw new Error('You need create Game instance before Sprite instance.'); 16 | } 17 | 18 | const game = Registry.getInstance().get('game'); 19 | game.addSharedObject(this); 20 | } 21 | 22 | generateUniqueId(): string { 23 | return Math.random().toString(36).slice(2) + '-' + Math.random().toString(36).slice(2); 24 | } 25 | 26 | getMultiplayerName(): string { 27 | return this.multiplayerName; 28 | } 29 | 30 | getSyncId(): number { 31 | return this.syncId; 32 | } 33 | 34 | increaseSyncId(): number { 35 | this.syncId++; 36 | 37 | return this.syncId; 38 | } 39 | 40 | getSyncData() { 41 | const data = {}; 42 | 43 | for (const key of Object.keys(this)) { 44 | data[key] = this[key]; 45 | } 46 | 47 | return data; 48 | } 49 | 50 | setSyncData(packName: string, data: any, deltaTime: number) { 51 | const oldData = {}; 52 | 53 | for (const key in data) { 54 | if (data.hasOwnProperty(key)) { 55 | oldData[key] = this[key]; 56 | 57 | this[key] = data[key]; 58 | } 59 | } 60 | 61 | if (this.syncCallback) { 62 | this.syncCallback(this, packName, data, oldData, deltaTime); 63 | } 64 | } 65 | 66 | onSync(callback: CallableFunction): void { 67 | this.syncCallback = callback; 68 | } 69 | 70 | removeSyncHandler(): void { 71 | this.syncCallback = null; 72 | } 73 | 74 | only(...properties): OrphanSharedData { 75 | return new OrphanSharedData(this, properties); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/SyncObjectInterface.ts: -------------------------------------------------------------------------------- 1 | import { OrphanSharedData } from './OrphanSharedData'; 2 | 3 | export interface SyncObjectInterface { 4 | getSyncId(): number; 5 | 6 | increaseSyncId(): number; 7 | 8 | getMultiplayerName(): string; 9 | 10 | getSyncData(): any; 11 | 12 | setSyncData(packName: string, data: any, deltaTime: number): void; 13 | 14 | onSync(callback: CallableFunction): void; 15 | 16 | removeSyncHandler(): void; 17 | 18 | only(...properties): OrphanSharedData; 19 | } 20 | -------------------------------------------------------------------------------- /src/browser/scrub.ts: -------------------------------------------------------------------------------- 1 | import * as Module from '../scrub' 2 | 3 | Object.assign(window, Module); 4 | -------------------------------------------------------------------------------- /src/collisions/BVHBranch.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @private 3 | */ 4 | const branch_pool = []; 5 | 6 | /** 7 | * A branch within a BVH 8 | * @class 9 | * @private 10 | */ 11 | export class BVHBranch { 12 | protected _bvh_parent = null; 13 | protected _bvh_branch = true; 14 | protected _bvh_left = null; 15 | protected _bvh_right = null; 16 | protected _bvh_sort = 0; 17 | protected _bvh_min_x = 0; 18 | protected _bvh_min_y = 0; 19 | protected _bvh_max_x = 0; 20 | protected _bvh_max_y = 0; 21 | 22 | /** 23 | * Returns a branch from the branch pool or creates a new branch 24 | * @returns {BVHBranch} 25 | */ 26 | static getBranch() { 27 | if (branch_pool.length) { 28 | return branch_pool.pop(); 29 | } 30 | 31 | return new BVHBranch(); 32 | } 33 | 34 | /** 35 | * Releases a branch back into the branch pool 36 | * @param {BVHBranch} branch The branch to release 37 | */ 38 | static releaseBranch(branch) { 39 | branch_pool.push(branch); 40 | } 41 | 42 | /** 43 | * Sorting callback used to sort branches by deepest first 44 | * @param {BVHBranch} a The first branch 45 | * @param {BVHBranch} b The second branch 46 | * @returns {Number} 47 | */ 48 | static sortBranches(a, b) { 49 | return a.sort > b.sort ? -1 : 1; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/collisions/CircleCollider.ts: -------------------------------------------------------------------------------- 1 | import { Collider } from './Collider'; 2 | 3 | /** 4 | * A circle used to detect collisions 5 | * @class 6 | */ 7 | export class CircleCollider extends Collider { 8 | radius: number; 9 | scale: number; 10 | 11 | /** 12 | * @constructor 13 | * @param {Number} [x = 0] The starting X coordinate 14 | * @param {Number} [y = 0] The starting Y coordinate 15 | * @param {Number} [radius = 0] The radius 16 | * @param {Number} [scale = 1] The scale 17 | * @param {Number} [padding = 5] The amount to pad the bounding volume when testing for potential collisions 18 | */ 19 | constructor(x = 0, y = 0, radius = 0, scale = 1, padding = 5) { 20 | super(x, y, padding); 21 | 22 | this.radius = radius; 23 | this.scale = scale; 24 | } 25 | 26 | /** 27 | * Draws the circle to a CanvasRenderingContext2D's current path 28 | * @param {CanvasRenderingContext2D} context The context to add the arc to 29 | */ 30 | draw(context) { 31 | const x = this.x; 32 | const y = this.y; 33 | const radius = this.radius * this.scale; 34 | 35 | context.moveTo(x + radius, y); 36 | context.arc(x, y, radius, 0, Math.PI * 2); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/collisions/Collider.ts: -------------------------------------------------------------------------------- 1 | import { SAT } from './SAT'; 2 | import { CollisionResult } from './CollisionResult'; 3 | 4 | /** 5 | * The base class for bodies used to detect collisions 6 | * @class 7 | * @protected 8 | */ 9 | export class Collider { 10 | /** 11 | * The X coordinate of the body 12 | */ 13 | x: number; 14 | 15 | /** 16 | * The Y coordinate of the body 17 | */ 18 | y: number; 19 | 20 | /** 21 | * The width of the body 22 | */ 23 | width: number; 24 | 25 | /** 26 | * The width of the body 27 | */ 28 | height: number 29 | 30 | /** 31 | * The amount to pad the bounding volume when testing for potential collisions 32 | */ 33 | padding: number; 34 | 35 | /** 36 | * The offset of the body along X axis 37 | */ 38 | protected _offset_x = 0; 39 | 40 | /** 41 | * The offset of the body along Y axis 42 | */ 43 | protected _offset_y = 0; 44 | 45 | protected _circle = false; 46 | protected _polygon = false; 47 | protected _point = false; 48 | protected _bvh = null; 49 | protected _bvh_parent = null; 50 | protected _bvh_branch = false; 51 | protected _bvh_padding: number; 52 | protected _bvh_min_x = 0; 53 | protected _bvh_min_y = 0; 54 | protected _bvh_max_x = 0; 55 | protected _bvh_max_y = 0; 56 | protected _parent_sprite = null; 57 | protected _center_distance = 0 58 | protected _center_angle = 0; 59 | 60 | /** 61 | * @constructor 62 | * @param {Number} [x = 0] The starting X coordinate 63 | * @param {Number} [y = 0] The starting Y coordinate 64 | * @param {Number} [padding = 5] The amount to pad the bounding volume when testing for potential collisions 65 | */ 66 | constructor(x = 0, y = 0, padding = 5) { 67 | this.x = x; 68 | this.y = y; 69 | this.padding = padding; 70 | this._bvh_padding = padding; 71 | } 72 | 73 | /** 74 | * Determines if the body is colliding with another body 75 | * @param {CircleCollider|PolygonCollider|PointCollider} target The target body to test against 76 | * @param {CollisionResult} [result = null] A Result object on which to store information about the collision 77 | * @param {Boolean} [aabb = true] Set to false to skip the AABB test (useful if you use your own potential collision heuristic) 78 | * @returns {Boolean} 79 | */ 80 | collides(target, result = null, aabb = true) { 81 | return SAT(this, target, result, aabb); 82 | } 83 | 84 | /** 85 | * Returns a list of potential collisions 86 | * @returns {Array} 87 | */ 88 | potentials() { 89 | const bvh = this._bvh; 90 | 91 | if (bvh === null) { 92 | throw new Error('Body does not belong to a collision system'); 93 | } 94 | 95 | return bvh.potentials(this); 96 | } 97 | 98 | /** 99 | * Removes the body from its current collision system 100 | */ 101 | remove() { 102 | const bvh = this._bvh; 103 | 104 | if (bvh) { 105 | bvh.remove(this, false); 106 | } 107 | } 108 | 109 | set parentSprite(value) { 110 | this._parent_sprite = value; 111 | } 112 | 113 | get parentSprite() { 114 | return this._parent_sprite; 115 | } 116 | 117 | set offset_x(value) { 118 | this._offset_x = -value; 119 | this.updateCenterParams() 120 | } 121 | 122 | get offset_x() { 123 | return -this._offset_x; 124 | } 125 | 126 | set offset_y(value) { 127 | this._offset_y = -value; 128 | this.updateCenterParams() 129 | } 130 | 131 | get offset_y() { 132 | return -this._offset_y; 133 | } 134 | 135 | get center_offset_x() { 136 | if (this._parent_sprite.rotateStyle === 'leftRight' || this._parent_sprite.rotateStyle === 'none') { 137 | const leftRightMultiplier = this._parent_sprite._direction > 180 && this._parent_sprite.rotateStyle === 'leftRight' ? -1 : 1; 138 | return this._offset_x * leftRightMultiplier; 139 | } 140 | 141 | return this._center_distance * Math.cos(this._center_angle - this._parent_sprite.globalAngleRadians); 142 | } 143 | 144 | get center_offset_y() { 145 | if (this._parent_sprite.rotateStyle === 'leftRight' || this._parent_sprite.rotateStyle === 'none') { 146 | return -this._offset_y; 147 | } 148 | 149 | return -this._center_distance * Math.sin(this._center_angle - this._parent_sprite.globalAngleRadians); 150 | } 151 | 152 | /** 153 | * Creates a {@link CollisionResult} used to collect the detailed results of a collision test 154 | */ 155 | createResult() { 156 | return new CollisionResult(); 157 | } 158 | 159 | updateCenterParams(): void { 160 | this._center_distance = Math.hypot(this._offset_x, this._offset_y); 161 | this._center_angle = -Math.atan2(-this._offset_y, -this._offset_x); 162 | } 163 | 164 | /** 165 | * Creates a Result used to collect the detailed results of a collision test 166 | */ 167 | static createResult() { 168 | return new CollisionResult(); 169 | } 170 | 171 | } 172 | -------------------------------------------------------------------------------- /src/collisions/CollisionResult.ts: -------------------------------------------------------------------------------- 1 | import { CircleCollider } from './CircleCollider'; 2 | import { PolygonCollider } from './PolygonCollider'; 3 | import { PointCollider } from './PointCollider'; 4 | 5 | 6 | /** 7 | * An object used to collect the detailed results of a collision test 8 | * 9 | * > **Note:** It is highly recommended you recycle the same Result object if possible in order to avoid wasting memory 10 | * @class 11 | */ 12 | export class CollisionResult { 13 | /** 14 | * True if a collision was detected 15 | */ 16 | collision = false; 17 | 18 | /** 19 | * The source body tested 20 | */ 21 | a: CircleCollider | PolygonCollider | PointCollider = null; 22 | 23 | /** 24 | * The target body tested against 25 | */ 26 | b: CircleCollider | PolygonCollider | PointCollider = null; 27 | 28 | /** 29 | * True if A is completely contained within B 30 | */ 31 | a_in_b = false; 32 | 33 | /** 34 | * True if B is completely contained within A 35 | */ 36 | b_in_a = false; 37 | 38 | /** 39 | * The magnitude of the shortest axis of overlap 40 | */ 41 | overlap = 0; 42 | 43 | /** 44 | * The X direction of the shortest axis of overlap 45 | */ 46 | overlap_x = 0; 47 | 48 | /** 49 | * The Y direction of the shortest axis of overlap 50 | */ 51 | overlap_y = 0; 52 | } 53 | -------------------------------------------------------------------------------- /src/collisions/CollisionSystem.ts: -------------------------------------------------------------------------------- 1 | import { BVH } from './BVH'; 2 | import { CircleCollider } from './CircleCollider'; 3 | import { PolygonCollider } from './PolygonCollider'; 4 | import { PointCollider } from './PointCollider'; 5 | import { CollisionResult } from './CollisionResult'; 6 | import { SAT } from './SAT'; 7 | 8 | 9 | /** 10 | * A collision system used to track bodies in order to improve collision detection performance 11 | * @class 12 | */ 13 | export class CollisionSystem { 14 | protected _bvh: BVH; 15 | 16 | /** 17 | * @constructor 18 | */ 19 | constructor() { 20 | this._bvh = new BVH(); 21 | } 22 | 23 | /** 24 | * Creates a {@link CircleCollider} and inserts it into the collision system 25 | * @param {Number} [x = 0] The starting X coordinate 26 | * @param {Number} [y = 0] The starting Y coordinate 27 | * @param {Number} [radius = 0] The radius 28 | * @param {Number} [scale = 1] The scale 29 | * @param {Number} [padding = 0] The amount to pad the bounding volume when testing for potential collisions 30 | * @returns {CircleCollider} 31 | */ 32 | createCircle(x = 0, y = 0, radius = 0, scale = 1, padding = 0) { 33 | const body = new CircleCollider(x, y, radius, scale, padding); 34 | 35 | this._bvh.insert(body); 36 | 37 | return body; 38 | } 39 | 40 | /** 41 | * Creates a {@link PolygonCollider} and inserts it into the collision system 42 | * @param {Number} [x = 0] The starting X coordinate 43 | * @param {Number} [y = 0] The starting Y coordinate 44 | * @param {Array} [points = []] An array of coordinate pairs making up the polygon - [[x1, y1], [x2, y2], ...] 45 | * @param {Number} [angle = 0] The starting rotation in radians 46 | * @param {Number} [scale_x = 1] The starting scale along the X axis 47 | * @param {Number} [scale_y = 1] The starting scale long the Y axis 48 | * @param {Number} [padding = 0] The amount to pad the bounding volume when testing for potential collisions 49 | * @returns {PolygonCollider} 50 | */ 51 | createPolygon(x = 0, y = 0, points = [[0, 0]], angle = 0, scale_x = 1, scale_y = 1, padding = 0) { 52 | const body = new PolygonCollider(x, y, points, angle, scale_x, scale_y, padding); 53 | 54 | this._bvh.insert(body); 55 | 56 | return body; 57 | } 58 | 59 | /** 60 | * Creates a {@link PointCollider} and inserts it into the collision system 61 | * @param {Number} [x = 0] The starting X coordinate 62 | * @param {Number} [y = 0] The starting Y coordinate 63 | * @param {Number} [padding = 0] The amount to pad the bounding volume when testing for potential collisions 64 | * @returns {PointCollider} 65 | */ 66 | createPoint(x = 0, y = 0, padding = 0) { 67 | const body = new PointCollider(x, y, padding); 68 | 69 | this._bvh.insert(body); 70 | 71 | return body; 72 | } 73 | 74 | /** 75 | * Creates a {@link CollisionResult} used to collect the detailed results of a collision test 76 | */ 77 | createResult() { 78 | return new CollisionResult(); 79 | } 80 | 81 | /** 82 | * Creates a Result used to collect the detailed results of a collision test 83 | */ 84 | static createResult() { 85 | return new CollisionResult(); 86 | } 87 | 88 | /** 89 | * Inserts bodies into the collision system 90 | * @param {...Circle|...Polygon|...Point} bodies 91 | */ 92 | insert(...bodies) { 93 | for (const body of bodies) { 94 | this._bvh.insert(body, false); 95 | } 96 | 97 | return this; 98 | } 99 | 100 | /** 101 | * Removes bodies from the collision system 102 | * @param {...Circle|...Polygon|...Point} bodies 103 | */ 104 | remove(...bodies) { 105 | for (const body of bodies) { 106 | this._bvh.remove(body, false); 107 | } 108 | 109 | return this; 110 | } 111 | 112 | /** 113 | * Updates the collision system. This should be called before any collisions are tested. 114 | */ 115 | update() { 116 | this._bvh.update(); 117 | 118 | return this; 119 | } 120 | 121 | /** 122 | * Draws the bodies within the system to a CanvasRenderingContext2D's current path 123 | * @param {CanvasRenderingContext2D} context The context to draw to 124 | */ 125 | draw(context) { 126 | return this._bvh.draw(context); 127 | } 128 | 129 | /** 130 | * Draws the system's BVH to a CanvasRenderingContext2D's current path. This is useful for testing out different padding values for bodies. 131 | * @param {CanvasRenderingContext2D} context The context to draw to 132 | */ 133 | drawBVH(context) { 134 | return this._bvh.drawBVH(context); 135 | } 136 | 137 | /** 138 | * Returns a list of potential collisions for a body 139 | * @param {CircleCollider|PolygonCollider|PointCollider} body The body to test for potential collisions against 140 | * @returns {Array} 141 | */ 142 | potentials(body) { 143 | return this._bvh.potentials(body); 144 | } 145 | 146 | /** 147 | * Determines if two bodies are colliding 148 | * @param source 149 | * @param {CircleCollider|PolygonCollider|PointCollider} target The target body to test against 150 | * @param {CollisionResult} [result = null] A Result object on which to store information about the collision 151 | * @param {Boolean} [aabb = true] Set to false to skip the AABB test (useful if you use your own potential collision heuristic) 152 | * @returns {Boolean} 153 | */ 154 | collides(source, target, result = null, aabb = true) { 155 | return SAT(source, target, result, aabb); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/collisions/PointCollider.ts: -------------------------------------------------------------------------------- 1 | import { PolygonCollider } from './PolygonCollider'; 2 | 3 | /** 4 | * A point used to detect collisions 5 | * @class 6 | */ 7 | export class PointCollider extends PolygonCollider { 8 | /** 9 | * @constructor 10 | * @param {Number} [x = 0] The starting X coordinate 11 | * @param {Number} [y = 0] The starting Y coordinate 12 | * @param {Number} [padding = 5] The amount to pad the bounding volume when testing for potential collisions 13 | */ 14 | constructor(x = 0, y = 0, padding = 5) { 15 | super(x, y, [[0, 0]], 0, 1, 1, padding); 16 | 17 | /** @private */ 18 | this._point = true; 19 | } 20 | } 21 | 22 | PointCollider.prototype.setPoints = undefined; 23 | -------------------------------------------------------------------------------- /src/collisions/PolygonCollider.ts: -------------------------------------------------------------------------------- 1 | import { Collider } from './Collider'; 2 | 3 | /** 4 | * A polygon used to detect collisions 5 | * @class 6 | */ 7 | export class PolygonCollider extends Collider { 8 | /** 9 | * The angle of the body in radians 10 | */ 11 | angle: number; 12 | 13 | /** 14 | * The scale of the body along the X axis 15 | */ 16 | scale_x: number; 17 | 18 | /** 19 | * The scale of the body along the Y axis 20 | */ 21 | scale_y: number; 22 | 23 | protected _x: number; 24 | protected _y: number; 25 | protected _angle: number; 26 | protected _scale_x: number; 27 | protected _scale_y: number; 28 | protected _min_x = 0; 29 | protected _min_y = 0; 30 | protected _max_x = 0; 31 | protected _max_y = 0; 32 | protected _points = null; 33 | protected _coords = null; 34 | protected _edges = null; 35 | protected _normals = null; 36 | protected _dirty_coords = true; 37 | protected _dirty_normals = true; 38 | protected _origin_points = null; 39 | 40 | /** 41 | * @constructor 42 | * @param {Number} [x = 0] The starting X coordinate 43 | * @param {Number} [y = 0] The starting Y coordinate 44 | * @param {Array} [points = []] An array of coordinate pairs making up the polygon - [[x1, y1], [x2, y2], ...] 45 | * @param {Number} [angle = 0] The starting rotation in radians 46 | * @param {Number} [scale_x = 1] The starting scale along the X axis 47 | * @param {Number} [scale_y = 1] The starting scale long the Y axis 48 | * @param {Number} [padding = 5] The amount to pad the bounding volume when testing for potential collisions 49 | */ 50 | constructor(x = 0, y = 0, points = [], angle = 0, scale_x = 1, scale_y = 1, padding = 5) { 51 | super(x, y, padding); 52 | 53 | this.angle = angle; 54 | this.scale_x = scale_x; 55 | this.scale_y = scale_y; 56 | this._polygon = true; 57 | 58 | this._x = x; 59 | this._y = y; 60 | this._angle = angle; 61 | this._scale_x = scale_x; 62 | this._scale_y = scale_y; 63 | this._origin_points = points; 64 | 65 | PolygonCollider.prototype.setPoints.call(this, points); 66 | } 67 | 68 | /** 69 | * Draws the polygon to a CanvasRenderingContext2D's current path 70 | * @param {CanvasRenderingContext2D} context The context to add the shape to 71 | */ 72 | draw(context) { 73 | if ( 74 | this._dirty_coords || 75 | this.x !== this._x || 76 | this.y !== this._y || 77 | this.angle !== this._angle || 78 | this.scale_x !== this._scale_x || 79 | this.scale_y !== this._scale_y 80 | ) { 81 | this._calculateCoords(); 82 | } 83 | 84 | const coords = this._coords; 85 | 86 | if (coords.length === 2) { 87 | context.moveTo(coords[0], coords[1]); 88 | context.arc(coords[0], coords[1], 1, 0, Math.PI * 2); 89 | } else { 90 | context.moveTo(coords[0], coords[1]); 91 | 92 | for (let i = 2; i < coords.length; i += 2) { 93 | context.lineTo(coords[i], coords[i + 1]); 94 | } 95 | 96 | if (coords.length > 4) { 97 | context.lineTo(coords[0], coords[1]); 98 | } 99 | } 100 | } 101 | 102 | /** 103 | * Sets the points making up the polygon. It's important to use this function when changing the polygon's shape to ensure internal data is also updated. 104 | * @param {Array} new_points An array of coordinate pairs making up the polygon - [[x1, y1], [x2, y2], ...] 105 | */ 106 | setPoints(new_points) { 107 | const count = new_points.length; 108 | 109 | this._points = new Float64Array(count * 2); 110 | this._coords = new Float64Array(count * 2); 111 | this._edges = new Float64Array(count * 2); 112 | this._normals = new Float64Array(count * 2); 113 | 114 | const points = this._points; 115 | 116 | for (let i = 0, ix = 0, iy = 1; i < count; ++i, ix += 2, iy += 2) { 117 | const new_point = new_points[i]; 118 | 119 | points[ix] = new_point[0]; 120 | points[iy] = new_point[1]; 121 | } 122 | 123 | this._dirty_coords = true; 124 | } 125 | 126 | /** 127 | * Calculates and caches the polygon's world coordinates based on its points, angle, and scale 128 | */ 129 | _calculateCoords() { 130 | const x = this.x; 131 | const y = this.y; 132 | const angle = this.angle; 133 | const scale_x = this.scale_x; 134 | const scale_y = this.scale_y; 135 | const points = this._points; 136 | const coords = this._coords; 137 | const count = points.length; 138 | 139 | let min_x; 140 | let max_x; 141 | let min_y; 142 | let max_y; 143 | 144 | for (let ix = 0, iy = 1; ix < count; ix += 2, iy += 2) { 145 | let coord_x = points[ix] * scale_x; 146 | let coord_y = points[iy] * scale_y; 147 | 148 | if (angle) { 149 | const cos = Math.cos(angle); 150 | const sin = Math.sin(angle); 151 | const tmp_x = coord_x; 152 | const tmp_y = coord_y; 153 | 154 | coord_x = tmp_x * cos - tmp_y * sin; 155 | coord_y = tmp_x * sin + tmp_y * cos; 156 | } 157 | 158 | coord_x += x; 159 | coord_y += y; 160 | 161 | coords[ix] = coord_x; 162 | coords[iy] = coord_y; 163 | 164 | if (ix === 0) { 165 | min_x = max_x = coord_x; 166 | min_y = max_y = coord_y; 167 | } else { 168 | if (coord_x < min_x) { 169 | min_x = coord_x; 170 | } else if (coord_x > max_x) { 171 | max_x = coord_x; 172 | } 173 | 174 | if (coord_y < min_y) { 175 | min_y = coord_y; 176 | } else if (coord_y > max_y) { 177 | max_y = coord_y; 178 | } 179 | } 180 | } 181 | 182 | this._x = x; 183 | this._y = y; 184 | this._angle = angle; 185 | this._scale_x = scale_x; 186 | this._scale_y = scale_y; 187 | this._min_x = min_x; 188 | this._min_y = min_y; 189 | this._max_x = max_x; 190 | this._max_y = max_y; 191 | this._dirty_coords = false; 192 | this._dirty_normals = true; 193 | } 194 | 195 | /** 196 | * Calculates the normals and edges of the polygon's sides 197 | */ 198 | _calculateNormals() { 199 | const coords = this._coords; 200 | const edges = this._edges; 201 | const normals = this._normals; 202 | const count = coords.length; 203 | 204 | for (let ix = 0, iy = 1; ix < count; ix += 2, iy += 2) { 205 | const next = ix + 2 < count ? ix + 2 : 0; 206 | const x = coords[next] - coords[ix]; 207 | const y = coords[next + 1] - coords[iy]; 208 | const length = x || y ? Math.sqrt(x * x + y * y) : 0; 209 | 210 | edges[ix] = x; 211 | edges[iy] = y; 212 | normals[ix] = length ? y / length : 0; 213 | normals[iy] = length ? -x / length : 0; 214 | } 215 | 216 | this._dirty_normals = false; 217 | } 218 | 219 | get points() { 220 | return this._origin_points; 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /src/collisions/index.ts: -------------------------------------------------------------------------------- 1 | export * from './BVH'; 2 | export * from './BVHBranch'; 3 | export * from './CircleCollider'; 4 | export * from './Collider'; 5 | export * from './CollisionResult'; 6 | export * from './CollisionSystem'; 7 | export * from './PointCollider'; 8 | export * from './PolygonCollider'; 9 | export * from './SAT'; 10 | -------------------------------------------------------------------------------- /src/jmp/JetcodeSocket.ts: -------------------------------------------------------------------------------- 1 | import { JetcodeSocketParameters } from './JetcodeSocketParameters'; 2 | import { JetcodeSocketConnection } from './JetcodeSocketConnect'; 3 | 4 | export class JetcodeSocket { 5 | static JOIN_LOBBY = 'JOIN_LOBBY'; 6 | static LEAVE_LOBBY = 'LEAVE_LOBBY'; 7 | static SEND_DATA = 'SEND_DATA'; 8 | 9 | static JOINED = 'JOINED'; 10 | static RECEIVE_DATA = 'RECEIVE_DATA'; 11 | static MEMBER_JOINED = 'MEMBER_JOINED'; 12 | static MEMBER_LEFT = 'MEMBER_LEFT'; 13 | static GAME_STARTED = 'GAME_STARTED'; 14 | static GAME_STOPPED = 'GAME_STOPPED'; 15 | static ERROR = 'ERROR'; 16 | 17 | private socketUrl: string; 18 | private socket: WebSocket; 19 | private defaultParameters: JetcodeSocketParameters; 20 | 21 | constructor(socketUrl = 'ws://localhost:17500') { 22 | this.socketUrl = socketUrl; 23 | this.socket = null; 24 | 25 | this.defaultParameters = { 26 | 'LobbyAutoCreate': true, 27 | 'MaxMembers': 2, 28 | 'MinMembers': 2, 29 | 'StartGameWithMembers': 2 30 | } 31 | } 32 | 33 | connect(gameToken, lobbyId = null, inParameters = {}) { 34 | const parameters = {...this.defaultParameters, ...inParameters}; 35 | 36 | return new Promise((resolve, reject) => { 37 | this.socket = new WebSocket(this.socketUrl); 38 | 39 | this.socket.onopen = () => { 40 | const connection = new JetcodeSocketConnection( 41 | this.socket, 42 | gameToken, 43 | lobbyId 44 | ); 45 | 46 | connection.joinLobby(gameToken, lobbyId, parameters) 47 | .then(() => { 48 | resolve(connection); 49 | }) 50 | .catch(reject); 51 | }; 52 | 53 | this.socket.onerror = (error) => { 54 | reject(error); 55 | }; 56 | }); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/jmp/JetcodeSocketConnect.ts: -------------------------------------------------------------------------------- 1 | import { JetcodeSocket } from './JetcodeSocket'; 2 | 3 | export class JetcodeSocketConnection { 4 | socket: WebSocket; 5 | lobbyId: string | number; 6 | memberId: string; 7 | deltaTime: number; 8 | 9 | private connects: {}; 10 | private connectActions = [ 11 | JetcodeSocket.JOINED, 12 | JetcodeSocket.RECEIVE_DATA, 13 | JetcodeSocket.MEMBER_JOINED, 14 | JetcodeSocket.MEMBER_LEFT, 15 | JetcodeSocket.GAME_STARTED, 16 | JetcodeSocket.GAME_STOPPED, 17 | JetcodeSocket.ERROR 18 | ]; 19 | 20 | constructor(socket: WebSocket, gameToken, lobbyId = 0) { 21 | this.socket = socket; 22 | this.lobbyId = lobbyId; 23 | this.memberId = null; 24 | this.connects = {}; 25 | 26 | this._listenSocket(); 27 | } 28 | 29 | _listenSocket() { 30 | this.socket.onmessage = (event) => { 31 | const [action, parameters, value] = this._parse(event.data) 32 | 33 | if (action === JetcodeSocket.RECEIVE_DATA) { 34 | this.emit(JetcodeSocket.RECEIVE_DATA, [value, parameters, parameters?.MemberId === this.memberId]); 35 | 36 | } else if (action === JetcodeSocket.MEMBER_JOINED) { 37 | this.emit(JetcodeSocket.MEMBER_JOINED, [parameters, parameters?.MemberId === this.memberId]); 38 | 39 | } else if (action === JetcodeSocket.MEMBER_LEFT) { 40 | this.emit(JetcodeSocket.MEMBER_LEFT, [parameters, parameters?.MemberId === this.memberId]); 41 | 42 | } else if (this.connects[action]) { 43 | this.emit(action, [parameters]); 44 | } 45 | } 46 | } 47 | 48 | emit(action: string, args: any[]): void { 49 | if (this.connects[action]) { 50 | this.connects[action].forEach(callback => { 51 | callback(...args); 52 | }); 53 | } 54 | } 55 | 56 | connect(action, callback): CallableFunction { 57 | if (!this.connectActions.includes(action)) { 58 | throw new Error('This actions is not defined.'); 59 | } 60 | 61 | if (!this.connects[action]) { 62 | this.connects[action] = []; 63 | } 64 | 65 | this.connects[action].push(callback); 66 | 67 | return callback; 68 | } 69 | 70 | disconnect(action: string, callback: Function): void { 71 | if (!this.connectActions.includes(action)) { 72 | throw new Error('This action is not defined.'); 73 | } 74 | 75 | if (!this.connects[action]) { 76 | return; 77 | } 78 | 79 | this.connects[action] = this.connects[action].filter(cb => cb !== callback); 80 | } 81 | 82 | sendData(value, parameters = {}) { 83 | if (!this.lobbyId) { 84 | throw new Error('You are not in the lobby!'); 85 | } 86 | 87 | let request = `${JetcodeSocket.SEND_DATA}\n`; 88 | 89 | for (const [key, value] of Object.entries(parameters)) { 90 | request += key + '=' + value + '\n'; 91 | } 92 | 93 | request += `SendTime=${Date.now()}\n`; 94 | request += '\n' + value; 95 | 96 | this.socket.send(request); 97 | } 98 | 99 | joinLobby(gameToken, lobbyId, parameters = {}) { 100 | return new Promise((resolve, reject) => { 101 | if (!lobbyId) { 102 | lobbyId = 0; 103 | } 104 | 105 | let request = `${JetcodeSocket.JOIN_LOBBY}\n`; 106 | request += `GameToken=${gameToken}\n`; 107 | request += `LobbyId=${lobbyId}\n`; 108 | 109 | for (const [key, value] of Object.entries(parameters)) { 110 | request += `${key}=${value}\n`; 111 | } 112 | 113 | this.socket.send(request); 114 | 115 | this.connect(JetcodeSocket.JOINED, (responseParams) => { 116 | if (responseParams.LobbyId && responseParams.MemberId && responseParams.CurrentTime) { 117 | this.lobbyId = responseParams.LobbyId; 118 | this.memberId = responseParams.MemberId; 119 | 120 | let currentTimeMs = Date.now(); 121 | this.deltaTime = currentTimeMs - Number(responseParams.CurrentTime); 122 | 123 | resolve(this.lobbyId); 124 | 125 | } else { 126 | reject(new Error('Couldn\'t join the lobby')); 127 | } 128 | }); 129 | }); 130 | } 131 | 132 | leaveLobby() { 133 | if (!this.lobbyId) { 134 | return; 135 | } 136 | 137 | let request = `${JetcodeSocket.LEAVE_LOBBY}\nLobbyId=${this.lobbyId}\n`; 138 | this.socket.send(request); 139 | 140 | this.lobbyId = null; 141 | } 142 | 143 | _parse(data) { 144 | let parsable = data.split('\n'); 145 | let action = parsable[0]; 146 | let value = ''; 147 | let parameters = []; 148 | 149 | let nextIs = 'parameters'; 150 | for (let i = 1; i < parsable.length; i++) { 151 | const line = parsable[i]; 152 | 153 | if (line === '' && nextIs === 'parameters') { 154 | nextIs = 'value'; 155 | 156 | } else if (nextIs === 'parameters') { 157 | const splitted = line.split('='); 158 | 159 | const parameter = splitted[0]; 160 | parameters[parameter] = splitted.length > 1 ? splitted[1] : null; 161 | 162 | } else if (nextIs === 'value') { 163 | value = value + line + '\n'; 164 | } 165 | } 166 | 167 | if (value) { 168 | value = value.slice(0, -1); 169 | } 170 | 171 | return [action, parameters, value]; 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/jmp/JetcodeSocketParameters.ts: -------------------------------------------------------------------------------- 1 | export interface JetcodeSocketParameters { 2 | LobbyAutoCreate: boolean; 3 | MinMembers: number; 4 | MaxMembers: number; 5 | StartGameWithMembers: number; 6 | } 7 | -------------------------------------------------------------------------------- /src/jmp/index.ts: -------------------------------------------------------------------------------- 1 | export * from './JetcodeSocket'; 2 | export * from './JetcodeSocketConnect'; 3 | export * from './JetcodeSocketParameters'; 4 | -------------------------------------------------------------------------------- /src/scrub.ts: -------------------------------------------------------------------------------- 1 | export * from './jmp'; 2 | export * from './collisions'; 3 | export * from './utils'; 4 | 5 | export * from './Costume'; 6 | export * from './EventEmitter'; 7 | export * from './Game'; 8 | export * from './ScheduledCallbackExecutor'; 9 | export * from './ScheduledCallbackItem'; 10 | export * from './ScheduledState'; 11 | export * from './Sprite'; 12 | export * from './Stage'; 13 | export * from './Camera'; 14 | export * from './CameraChanges'; 15 | 16 | export * from './MultiplayerControl'; 17 | export * from './MultiplayerGame'; 18 | export * from './MultiplayerSprite'; 19 | export * from './OrphanSharedData'; 20 | export * from './Player'; 21 | export * from './SharedData'; 22 | export * from './SyncObjectInterface'; 23 | -------------------------------------------------------------------------------- /src/utils/ErrorMessages.ts: -------------------------------------------------------------------------------- 1 | export class ErrorMessages { 2 | static readonly SCRIPT_ERROR = 'script_error'; 3 | static readonly MISTAKE_METHOD = 'mistake_method'; 4 | static readonly MISTAKE_METHOD_WITH_CLOSEST = 'mistake_method_with_closest'; 5 | static readonly NEED_STAGE_BEFORE_RUN_GAME = 'need_stage_before_run_game'; 6 | static readonly NEED_CREATE_STAGE_BEFORE_SPRITE = 'need_create_stage_before_sprite'; 7 | static readonly COSTUME_NOT_LOADED = 'costume_not_loaded'; 8 | static readonly BACKGROUND_NOT_LOADED = 'background_not_loaded'; 9 | static readonly CLONED_NOT_READY = 'cloned_not_ready'; 10 | static readonly SOUND_INDEX_NOT_FOUND = 'sound_index_not_found'; 11 | static readonly SOUND_NAME_NOT_FOUND = 'sound_name_not_found'; 12 | static readonly SOUND_NAME_ALREADY_EXISTS = 'sound_name_already_exists'; 13 | static readonly SOUND_NOT_ALLOWED_ERROR = 'sound_not_allowed_error'; 14 | static readonly SOUND_USE_NOT_READY = 'sound_use_not_ready'; 15 | static readonly COSTUME_INDEX_NOT_FOUND = 'costume_index_not_found'; 16 | static readonly COSTUME_NAME_NOT_FOUND = 'costume_name_not_found'; 17 | static readonly COSTUME_SWITCH_NOT_READY = 'costume_switch_not_ready'; 18 | static readonly STAMP_NOT_READY = 'stamp_not_ready'; 19 | static readonly STAMP_COSTUME_NOT_FOUND = 'stamp_costume_not_found'; 20 | static readonly COLLIDER_NAME_NOT_FOUND = 'collider_name_not_found'; 21 | static readonly STAGE_SET_BEFORE_GAME_READY = 'stage_set_before_game_ready'; 22 | 23 | static readonly messages = { 24 | script_error: { 25 | 'ru': 'Произошла ошибка, ознакомьтесь с подробной информацией в консоли.', 26 | 'en': 'An error has occurred, take a look at the details in the console.' 27 | }, 28 | mistake_method: { 29 | 'ru': '${className}: Метод или свойство "${prop}" не найдено', 30 | 'en': '${className}: Method "${prop}" not found' 31 | }, 32 | mistake_method_with_closest: { 33 | 'ru': '${className}: Метод или свойство "${prop}" не найдено. Возможно вы имели ввиду: ${closestString}?', 34 | 'en': '${className}: Method "${prop}" not found. Did you mean: ${closestString}?' 35 | }, 36 | need_stage_before_run_game: { 37 | 'ru': 'Вам нужно создать экземпляр Stage перед запуском игры.', 38 | 'en': 'You need create Stage instance before run game.' 39 | }, 40 | need_create_stage_before_sprite: { 41 | 'ru': 'Вам нужно создать экземпляр класса Stage перед экземпляром класса Sprite.', 42 | 'en': 'You need create Stage instance before Sprite instance.' 43 | }, 44 | costume_not_loaded: { 45 | 'ru': 'Изображение для костюма "${costumePath}" не было загружено. Проверьте правильность пути.', 46 | 'en': 'Costume image "${costumePath}" was not loaded. Check that the path is correct.' 47 | }, 48 | background_not_loaded: { 49 | 'ru': 'Изображение для фона "${backgroundPath}" не было загружено. Проверьте правильность пути.', 50 | 'en': 'Background image "${backgroundPath}" was not loaded. Check that the path is correct.' 51 | }, 52 | cloned_not_ready: { 53 | 'ru': 'Спрайт не может быть клонирован, потому что он еще не готов. Попробуйте использовать метод sprite.onReady()', 54 | 'en': 'Sprite cannot be cloned because one is not ready. Try using the sprite.onReady() method.' 55 | }, 56 | sound_index_not_found: { 57 | 'ru': 'Звук с индексом "${soundIndex}" не найден.', 58 | 'en': 'Sound with index "${soundIndex}" not found.' 59 | }, 60 | sound_name_not_found: { 61 | 'ru': 'Звук с именем "${soundName}" не найден.', 62 | 'en': 'Sound with name "${soundName}" not found.' 63 | }, 64 | sound_name_already_exists: { 65 | 'ru': 'Звук с именем "${soundName}" уже добавлен.', 66 | 'en': 'Sound with name "${soundName}" already exists.' 67 | }, 68 | sound_use_not_ready: { 69 | 'ru': 'Спрайт не может использовать звуки, потому что спрайт еще не готов. Попробуйте использовать метод sprite.onReady().', 70 | 'en': 'Sprite cannot use sounds because sprite is not ready. Try using the sprite.onReady() method.' 71 | }, 72 | sound_not_allowed_error: { 73 | 'ru': 'Воспроизведение звука заблокировано. Пользователь должен сначала взаимодействовать с игрой. Воспользуйтесь методом Game.onUserInteracted()', 74 | 'en': 'Audio playback is blocked. The user must first interact with the game. Use the Game.onUserInteracted() method.' 75 | }, 76 | costume_index_not_found: { 77 | 'ru': 'Костюм с индексом "${costumeIndex}" не найден.', 78 | 'en': 'Costume with index "${costumeIndex}" not found.' 79 | }, 80 | costume_name_not_found: { 81 | 'ru': 'Костюм с именем "${costumeName}" не найден.', 82 | 'en': 'Costume with name "${costumeName}" not found.' 83 | }, 84 | costume_switch_not_ready: { 85 | 'ru': 'Спрайт не может изменить костюм, потому что спрайт еще не готов. Попробуйте использовать метод sprite.onReady().', 86 | 'en': 'Sprite cannot change a costume because sprite is not ready. Try using the sprite.onReady() method.' 87 | }, 88 | stamp_not_ready: { 89 | 'ru': 'Спрайт не может создать штамп, потому что он еще не готов. Попробуйте использовать метод sprite.onReady()', 90 | 'en': 'Sprite cannot create a stamp because sprite is not ready. Try using the sprite.onReady() method.' 91 | }, 92 | stamp_costume_not_found: { 93 | 'ru': 'Штам не может быть создан, так как костюм с индексом "${costumeIndex}" не найден.', 94 | 'en': 'The stamp cannot be created because the costume with the index "${costumeIndex}" has not been found.' 95 | }, 96 | collider_name_not_found: { 97 | 'ru': 'Коллайдер с именем "${colliderName}" не найден.', 98 | 'en': 'Collider with name "${colliderName}" not found.' 99 | }, 100 | stage_set_before_game_ready: { 101 | 'ru': 'Спрайт меняет сцену до готовности игры.', 102 | 'en': 'Sprite changed stage before game is ready.' 103 | }, 104 | } 105 | 106 | static getMessage(messageId: string, locale: string, variables: {} | null = null): string { 107 | if (!ErrorMessages.messages[messageId]) { 108 | throw new Error('Message is not defined.'); 109 | } 110 | 111 | if (!ErrorMessages.messages[messageId][locale]) { 112 | throw new Error('Message for this locale is not defined.'); 113 | } 114 | 115 | let message = ErrorMessages.messages[messageId][locale]; 116 | 117 | if (variables) { 118 | message = ErrorMessages.replaceVariables(message, variables); 119 | } 120 | 121 | return message; 122 | } 123 | 124 | private static replaceVariables(message: string, variables: {}): string { 125 | return message.replace(/\${([^}]+)}/g, (match, key) => { 126 | return variables[key] !== undefined ? variables[key] : ''; 127 | }); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/utils/Keyboard.ts: -------------------------------------------------------------------------------- 1 | import { KeyboardMap } from './KeyboardMap'; 2 | 3 | export class Keyboard { 4 | keys = {}; 5 | 6 | constructor() { 7 | document.addEventListener('keydown', (event) => { 8 | const char = KeyboardMap.getChar(event.keyCode); 9 | 10 | this.keys[char] = true; 11 | }); 12 | 13 | document.addEventListener('keyup', (event) => { 14 | const char = KeyboardMap.getChar(event.keyCode); 15 | 16 | delete this.keys[char] 17 | }); 18 | } 19 | 20 | keyPressed(char) { 21 | return this.keys[char.toUpperCase()] !== undefined; 22 | } 23 | 24 | keyDown(char: string, callback) { 25 | document.addEventListener('keydown', (event) => { 26 | const pressedChar = KeyboardMap.getChar(event.keyCode); 27 | 28 | if (char.toUpperCase() == pressedChar) { 29 | callback(event); 30 | } 31 | }); 32 | } 33 | 34 | keyUp(char: string, callback) { 35 | document.addEventListener('keyup', (event) => { 36 | const pressedChar = KeyboardMap.getChar(event.keyCode); 37 | 38 | if (char.toUpperCase() == pressedChar) { 39 | callback(event); 40 | } 41 | }); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/utils/KeyboardMap.ts: -------------------------------------------------------------------------------- 1 | export class KeyboardMap { 2 | private static map = [ 3 | '', // [0] 4 | '', // [1] 5 | '', // [2] 6 | 'CANCEL', // [3] 7 | '', // [4] 8 | '', // [5] 9 | 'HELP', // [6] 10 | '', // [7] 11 | 'BACK_SPACE', // [8] 12 | 'TAB', // [9] 13 | '', // [10] 14 | '', // [11] 15 | 'CLEAR', // [12] 16 | 'ENTER', // [13] 17 | 'ENTER_SPECIAL', // [14] 18 | '', // [15] 19 | 'SHIFT', // [16] 20 | 'CONTROL', // [17] 21 | 'ALT', // [18] 22 | 'PAUSE', // [19] 23 | 'CAPS_LOCK', // [20] 24 | 'KANA', // [21] 25 | 'EISU', // [22] 26 | 'JUNJA', // [23] 27 | 'FINAL', // [24] 28 | 'HANJA', // [25] 29 | '', // [26] 30 | 'ESCAPE', // [27] 31 | 'CONVERT', // [28] 32 | 'NONCONVERT', // [29] 33 | 'ACCEPT', // [30] 34 | 'MODECHANGE', // [31] 35 | 'SPACE', // [32] 36 | 'PAGE_UP', // [33] 37 | 'PAGE_DOWN', // [34] 38 | 'END', // [35] 39 | 'HOME', // [36] 40 | 'LEFT', // [37] 41 | 'UP', // [38] 42 | 'RIGHT', // [39] 43 | 'DOWN', // [40] 44 | 'SELECT', // [41] 45 | 'PRINT', // [42] 46 | 'EXECUTE', // [43] 47 | 'PRINTSCREEN', // [44] 48 | 'INSERT', // [45] 49 | 'DELETE', // [46] 50 | '', // [47] 51 | '0', // [48] 52 | '1', // [49] 53 | '2', // [50] 54 | '3', // [51] 55 | '4', // [52] 56 | '5', // [53] 57 | '6', // [54] 58 | '7', // [55] 59 | '8', // [56] 60 | '9', // [57] 61 | 'COLON', // [58] 62 | 'SEMICOLON', // [59] 63 | 'LESS_THAN', // [60] 64 | 'EQUALS', // [61] 65 | 'GREATER_THAN', // [62] 66 | 'QUESTION_MARK', // [63] 67 | 'AT', // [64] 68 | 'A', // [65] 69 | 'B', // [66] 70 | 'C', // [67] 71 | 'D', // [68] 72 | 'E', // [69] 73 | 'F', // [70] 74 | 'G', // [71] 75 | 'H', // [72] 76 | 'I', // [73] 77 | 'J', // [74] 78 | 'K', // [75] 79 | 'L', // [76] 80 | 'M', // [77] 81 | 'N', // [78] 82 | 'O', // [79] 83 | 'P', // [80] 84 | 'Q', // [81] 85 | 'R', // [82] 86 | 'S', // [83] 87 | 'T', // [84] 88 | 'U', // [85] 89 | 'V', // [86] 90 | 'W', // [87] 91 | 'X', // [88] 92 | 'Y', // [89] 93 | 'Z', // [90] 94 | 'OS_KEY', // [91] Windows Key (Windows) or Command Key (Mac) 95 | '', // [92] 96 | 'CONTEXT_MENU', // [93] 97 | '', // [94] 98 | 'SLEEP', // [95] 99 | 'NUMPAD0', // [96] 100 | 'NUMPAD1', // [97] 101 | 'NUMPAD2', // [98] 102 | 'NUMPAD3', // [99] 103 | 'NUMPAD4', // [100] 104 | 'NUMPAD5', // [101] 105 | 'NUMPAD6', // [102] 106 | 'NUMPAD7', // [103] 107 | 'NUMPAD8', // [104] 108 | 'NUMPAD9', // [105] 109 | 'MULTIPLY', // [106] 110 | 'ADD', // [107] 111 | 'SEPARATOR', // [108] 112 | 'SUBTRACT', // [109] 113 | 'DECIMAL', // [110] 114 | 'DIVIDE', // [111] 115 | 'F1', // [112] 116 | 'F2', // [113] 117 | 'F3', // [114] 118 | 'F4', // [115] 119 | 'F5', // [116] 120 | 'F6', // [117] 121 | 'F7', // [118] 122 | 'F8', // [119] 123 | 'F9', // [120] 124 | 'F10', // [121] 125 | 'F11', // [122] 126 | 'F12', // [123] 127 | 'F13', // [124] 128 | 'F14', // [125] 129 | 'F15', // [126] 130 | 'F16', // [127] 131 | 'F17', // [128] 132 | 'F18', // [129] 133 | 'F19', // [130] 134 | 'F20', // [131] 135 | 'F21', // [132] 136 | 'F22', // [133] 137 | 'F23', // [134] 138 | 'F24', // [135] 139 | '', // [136] 140 | '', // [137] 141 | '', // [138] 142 | '', // [139] 143 | '', // [140] 144 | '', // [141] 145 | '', // [142] 146 | '', // [143] 147 | 'NUM_LOCK', // [144] 148 | 'SCROLL_LOCK', // [145] 149 | 'WIN_OEM_FJ_JISHO', // [146] 150 | 'WIN_OEM_FJ_MASSHOU', // [147] 151 | 'WIN_OEM_FJ_TOUROKU', // [148] 152 | 'WIN_OEM_FJ_LOYA', // [149] 153 | 'WIN_OEM_FJ_ROYA', // [150] 154 | '', // [151] 155 | '', // [152] 156 | '', // [153] 157 | '', // [154] 158 | '', // [155] 159 | '', // [156] 160 | '', // [157] 161 | '', // [158] 162 | '', // [159] 163 | 'CIRCUMFLEX', // [160] 164 | 'EXCLAMATION', // [161] 165 | 'DOUBLE_QUOTE', // [162] 166 | 'HASH', // [163] 167 | 'DOLLAR', // [164] 168 | 'PERCENT', // [165] 169 | 'AMPERSAND', // [166] 170 | 'UNDERSCORE', // [167] 171 | 'OPEN_PAREN', // [168] 172 | 'CLOSE_PAREN', // [169] 173 | 'ASTERISK', // [170] 174 | 'PLUS', // [171] 175 | 'PIPE', // [172] 176 | 'HYPHEN_MINUS', // [173] 177 | 'OPEN_CURLY_BRACKET', // [174] 178 | 'CLOSE_CURLY_BRACKET', // [175] 179 | 'TILDE', // [176] 180 | '', // [177] 181 | '', // [178] 182 | '', // [179] 183 | '', // [180] 184 | 'VOLUME_MUTE', // [181] 185 | 'VOLUME_DOWN', // [182] 186 | 'VOLUME_UP', // [183] 187 | '', // [184] 188 | '', // [185] 189 | 'SEMICOLON', // [186] 190 | 'EQUALS', // [187] 191 | 'COMMA', // [188] 192 | 'MINUS', // [189] 193 | 'PERIOD', // [190] 194 | 'SLASH', // [191] 195 | 'BACK_QUOTE', // [192] 196 | '', // [193] 197 | '', // [194] 198 | '', // [195] 199 | '', // [196] 200 | '', // [197] 201 | '', // [198] 202 | '', // [199] 203 | '', // [200] 204 | '', // [201] 205 | '', // [202] 206 | '', // [203] 207 | '', // [204] 208 | '', // [205] 209 | '', // [206] 210 | '', // [207] 211 | '', // [208] 212 | '', // [209] 213 | '', // [210] 214 | '', // [211] 215 | '', // [212] 216 | '', // [213] 217 | '', // [214] 218 | '', // [215] 219 | '', // [216] 220 | '', // [217] 221 | '', // [218] 222 | 'OPEN_BRACKET', // [219] 223 | 'BACK_SLASH', // [220] 224 | 'CLOSE_BRACKET', // [221] 225 | 'QUOTE', // [222] 226 | '', // [223] 227 | 'META', // [224] 228 | 'ALTGR', // [225] 229 | '', // [226] 230 | 'WIN_ICO_HELP', // [227] 231 | 'WIN_ICO_00', // [228] 232 | '', // [229] 233 | 'WIN_ICO_CLEAR', // [230] 234 | '', // [231] 235 | '', // [232] 236 | 'WIN_OEM_RESET', // [233] 237 | 'WIN_OEM_JUMP', // [234] 238 | 'WIN_OEM_PA1', // [235] 239 | 'WIN_OEM_PA2', // [236] 240 | 'WIN_OEM_PA3', // [237] 241 | 'WIN_OEM_WSCTRL', // [238] 242 | 'WIN_OEM_CUSEL', // [239] 243 | 'WIN_OEM_ATTN', // [240] 244 | 'WIN_OEM_FINISH', // [241] 245 | 'WIN_OEM_COPY', // [242] 246 | 'WIN_OEM_AUTO', // [243] 247 | 'WIN_OEM_ENLW', // [244] 248 | 'WIN_OEM_BACKTAB', // [245] 249 | 'ATTN', // [246] 250 | 'CRSEL', // [247] 251 | 'EXSEL', // [248] 252 | 'EREOF', // [249] 253 | 'PLAY', // [250] 254 | 'ZOOM', // [251] 255 | '', // [252] 256 | 'PA1', // [253] 257 | 'WIN_OEM_CLEAR', // [254] 258 | '' // [255] 259 | ]; 260 | 261 | static getChar(keyCode: number) { 262 | return KeyboardMap.map[keyCode]; 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /src/utils/Mouse.ts: -------------------------------------------------------------------------------- 1 | import { PointCollider } from '../collisions'; 2 | import { Stage } from '../Stage'; 3 | import { Game } from '../Game'; 4 | 5 | export class Mouse { 6 | x = 0; 7 | y = 0; 8 | private isDown = false; 9 | private point: PointCollider; 10 | private lastStage: Stage; 11 | 12 | constructor(game: Game) { 13 | document.addEventListener('mousedown', () => { 14 | this.isDown = true; 15 | this.lastStage = game.getActiveStage(); 16 | }); 17 | 18 | document.addEventListener('mouseup', () => { 19 | this.isDown = false; 20 | }); 21 | 22 | document.addEventListener('mousemove', (e) => { 23 | this.x = game.correctMouseX(e.clientX); 24 | this.y = game.correctMouseY(e.clientY); 25 | }); 26 | 27 | this.point = new PointCollider(this.x, this.y); 28 | } 29 | 30 | getPoint() { 31 | this.point.x = this.x; 32 | this.point.y = this.y; 33 | 34 | return this.point; 35 | } 36 | 37 | isMouseDown(stage: Stage) { 38 | return this.isDown && stage === this.lastStage; 39 | } 40 | 41 | clearMouseDown(): void { 42 | this.isDown = false; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/utils/Registry.ts: -------------------------------------------------------------------------------- 1 | export class Registry { 2 | private static instance: Registry; 3 | private data = {}; 4 | 5 | private constructor() { 6 | } 7 | 8 | public static getInstance(): Registry { 9 | if (!this.instance) { 10 | this.instance = new Registry(); 11 | } 12 | 13 | return this.instance; 14 | } 15 | 16 | public set(name: string, value: any) { 17 | this.data[name] = value; 18 | } 19 | 20 | public has(name: string): boolean { 21 | return this.data[name] !== undefined; 22 | } 23 | 24 | public get(name: string): any { 25 | return this.data[name]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/utils/Styles.ts: -------------------------------------------------------------------------------- 1 | export class Styles { 2 | canvas; 3 | canvasRect; 4 | 5 | constructor(canvas, width, height) { 6 | this.canvas = canvas; 7 | this.setEnvironmentStyles(); 8 | 9 | this.setCanvasSize(width, height); 10 | this.canvasRect = canvas.getBoundingClientRect(); 11 | 12 | window.addEventListener('resize', () => { 13 | this.setCanvasSize(width, height); 14 | this.canvasRect = canvas.getBoundingClientRect(); 15 | }); 16 | } 17 | 18 | setEnvironmentStyles() { 19 | document.body.style.margin = '0'; 20 | document.body.style.height = '100' + 'vh'; 21 | document.body.style.padding = '0'; 22 | document.body.style.overflow = 'hidden'; 23 | document.body.style.display = 'flex'; 24 | document.body.style.alignItems = 'center'; 25 | document.body.style.justifyContent = 'center'; 26 | } 27 | 28 | setCanvasSize(width, height) { 29 | this.canvas.width = width ? width : document.body.clientWidth; 30 | this.canvas.height = height ? height : document.body.clientHeight; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/utils/ValidatorFactory.ts: -------------------------------------------------------------------------------- 1 | import { Game } from '../Game'; 2 | import { ErrorMessages } from './ErrorMessages'; 3 | 4 | export class ValidatorFactory { 5 | constructor(private game: Game) { 6 | } 7 | 8 | createValidator(target: T, className: string): T { 9 | const game = this.game; 10 | 11 | return new Proxy(target, { 12 | get(obj, prop) { 13 | if (prop in obj) { 14 | return obj[prop]; 15 | } 16 | 17 | if (typeof prop === 'symbol' || prop.startsWith('_')) { // Исключаем служебные свойства 18 | return undefined; 19 | } 20 | 21 | const methods = Object.getOwnPropertyNames(Object.getPrototypeOf(obj)) 22 | .filter(m => m !== 'constructor'); 23 | 24 | const closest = ValidatorFactory.findClosestMethods(prop.toString(), methods); 25 | 26 | if (closest.length) { 27 | const closestString = closest.join(', '); 28 | game.throwError(ErrorMessages.MISTAKE_METHOD_WITH_CLOSEST, {className, prop, closestString}); 29 | 30 | } else { 31 | game.throwError(ErrorMessages.MISTAKE_METHOD, {className, prop}); 32 | } 33 | } 34 | }); 35 | } 36 | 37 | static findClosestMethods(input: string, methods: string[], maxDistance = 2): string[] { 38 | return methods 39 | .map(method => ({ 40 | name: method, 41 | distance: ValidatorFactory.levenshteinDistance(input.toLowerCase(), method.toLowerCase()) 42 | })) 43 | .filter(({distance}) => distance <= maxDistance) 44 | .sort((a, b) => a.distance - b.distance) 45 | .map(({name}) => name) 46 | .slice(0, 3); 47 | } 48 | 49 | static levenshteinDistance(a: string, b: string): number { 50 | const matrix = Array(a.length + 1) 51 | .fill(null) 52 | .map(() => Array(b.length + 1).fill(0)); 53 | 54 | for (let i = 0; i <= a.length; i++) matrix[i][0] = i; 55 | for (let j = 0; j <= b.length; j++) matrix[0][j] = j; 56 | 57 | for (let i = 1; i <= a.length; i++) { 58 | for (let j = 1; j <= b.length; j++) { 59 | const cost = a[i - 1] === b[j - 1] ? 0 : 1; 60 | matrix[i][j] = Math.min( 61 | matrix[i - 1][j] + 1, 62 | matrix[i][j - 1] + 1, 63 | matrix[i - 1][j - 1] + cost 64 | ); 65 | } 66 | } 67 | 68 | return matrix[a.length][b.length]; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ErrorMessages'; 2 | export * from './Keyboard'; 3 | export * from './KeyboardMap'; 4 | export * from './Mouse'; 5 | export * from './Registry'; 6 | export * from './Styles'; 7 | export * from './ValidatorFactory'; 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "es2020", 5 | "moduleResolution": "node", 6 | "lib": ["es2020","dom"], 7 | "strict": false, 8 | "esModuleInterop": true, 9 | "skipLibCheck": true, 10 | "declaration": true, 11 | "sourceMap": true, 12 | "rootDir": "src", 13 | "outDir": "dist", 14 | "baseUrl": "." 15 | }, 16 | "files": ["src/scrub.ts", "src/browser/scrub.ts"], 17 | "exclude": ["node_modules"] 18 | } 19 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | const extensionMap = { 4 | esm: 'mjs', 5 | cjs: 'cjs', 6 | iife: 'js' 7 | }; 8 | 9 | const outExtension = ({format}) => ({ 10 | js: `.${extensionMap[format]}` 11 | }); 12 | 13 | const commonConfig = { 14 | outExtension, 15 | publicDir: 'public', 16 | clean: true, 17 | minify: true, 18 | sourcemap: true 19 | }; 20 | 21 | export default defineConfig([ 22 | { 23 | ...commonConfig, 24 | entry: ['src/scrub.ts'], 25 | format: ['esm'] 26 | }, 27 | { 28 | ...commonConfig, 29 | entry: ['src/scrub.ts'], 30 | format: ['cjs'], 31 | dts: true 32 | }, 33 | { 34 | ...commonConfig, 35 | entry: ['src/browser/scrub.ts'], 36 | format: ['iife'] 37 | } 38 | ]); 39 | --------------------------------------------------------------------------------