├── .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 |
--------------------------------------------------------------------------------