reqct-gamehub-module 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,2843 @@
1
+ // src/components/GameHubNode/GameHubNode.js
2
+ import React7, { memo, useMemo, useState as useState7, useEffect as useEffect7 } from "react";
3
+ import { Handle, Position } from "reactflow";
4
+
5
+ // src/games/Snake/SnakeGame.js
6
+ import React, { useState, useEffect, useCallback } from "react";
7
+ import { jsx, jsxs } from "react/jsx-runtime";
8
+ var SnakeGame = () => {
9
+ const GRID_SIZES = {
10
+ easy: 15,
11
+ medium: 20,
12
+ hard: 25
13
+ };
14
+ const SPEEDS = {
15
+ easy: 200,
16
+ medium: 150,
17
+ hard: 100
18
+ };
19
+ const [difficulty, setDifficulty] = useState("medium");
20
+ const [gridSize, setGridSize] = useState(GRID_SIZES.medium);
21
+ const [snake, setSnake] = useState([]);
22
+ const [food, setFood] = useState({ x: 5, y: 5 });
23
+ const [direction, setDirection] = useState("RIGHT");
24
+ const [nextDirection, setNextDirection] = useState("RIGHT");
25
+ const [score, setScore] = useState(0);
26
+ const [gameOver, setGameOver] = useState(false);
27
+ const [gameStarted, setGameStarted] = useState(false);
28
+ const [speed, setSpeed] = useState(SPEEDS.medium);
29
+ const [highScore, setHighScore] = useState(0);
30
+ const [gamePaused, setGamePaused] = useState(false);
31
+ const [gameSpeed, setGameSpeed] = useState(1);
32
+ useEffect(() => {
33
+ const savedHighScore = localStorage.getItem("snakeHighScore");
34
+ if (savedHighScore) {
35
+ setHighScore(parseInt(savedHighScore));
36
+ }
37
+ initializeGame();
38
+ }, []);
39
+ const generateFood = useCallback(() => {
40
+ const emptyCells = [];
41
+ for (let y = 0; y < gridSize; y++) {
42
+ for (let x = 0; x < gridSize; x++) {
43
+ if (!snake.some((segment) => segment.x === x && segment.y === y)) {
44
+ emptyCells.push({ x, y });
45
+ }
46
+ }
47
+ }
48
+ if (emptyCells.length > 0) {
49
+ const randomCell = emptyCells[Math.floor(Math.random() * emptyCells.length)];
50
+ return randomCell;
51
+ }
52
+ return { x: -1, y: -1 };
53
+ }, [snake, gridSize]);
54
+ const initializeGame = () => {
55
+ const size = GRID_SIZES[difficulty];
56
+ const initialSpeed = SPEEDS[difficulty];
57
+ const midPoint = Math.floor(size / 2);
58
+ const initialSnake = [{ x: midPoint, y: midPoint }];
59
+ const initialFood = {
60
+ x: Math.floor(Math.random() * size),
61
+ y: Math.floor(Math.random() * size)
62
+ };
63
+ if (initialSnake.some((segment) => segment.x === initialFood.x && segment.y === initialFood.y)) {
64
+ initialFood.x = (initialFood.x + 5) % size;
65
+ initialFood.y = (initialFood.y + 5) % size;
66
+ }
67
+ setGridSize(size);
68
+ setSnake(initialSnake);
69
+ setFood(initialFood);
70
+ setDirection("RIGHT");
71
+ setNextDirection("RIGHT");
72
+ setScore(0);
73
+ setGameOver(false);
74
+ setGameStarted(true);
75
+ setGamePaused(false);
76
+ setSpeed(initialSpeed);
77
+ setGameSpeed(1);
78
+ };
79
+ const changeDifficulty = (newDifficulty) => {
80
+ if (gameStarted && !gameOver) {
81
+ if (window.confirm("\u0421\u043C\u0435\u043D\u0438\u0442\u044C \u0441\u043B\u043E\u0436\u043D\u043E\u0441\u0442\u044C? \u0422\u0435\u043A\u0443\u0449\u0430\u044F \u0438\u0433\u0440\u0430 \u0431\u0443\u0434\u0435\u0442 \u0441\u0431\u0440\u043E\u0448\u0435\u043D\u0430.")) {
82
+ setDifficulty(newDifficulty);
83
+ setTimeout(() => initializeGame(), 100);
84
+ }
85
+ } else {
86
+ setDifficulty(newDifficulty);
87
+ }
88
+ };
89
+ const checkCollision = useCallback((head) => {
90
+ if (head.x < 0 || head.x >= gridSize || head.y < 0 || head.y >= gridSize) {
91
+ return true;
92
+ }
93
+ for (let i = 1; i < snake.length; i++) {
94
+ if (head.x === snake[i].x && head.y === snake[i].y) {
95
+ return true;
96
+ }
97
+ }
98
+ return false;
99
+ }, [snake, gridSize]);
100
+ const moveSnake = useCallback(() => {
101
+ if (!gameStarted || gameOver || gamePaused) return;
102
+ setSnake((prevSnake) => {
103
+ const head = { ...prevSnake[0] };
104
+ const currentDirection = nextDirection;
105
+ setDirection(currentDirection);
106
+ switch (currentDirection) {
107
+ case "RIGHT":
108
+ head.x += 1;
109
+ break;
110
+ case "LEFT":
111
+ head.x -= 1;
112
+ break;
113
+ case "UP":
114
+ head.y -= 1;
115
+ break;
116
+ case "DOWN":
117
+ head.y += 1;
118
+ break;
119
+ default:
120
+ break;
121
+ }
122
+ if (checkCollision(head)) {
123
+ setGameOver(true);
124
+ if (score > highScore) {
125
+ const newHighScore = score;
126
+ setHighScore(newHighScore);
127
+ localStorage.setItem("snakeHighScore", newHighScore.toString());
128
+ }
129
+ return prevSnake;
130
+ }
131
+ const newSnake = [head, ...prevSnake];
132
+ if (head.x === food.x && head.y === food.y) {
133
+ const newScore = score + 10;
134
+ setScore(newScore);
135
+ if (newScore > 0 && newScore % 50 === 0) {
136
+ const newSpeed = Math.max(50, speed - 20);
137
+ setSpeed(newSpeed);
138
+ setGameSpeed(Math.round(SPEEDS[difficulty] / newSpeed));
139
+ }
140
+ const newFood = generateFood();
141
+ if (newFood.x !== -1 && newFood.y !== -1) {
142
+ setFood(newFood);
143
+ } else {
144
+ setGameOver(true);
145
+ }
146
+ } else {
147
+ newSnake.pop();
148
+ }
149
+ return newSnake;
150
+ });
151
+ }, [gameStarted, gameOver, gamePaused, nextDirection, checkCollision, food, score, highScore, speed, difficulty, generateFood]);
152
+ useEffect(() => {
153
+ const handleKeyDown = (e) => {
154
+ if (gameOver) return;
155
+ switch (e.key) {
156
+ case "ArrowUp":
157
+ if (direction !== "DOWN") {
158
+ e.preventDefault();
159
+ setNextDirection("UP");
160
+ }
161
+ break;
162
+ case "ArrowDown":
163
+ if (direction !== "UP") {
164
+ e.preventDefault();
165
+ setNextDirection("DOWN");
166
+ }
167
+ break;
168
+ case "ArrowLeft":
169
+ if (direction !== "RIGHT") {
170
+ e.preventDefault();
171
+ setNextDirection("LEFT");
172
+ }
173
+ break;
174
+ case "ArrowRight":
175
+ if (direction !== "LEFT") {
176
+ e.preventDefault();
177
+ setNextDirection("RIGHT");
178
+ }
179
+ break;
180
+ case " ":
181
+ e.preventDefault();
182
+ if (gameStarted && !gameOver) {
183
+ setGamePaused((prev) => !prev);
184
+ }
185
+ break;
186
+ case "Enter":
187
+ if (!gameStarted || gameOver) {
188
+ e.preventDefault();
189
+ initializeGame();
190
+ }
191
+ break;
192
+ default:
193
+ break;
194
+ }
195
+ };
196
+ window.addEventListener("keydown", handleKeyDown);
197
+ return () => window.removeEventListener("keydown", handleKeyDown);
198
+ }, [direction, gameStarted, gameOver, gamePaused]);
199
+ useEffect(() => {
200
+ if (!gameStarted || gameOver || gamePaused) return;
201
+ const gameInterval = setInterval(moveSnake, speed);
202
+ return () => clearInterval(gameInterval);
203
+ }, [gameStarted, gameOver, gamePaused, moveSnake, speed]);
204
+ useEffect(() => {
205
+ if (gameStarted) {
206
+ const event = new CustomEvent("gameScoreUpdate", {
207
+ detail: { game: "snake", score }
208
+ });
209
+ window.dispatchEvent(event);
210
+ }
211
+ }, [score, gameStarted]);
212
+ const handleButtonControl = (newDirection) => {
213
+ if (gamePaused || gameOver || !gameStarted) return;
214
+ if (newDirection === "UP" && direction !== "DOWN" || newDirection === "DOWN" && direction !== "UP" || newDirection === "LEFT" && direction !== "RIGHT" || newDirection === "RIGHT" && direction !== "LEFT") {
215
+ setNextDirection(newDirection);
216
+ }
217
+ };
218
+ const getDifficultyColor = (diff) => {
219
+ return diff === difficulty ? diff === "easy" ? "#00b09b" : diff === "medium" ? "#667eea" : "#ff5e62" : "#666";
220
+ };
221
+ return /* @__PURE__ */ jsxs("div", { className: "snake-game", children: [
222
+ /* @__PURE__ */ jsx("div", { className: "game-header", children: /* @__PURE__ */ jsx("div", { className: "game-controls", children: !gameStarted || gameOver ? /* @__PURE__ */ jsx("button", { className: "control-btn start-btn", onClick: initializeGame, children: gameOver ? "\u{1F504} \u0418\u0433\u0440\u0430\u0442\u044C \u0441\u043D\u043E\u0432\u0430" : "\u25B6\uFE0F \u041D\u0430\u0447\u0430\u0442\u044C \u0438\u0433\u0440\u0443" }) : /* @__PURE__ */ jsx(
223
+ "button",
224
+ {
225
+ className: "control-btn pause-btn",
226
+ onClick: () => setGamePaused(!gamePaused),
227
+ children: gamePaused ? "\u25B6\uFE0F \u041F\u0440\u043E\u0434\u043E\u043B\u0436\u0438\u0442\u044C" : "\u23F8\uFE0F \u041F\u0430\u0443\u0437\u0430"
228
+ }
229
+ ) }) }),
230
+ /* @__PURE__ */ jsxs("div", { className: "difficulty-selector", children: [
231
+ /* @__PURE__ */ jsx("div", { className: "difficulty-label", children: "\u0421\u043B\u043E\u0436\u043D\u043E\u0441\u0442\u044C:" }),
232
+ ["easy", "medium", "hard"].map((diff) => /* @__PURE__ */ jsxs(
233
+ "button",
234
+ {
235
+ className: `difficulty-btn ${difficulty === diff ? "active" : ""}`,
236
+ onClick: () => changeDifficulty(diff),
237
+ style: {
238
+ background: difficulty === diff ? getDifficultyColor(diff) : void 0,
239
+ color: difficulty === diff ? "white" : void 0
240
+ },
241
+ children: [
242
+ diff === "easy" && "\u{1F422} \u041B\u0435\u0433\u043A\u0430\u044F",
243
+ diff === "medium" && "\u26A1 \u0421\u0440\u0435\u0434\u043D\u044F\u044F",
244
+ diff === "hard" && "\u{1F525} \u0421\u043B\u043E\u0436\u043D\u0430\u044F"
245
+ ]
246
+ },
247
+ diff
248
+ ))
249
+ ] }),
250
+ /* @__PURE__ */ jsxs("div", { className: "difficulty-info", children: [
251
+ /* @__PURE__ */ jsxs("div", { className: "info-item", children: [
252
+ /* @__PURE__ */ jsx("span", { children: "\u0420\u0430\u0437\u043C\u0435\u0440 \u043F\u043E\u043B\u044F:" }),
253
+ /* @__PURE__ */ jsxs("strong", { children: [
254
+ gridSize,
255
+ "x",
256
+ gridSize
257
+ ] })
258
+ ] }),
259
+ /* @__PURE__ */ jsxs("div", { className: "info-item", children: [
260
+ /* @__PURE__ */ jsx("span", { children: "\u0421\u043A\u043E\u0440\u043E\u0441\u0442\u044C:" }),
261
+ /* @__PURE__ */ jsxs("strong", { children: [
262
+ gameSpeed,
263
+ "x"
264
+ ] })
265
+ ] }),
266
+ /* @__PURE__ */ jsxs("div", { className: "info-item", children: [
267
+ /* @__PURE__ */ jsx("span", { children: "\u0411\u043E\u043D\u0443\u0441:" }),
268
+ /* @__PURE__ */ jsxs("strong", { children: [
269
+ "+",
270
+ difficulty === "hard" ? "20" : difficulty === "medium" ? "15" : "10",
271
+ "/\u0435\u0434\u0430"
272
+ ] })
273
+ ] })
274
+ ] }),
275
+ /* @__PURE__ */ jsxs("div", { className: "game-stats", children: [
276
+ /* @__PURE__ */ jsxs("div", { className: "stat-box", children: [
277
+ /* @__PURE__ */ jsx("div", { className: "stat-label", children: "\u0421\u0447\u0435\u0442" }),
278
+ /* @__PURE__ */ jsx("div", { className: "stat-value", children: score })
279
+ ] }),
280
+ /* @__PURE__ */ jsxs("div", { className: "stat-box", children: [
281
+ /* @__PURE__ */ jsx("div", { className: "stat-label", children: "\u0420\u0435\u043A\u043E\u0440\u0434" }),
282
+ /* @__PURE__ */ jsx("div", { className: "stat-value", children: highScore })
283
+ ] }),
284
+ /* @__PURE__ */ jsxs("div", { className: "stat-box", children: [
285
+ /* @__PURE__ */ jsx("div", { className: "stat-label", children: "\u0414\u043B\u0438\u043D\u0430" }),
286
+ /* @__PURE__ */ jsx("div", { className: "stat-value", children: snake.length })
287
+ ] }),
288
+ /* @__PURE__ */ jsxs("div", { className: "stat-box", children: [
289
+ /* @__PURE__ */ jsx("div", { className: "stat-label", children: "\u0421\u043B\u043E\u0436\u043D\u043E\u0441\u0442\u044C" }),
290
+ /* @__PURE__ */ jsx("div", { className: "stat-value", children: difficulty === "easy" ? "\u{1F422}" : difficulty === "medium" ? "\u26A1" : "\u{1F525}" })
291
+ ] })
292
+ ] }),
293
+ /* @__PURE__ */ jsx("div", { className: "game-board-container", children: !gameStarted ? /* @__PURE__ */ jsx("div", { className: "start-screen", children: /* @__PURE__ */ jsxs("div", { className: "instructions", children: [
294
+ /* @__PURE__ */ jsx("h3", { children: "\u041A\u0430\u043A \u0438\u0433\u0440\u0430\u0442\u044C:" }),
295
+ /* @__PURE__ */ jsxs("ul", { children: [
296
+ /* @__PURE__ */ jsx("li", { children: "\u{1F4CD} \u0418\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0439\u0442\u0435 \u0441\u0442\u0440\u0435\u043B\u043A\u0438 \u0434\u043B\u044F \u0443\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0438\u044F" }),
297
+ /* @__PURE__ */ jsx("li", { children: "\u{1F34E} \u0421\u043E\u0431\u0438\u0440\u0430\u0439\u0442\u0435 \u043A\u0440\u0430\u0441\u043D\u044B\u0435 \u044F\u0431\u043B\u043E\u043A\u0438" }),
298
+ /* @__PURE__ */ jsx("li", { children: "\u{1F6AB} \u0418\u0437\u0431\u0435\u0433\u0430\u0439\u0442\u0435 \u0441\u0442\u0435\u043D \u0438 \u0441\u0435\u0431\u044F" }),
299
+ /* @__PURE__ */ jsx("li", { children: "\u{1F3AF} \u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043B\u043E\u0436\u043D\u043E\u0441\u0442\u044C \u0432\u044B\u0448\u0435" }),
300
+ /* @__PURE__ */ jsx("li", { children: "\u{1F3C6} \u041F\u043E\u0431\u0438\u0432\u0430\u0439\u0442\u0435 \u0441\u0432\u043E\u0439 \u0440\u0435\u043A\u043E\u0440\u0434!" })
301
+ ] }),
302
+ /* @__PURE__ */ jsx("button", { className: "start-instruction-btn", onClick: initializeGame, children: "\u041D\u0410\u0427\u0410\u0422\u042C \u0418\u0413\u0420\u0423" })
303
+ ] }) }) : gamePaused ? /* @__PURE__ */ jsxs("div", { className: "paused-screen", children: [
304
+ /* @__PURE__ */ jsx("h3", { children: "\u23F8\uFE0F \u0418\u0413\u0420\u0410 \u041D\u0410 \u041F\u0410\u0423\u0417\u0415" }),
305
+ /* @__PURE__ */ jsx("p", { children: '\u041D\u0430\u0436\u043C\u0438\u0442\u0435 \u043F\u0440\u043E\u0431\u0435\u043B \u0438\u043B\u0438 \u043A\u043D\u043E\u043F\u043A\u0443 "\u041F\u0440\u043E\u0434\u043E\u043B\u0436\u0438\u0442\u044C"' })
306
+ ] }) : /* @__PURE__ */ jsxs(
307
+ "div",
308
+ {
309
+ className: "game-board",
310
+ style: {
311
+ gridTemplateColumns: `repeat(${gridSize}, 1fr)`,
312
+ gridTemplateRows: `repeat(${gridSize}, 1fr)`
313
+ },
314
+ children: [
315
+ Array.from({ length: gridSize * gridSize }).map((_, index) => {
316
+ const y = Math.floor(index / gridSize);
317
+ const x = index % gridSize;
318
+ const isSnakeHead = snake[0]?.x === x && snake[0]?.y === y;
319
+ const isSnakeBody = snake.slice(1).some((segment) => segment.x === x && segment.y === y);
320
+ const isFood = food.x === x && food.y === y;
321
+ let cellClass = "grid-cell";
322
+ if (isSnakeHead) cellClass += " snake-head";
323
+ else if (isSnakeBody) cellClass += " snake-body";
324
+ else if (isFood) cellClass += " food";
325
+ return /* @__PURE__ */ jsx(
326
+ "div",
327
+ {
328
+ className: cellClass,
329
+ style: {
330
+ width: `${400 / gridSize}px`,
331
+ height: `${400 / gridSize}px`
332
+ }
333
+ },
334
+ `${x}-${y}`
335
+ );
336
+ }),
337
+ gameOver && /* @__PURE__ */ jsx("div", { className: "game-over-overlay", children: /* @__PURE__ */ jsxs("div", { className: "game-over-content", children: [
338
+ /* @__PURE__ */ jsx("h3", { children: "\u{1F480} \u0418\u0413\u0420\u0410 \u041E\u041A\u041E\u041D\u0427\u0415\u041D\u0410" }),
339
+ /* @__PURE__ */ jsxs("div", { className: "final-stats", children: [
340
+ /* @__PURE__ */ jsxs("p", { children: [
341
+ "\u0412\u0430\u0448 \u0441\u0447\u0435\u0442: ",
342
+ /* @__PURE__ */ jsx("strong", { children: score })
343
+ ] }),
344
+ /* @__PURE__ */ jsxs("p", { children: [
345
+ "\u0420\u0435\u043A\u043E\u0440\u0434: ",
346
+ /* @__PURE__ */ jsx("strong", { children: Math.max(score, highScore) })
347
+ ] }),
348
+ /* @__PURE__ */ jsxs("p", { children: [
349
+ "\u0414\u043B\u0438\u043D\u0430 \u0437\u043C\u0435\u0439\u043A\u0438: ",
350
+ /* @__PURE__ */ jsx("strong", { children: snake.length })
351
+ ] }),
352
+ /* @__PURE__ */ jsxs("p", { children: [
353
+ "\u0421\u043B\u043E\u0436\u043D\u043E\u0441\u0442\u044C: ",
354
+ /* @__PURE__ */ jsx("strong", { children: difficulty === "easy" ? "\u041B\u0435\u0433\u043A\u0430\u044F" : difficulty === "medium" ? "\u0421\u0440\u0435\u0434\u043D\u044F\u044F" : "\u0421\u043B\u043E\u0436\u043D\u0430\u044F" })
355
+ ] })
356
+ ] }),
357
+ /* @__PURE__ */ jsx(
358
+ "button",
359
+ {
360
+ className: "play-again-btn",
361
+ onClick: initializeGame,
362
+ children: "\u{1F3AE} \u0418\u0433\u0440\u0430\u0442\u044C \u0441\u043D\u043E\u0432\u0430"
363
+ }
364
+ )
365
+ ] }) })
366
+ ]
367
+ }
368
+ ) }),
369
+ /* @__PURE__ */ jsxs("div", { className: "mobile-controls", children: [
370
+ /* @__PURE__ */ jsx(
371
+ "button",
372
+ {
373
+ className: "mobile-btn up-btn",
374
+ onClick: () => handleButtonControl("UP"),
375
+ children: "\u2191"
376
+ }
377
+ ),
378
+ /* @__PURE__ */ jsxs("div", { className: "horizontal-controls", children: [
379
+ /* @__PURE__ */ jsx(
380
+ "button",
381
+ {
382
+ className: "mobile-btn left-btn",
383
+ onClick: () => handleButtonControl("LEFT"),
384
+ children: "\u2190"
385
+ }
386
+ ),
387
+ /* @__PURE__ */ jsx("div", { className: "center-space" }),
388
+ /* @__PURE__ */ jsx(
389
+ "button",
390
+ {
391
+ className: "mobile-btn right-btn",
392
+ onClick: () => handleButtonControl("RIGHT"),
393
+ children: "\u2192"
394
+ }
395
+ )
396
+ ] }),
397
+ /* @__PURE__ */ jsx(
398
+ "button",
399
+ {
400
+ className: "mobile-btn down-btn",
401
+ onClick: () => handleButtonControl("DOWN"),
402
+ children: "\u2193"
403
+ }
404
+ )
405
+ ] }),
406
+ /* @__PURE__ */ jsxs("div", { className: "game-instructions", children: [
407
+ /* @__PURE__ */ jsxs("div", { className: "key-instructions", children: [
408
+ /* @__PURE__ */ jsxs("p", { children: [
409
+ /* @__PURE__ */ jsx("strong", { children: "\u0423\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0438\u0435:" }),
410
+ " \u2190\u2191\u2193\u2192 \u0438\u043B\u0438 WASD"
411
+ ] }),
412
+ /* @__PURE__ */ jsxs("p", { children: [
413
+ /* @__PURE__ */ jsx("strong", { children: "\u041F\u0430\u0443\u0437\u0430:" }),
414
+ " \u041F\u0440\u043E\u0431\u0435\u043B \u2022 ",
415
+ /* @__PURE__ */ jsx("strong", { children: "\u0420\u0435\u0441\u0442\u0430\u0440\u0442:" }),
416
+ " Enter"
417
+ ] })
418
+ ] }),
419
+ /* @__PURE__ */ jsxs("div", { className: "legend", children: [
420
+ /* @__PURE__ */ jsxs("div", { className: "legend-item", children: [
421
+ /* @__PURE__ */ jsx("div", { className: "legend-color snake-head" }),
422
+ /* @__PURE__ */ jsx("span", { children: "\u0413\u043E\u043B\u043E\u0432\u0430 \u0437\u043C\u0435\u0439\u043A\u0438" })
423
+ ] }),
424
+ /* @__PURE__ */ jsxs("div", { className: "legend-item", children: [
425
+ /* @__PURE__ */ jsx("div", { className: "legend-color snake-body" }),
426
+ /* @__PURE__ */ jsx("span", { children: "\u0422\u0435\u043B\u043E \u0437\u043C\u0435\u0439\u043A\u0438" })
427
+ ] }),
428
+ /* @__PURE__ */ jsxs("div", { className: "legend-item", children: [
429
+ /* @__PURE__ */ jsx("div", { className: "legend-color food" }),
430
+ /* @__PURE__ */ jsx("span", { children: "\u0415\u0434\u0430 (+10 \u043E\u0447\u043A\u043E\u0432)" })
431
+ ] })
432
+ ] })
433
+ ] })
434
+ ] });
435
+ };
436
+ var SnakeGame_default = SnakeGame;
437
+
438
+ // src/games/Game2048/Game2048.js
439
+ import React2, { useState as useState2, useEffect as useEffect2, useCallback as useCallback2 } from "react";
440
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
441
+ var Game2048 = () => {
442
+ const [board, setBoard] = useState2([]);
443
+ const [score, setScore] = useState2(0);
444
+ const [gameOver, setGameOver] = useState2(false);
445
+ const [won, setWon] = useState2(false);
446
+ const [lastMove, setLastMove] = useState2(null);
447
+ const initializeBoard = () => {
448
+ const newBoard = Array(4).fill().map(() => Array(4).fill(0));
449
+ addRandomTile(newBoard);
450
+ addRandomTile(newBoard);
451
+ setBoard(newBoard);
452
+ setScore(0);
453
+ setGameOver(false);
454
+ setWon(false);
455
+ setLastMove(null);
456
+ };
457
+ const addRandomTile = (boardCopy) => {
458
+ const emptyCells = [];
459
+ for (let i = 0; i < 4; i++) {
460
+ for (let j = 0; j < 4; j++) {
461
+ if (boardCopy[i][j] === 0) {
462
+ emptyCells.push({ i, j });
463
+ }
464
+ }
465
+ }
466
+ if (emptyCells.length > 0) {
467
+ const { i, j } = emptyCells[Math.floor(Math.random() * emptyCells.length)];
468
+ boardCopy[i][j] = Math.random() < 0.9 ? 2 : 4;
469
+ }
470
+ return boardCopy;
471
+ };
472
+ const moveLeft = useCallback2((boardCopy) => {
473
+ let moved = false;
474
+ let newScore = score;
475
+ for (let i = 0; i < 4; i++) {
476
+ let row = boardCopy[i].filter((cell) => cell !== 0);
477
+ for (let j = 0; j < row.length - 1; j++) {
478
+ if (row[j] === row[j + 1]) {
479
+ row[j] *= 2;
480
+ newScore += row[j];
481
+ row.splice(j + 1, 1);
482
+ moved = true;
483
+ if (row[j] === 2048) {
484
+ setWon(true);
485
+ }
486
+ }
487
+ }
488
+ while (row.length < 4) {
489
+ row.push(0);
490
+ }
491
+ if (JSON.stringify(boardCopy[i]) !== JSON.stringify(row)) {
492
+ moved = true;
493
+ }
494
+ boardCopy[i] = row;
495
+ }
496
+ if (moved) {
497
+ setScore(newScore);
498
+ }
499
+ return moved;
500
+ }, [score]);
501
+ const rotateBoard = (boardToRotate, times = 1) => {
502
+ let result = boardToRotate.map((row) => [...row]);
503
+ for (let t = 0; t < times; t++) {
504
+ const newBoard = Array(4).fill().map(() => Array(4).fill(0));
505
+ for (let i = 0; i < 4; i++) {
506
+ for (let j = 0; j < 4; j++) {
507
+ newBoard[j][3 - i] = result[i][j];
508
+ }
509
+ }
510
+ result = newBoard;
511
+ }
512
+ return result;
513
+ };
514
+ const move = useCallback2((direction) => {
515
+ if (gameOver) return;
516
+ let boardCopy = board.map((row) => [...row]);
517
+ let moved = false;
518
+ const oldBoard = JSON.parse(JSON.stringify(boardCopy));
519
+ switch (direction) {
520
+ case "left":
521
+ moved = moveLeft(boardCopy);
522
+ setLastMove({ direction: "left", oldBoard, newBoard: boardCopy });
523
+ break;
524
+ case "right":
525
+ boardCopy = boardCopy.map((row) => [...row].reverse());
526
+ moved = moveLeft(boardCopy);
527
+ boardCopy = boardCopy.map((row) => [...row].reverse());
528
+ setLastMove({ direction: "right", oldBoard, newBoard: boardCopy });
529
+ break;
530
+ case "up":
531
+ boardCopy = rotateBoard(boardCopy, 3);
532
+ moved = moveLeft(boardCopy);
533
+ boardCopy = rotateBoard(boardCopy, 1);
534
+ setLastMove({ direction: "up", oldBoard, newBoard: boardCopy });
535
+ break;
536
+ case "down":
537
+ boardCopy = rotateBoard(boardCopy, 1);
538
+ moved = moveLeft(boardCopy);
539
+ boardCopy = rotateBoard(boardCopy, 3);
540
+ setLastMove({ direction: "down", oldBoard, newBoard: boardCopy });
541
+ break;
542
+ default:
543
+ break;
544
+ }
545
+ if (moved) {
546
+ addRandomTile(boardCopy);
547
+ setBoard(boardCopy);
548
+ const event = new CustomEvent("gameScoreUpdate", {
549
+ detail: { game: "2048", score }
550
+ });
551
+ window.dispatchEvent(event);
552
+ if (isGameOver(boardCopy)) {
553
+ setGameOver(true);
554
+ }
555
+ }
556
+ }, [board, gameOver, moveLeft]);
557
+ const isGameOver = (boardCopy) => {
558
+ for (let i = 0; i < 4; i++) {
559
+ for (let j = 0; j < 4; j++) {
560
+ if (boardCopy[i][j] === 0) return false;
561
+ }
562
+ }
563
+ for (let i = 0; i < 4; i++) {
564
+ for (let j = 0; j < 4; j++) {
565
+ if (j < 3 && boardCopy[i][j] === boardCopy[i][j + 1]) return false;
566
+ if (i < 3 && boardCopy[i][j] === boardCopy[i + 1][j]) return false;
567
+ }
568
+ }
569
+ return true;
570
+ };
571
+ useEffect2(() => {
572
+ initializeBoard();
573
+ }, []);
574
+ useEffect2(() => {
575
+ const handleKeyDown = (e) => {
576
+ e.preventDefault();
577
+ if (gameOver || won) return;
578
+ switch (e.key.toLowerCase()) {
579
+ case "arrowleft":
580
+ case "a":
581
+ move("left");
582
+ break;
583
+ case "arrowright":
584
+ case "d":
585
+ move("right");
586
+ break;
587
+ case "arrowup":
588
+ case "w":
589
+ move("up");
590
+ break;
591
+ case "arrowdown":
592
+ case "s":
593
+ move("down");
594
+ break;
595
+ case "r":
596
+ initializeBoard();
597
+ break;
598
+ default:
599
+ break;
600
+ }
601
+ };
602
+ window.addEventListener("keydown", handleKeyDown);
603
+ return () => window.removeEventListener("keydown", handleKeyDown);
604
+ }, [move, gameOver, won]);
605
+ const getTileColor = (value) => {
606
+ const colors = {
607
+ 2: "#eee4da",
608
+ 4: "#ede0c8",
609
+ 8: "#f2b179",
610
+ 16: "#f59563",
611
+ 32: "#f67c5f",
612
+ 64: "#f65e3b",
613
+ 128: "#edcf72",
614
+ 256: "#edcc61",
615
+ 512: "#edc850",
616
+ 1024: "#edc53f",
617
+ 2048: "#edc22e",
618
+ 4096: "#edc22e",
619
+ 8192: "#edc22e"
620
+ };
621
+ return colors[value] || "#3c3a32";
622
+ };
623
+ const getTileFontSize = (value) => {
624
+ if (value < 100) return "18px";
625
+ if (value < 1e3) return "16px";
626
+ if (value < 1e4) return "14px";
627
+ return "12px";
628
+ };
629
+ return /* @__PURE__ */ jsxs2("div", { className: "game-2048", children: [
630
+ /* @__PURE__ */ jsxs2("div", { className: "game-header", children: [
631
+ /* @__PURE__ */ jsx2("div", { className: "header-left", children: /* @__PURE__ */ jsx2("div", { className: "game-subtitle", children: "\u041E\u0431\u044A\u0435\u0434\u0438\u043D\u044F\u0439\u0442\u0435 \u043F\u043B\u0438\u0442\u043A\u0438, \u0447\u0442\u043E\u0431\u044B \u043F\u043E\u043B\u0443\u0447\u0438\u0442\u044C 2048!" }) }),
632
+ /* @__PURE__ */ jsxs2("div", { className: "header-right", children: [
633
+ /* @__PURE__ */ jsxs2("div", { className: "score-display", children: [
634
+ /* @__PURE__ */ jsx2("div", { className: "score-label", children: "\u0421\u0447\u0435\u0442" }),
635
+ /* @__PURE__ */ jsx2("div", { className: "score-value", children: score })
636
+ ] }),
637
+ /* @__PURE__ */ jsx2("div", { className: "game-controls", children: /* @__PURE__ */ jsx2(
638
+ "button",
639
+ {
640
+ className: "control-btn new-game-btn",
641
+ onClick: initializeBoard,
642
+ children: "\u041D\u043E\u0432\u0430\u044F \u0438\u0433\u0440\u0430"
643
+ }
644
+ ) })
645
+ ] })
646
+ ] }),
647
+ /* @__PURE__ */ jsxs2("div", { className: "instructions", children: [
648
+ /* @__PURE__ */ jsxs2("p", { children: [
649
+ "\u0418\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0439\u0442\u0435 ",
650
+ /* @__PURE__ */ jsx2("strong", { children: "\u0441\u0442\u0440\u0435\u043B\u043A\u0438" }),
651
+ " \u0438\u043B\u0438 ",
652
+ /* @__PURE__ */ jsx2("strong", { children: "WASD" }),
653
+ " \u0434\u043B\u044F \u0434\u0432\u0438\u0436\u0435\u043D\u0438\u044F"
654
+ ] }),
655
+ /* @__PURE__ */ jsxs2("p", { children: [
656
+ "\u041D\u0430\u0436\u043C\u0438\u0442\u0435 ",
657
+ /* @__PURE__ */ jsx2("strong", { children: "R" }),
658
+ " \u0434\u043B\u044F \u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A\u0430"
659
+ ] })
660
+ ] }),
661
+ /* @__PURE__ */ jsxs2("div", { className: "game-area", children: [
662
+ /* @__PURE__ */ jsx2("div", { className: "board-container", children: /* @__PURE__ */ jsx2("div", { className: "board", children: board.map((row, i) => /* @__PURE__ */ jsx2("div", { className: "row", children: row.map((cell, j) => /* @__PURE__ */ jsx2("div", { className: "cell", children: cell !== 0 && /* @__PURE__ */ jsx2(
663
+ "div",
664
+ {
665
+ className: `tile tile-${cell}`,
666
+ style: {
667
+ backgroundColor: getTileColor(cell),
668
+ color: cell <= 4 ? "#776e65" : "#f9f6f2",
669
+ fontSize: getTileFontSize(cell)
670
+ },
671
+ "data-value": cell,
672
+ children: cell
673
+ }
674
+ ) }, `${i}-${j}`)) }, i)) }) }),
675
+ /* @__PURE__ */ jsxs2("div", { className: "mobile-controls", children: [
676
+ /* @__PURE__ */ jsx2("div", { className: "control-row", children: /* @__PURE__ */ jsx2(
677
+ "button",
678
+ {
679
+ className: "mobile-control-btn up",
680
+ onClick: () => move("up"),
681
+ children: "\u2191"
682
+ }
683
+ ) }),
684
+ /* @__PURE__ */ jsxs2("div", { className: "control-row", children: [
685
+ /* @__PURE__ */ jsx2(
686
+ "button",
687
+ {
688
+ className: "mobile-control-btn left",
689
+ onClick: () => move("left"),
690
+ children: "\u2190"
691
+ }
692
+ ),
693
+ /* @__PURE__ */ jsx2("div", { className: "spacer" }),
694
+ /* @__PURE__ */ jsx2(
695
+ "button",
696
+ {
697
+ className: "mobile-control-btn right",
698
+ onClick: () => move("right"),
699
+ children: "\u2192"
700
+ }
701
+ )
702
+ ] }),
703
+ /* @__PURE__ */ jsx2("div", { className: "control-row", children: /* @__PURE__ */ jsx2(
704
+ "button",
705
+ {
706
+ className: "mobile-control-btn down",
707
+ onClick: () => move("down"),
708
+ children: "\u2193"
709
+ }
710
+ ) })
711
+ ] })
712
+ ] }),
713
+ gameOver && /* @__PURE__ */ jsx2("div", { className: "game-over-overlay", children: /* @__PURE__ */ jsxs2("div", { className: "game-over-content", children: [
714
+ /* @__PURE__ */ jsx2("h3", { children: "\u{1F3AE} \u0418\u0433\u0440\u0430 \u043E\u043A\u043E\u043D\u0447\u0435\u043D\u0430!" }),
715
+ /* @__PURE__ */ jsxs2("div", { className: "final-score", children: [
716
+ /* @__PURE__ */ jsxs2("p", { children: [
717
+ "\u0412\u0430\u0448 \u0441\u0447\u0435\u0442: ",
718
+ /* @__PURE__ */ jsx2("strong", { children: score })
719
+ ] }),
720
+ /* @__PURE__ */ jsxs2("p", { children: [
721
+ "\u041C\u0430\u043A\u0441\u0438\u043C\u0430\u043B\u044C\u043D\u0430\u044F \u043F\u043B\u0438\u0442\u043A\u0430: ",
722
+ /* @__PURE__ */ jsx2("strong", { children: Math.max(...board.flat()) })
723
+ ] })
724
+ ] }),
725
+ /* @__PURE__ */ jsx2(
726
+ "button",
727
+ {
728
+ className: "play-again-btn",
729
+ onClick: initializeBoard,
730
+ children: "\u0418\u0433\u0440\u0430\u0442\u044C \u0441\u043D\u043E\u0432\u0430"
731
+ }
732
+ )
733
+ ] }) }),
734
+ won && /* @__PURE__ */ jsx2("div", { className: "win-overlay", children: /* @__PURE__ */ jsxs2("div", { className: "win-content", children: [
735
+ /* @__PURE__ */ jsx2("div", { className: "confetti", children: "\u{1F389}" }),
736
+ /* @__PURE__ */ jsx2("h3", { children: "\u{1F3C6} \u041F\u043E\u0431\u0435\u0434\u0430!" }),
737
+ /* @__PURE__ */ jsx2("p", { children: "\u0412\u044B \u043F\u043E\u043B\u0443\u0447\u0438\u043B\u0438 \u043F\u043B\u0438\u0442\u043A\u0443 2048!" }),
738
+ /* @__PURE__ */ jsxs2("div", { className: "win-buttons", children: [
739
+ /* @__PURE__ */ jsx2(
740
+ "button",
741
+ {
742
+ className: "continue-btn",
743
+ onClick: () => setWon(false),
744
+ children: "\u041F\u0440\u043E\u0434\u043E\u043B\u0436\u0438\u0442\u044C"
745
+ }
746
+ ),
747
+ /* @__PURE__ */ jsx2(
748
+ "button",
749
+ {
750
+ className: "restart-btn",
751
+ onClick: initializeBoard,
752
+ children: "\u041D\u043E\u0432\u0430\u044F \u0438\u0433\u0440\u0430"
753
+ }
754
+ )
755
+ ] })
756
+ ] }) })
757
+ ] });
758
+ };
759
+ var Game2048_default = Game2048;
760
+
761
+ // src/games/Memory/MemoryGame.js
762
+ import React3, { useState as useState3, useEffect as useEffect3 } from "react";
763
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
764
+ var MemoryGame = () => {
765
+ const [difficulty, setDifficulty] = useState3("medium");
766
+ const [cards, setCards] = useState3([]);
767
+ const [flipped, setFlipped] = useState3([]);
768
+ const [solved, setSolved] = useState3([]);
769
+ const [disabled, setDisabled] = useState3(false);
770
+ const [score, setScore] = useState3(0);
771
+ const [moves, setMoves] = useState3(0);
772
+ const difficulties = {
773
+ easy: { pairs: 8, gridSize: 4 },
774
+ medium: { pairs: 12, gridSize: 4 },
775
+ hard: { pairs: 16, gridSize: 4 }
776
+ };
777
+ const emojis = ["\u{1F436}", "\u{1F431}", "\u{1F42D}", "\u{1F439}", "\u{1F430}", "\u{1F98A}", "\u{1F43B}", "\u{1F43C}", "\u{1F428}", "\u{1F42F}", "\u{1F981}", "\u{1F42E}", "\u{1F437}", "\u{1F438}", "\u{1F435}", "\u{1F414}"];
778
+ useEffect3(() => {
779
+ initializeGame();
780
+ }, [difficulty]);
781
+ useEffect3(() => {
782
+ if (score > 0) {
783
+ const event = new CustomEvent("gameScoreUpdate", {
784
+ detail: { game: "memory", score }
785
+ });
786
+ window.dispatchEvent(event);
787
+ }
788
+ }, [score]);
789
+ const initializeGame = () => {
790
+ const { pairs } = difficulties[difficulty];
791
+ const selectedEmojis = emojis.slice(0, pairs);
792
+ const cardPairs = [...selectedEmojis, ...selectedEmojis];
793
+ const shuffledCards = cardPairs.sort(() => Math.random() - 0.5).map((emoji, index) => ({
794
+ id: index,
795
+ emoji,
796
+ flipped: false
797
+ }));
798
+ setCards(shuffledCards);
799
+ setFlipped([]);
800
+ setSolved([]);
801
+ setScore(0);
802
+ setMoves(0);
803
+ };
804
+ const handleCardClick = (id) => {
805
+ if (disabled || flipped.length === 2 || solved.includes(id) || flipped.includes(id)) {
806
+ return;
807
+ }
808
+ const newFlipped = [...flipped, id];
809
+ setFlipped(newFlipped);
810
+ setMoves(moves + 1);
811
+ if (newFlipped.length === 2) {
812
+ setDisabled(true);
813
+ const [firstId, secondId] = newFlipped;
814
+ const firstCard = cards.find((card) => card.id === firstId);
815
+ const secondCard = cards.find((card) => card.id === secondId);
816
+ if (firstCard.emoji === secondCard.emoji) {
817
+ const bonus = difficulty === "hard" ? 15 : difficulty === "medium" ? 10 : 5;
818
+ setSolved([...solved, firstId, secondId]);
819
+ setScore(score + bonus);
820
+ setFlipped([]);
821
+ setDisabled(false);
822
+ } else {
823
+ setTimeout(() => {
824
+ setFlipped([]);
825
+ setDisabled(false);
826
+ }, 1e3);
827
+ }
828
+ }
829
+ };
830
+ const changeDifficulty = (newDifficulty) => {
831
+ if (window.confirm("\u0421\u043C\u0435\u043D\u0438\u0442\u044C \u0441\u043B\u043E\u0436\u043D\u043E\u0441\u0442\u044C? \u0422\u0435\u043A\u0443\u0449\u0430\u044F \u0438\u0433\u0440\u0430 \u0431\u0443\u0434\u0435\u0442 \u0441\u0431\u0440\u043E\u0448\u0435\u043D\u0430.")) {
832
+ setDifficulty(newDifficulty);
833
+ }
834
+ };
835
+ const getCardSize = () => {
836
+ switch (difficulty) {
837
+ case "easy":
838
+ return "large";
839
+ case "medium":
840
+ return "medium";
841
+ case "hard":
842
+ return "small";
843
+ default:
844
+ return "medium";
845
+ }
846
+ };
847
+ return /* @__PURE__ */ jsxs3("div", { className: "memory-game", children: [
848
+ /* @__PURE__ */ jsxs3("div", { className: "difficulty-selector", children: [
849
+ /* @__PURE__ */ jsx3("div", { className: "difficulty-label", children: "\u0421\u043B\u043E\u0436\u043D\u043E\u0441\u0442\u044C:" }),
850
+ ["easy", "medium", "hard"].map((diff) => /* @__PURE__ */ jsxs3(
851
+ "button",
852
+ {
853
+ className: `difficulty-btn ${difficulty === diff ? "active" : ""}`,
854
+ onClick: () => changeDifficulty(diff),
855
+ children: [
856
+ diff === "easy" && "\u{1F422} \u041B\u0435\u0433\u043A\u0430\u044F (8 \u043F\u0430\u0440)",
857
+ diff === "medium" && "\u26A1 \u0421\u0440\u0435\u0434\u043D\u044F\u044F (12 \u043F\u0430\u0440)",
858
+ diff === "hard" && "\u{1F525} \u0421\u043B\u043E\u0436\u043D\u0430\u044F (16 \u043F\u0430\u0440)"
859
+ ]
860
+ },
861
+ diff
862
+ ))
863
+ ] }),
864
+ /* @__PURE__ */ jsxs3("div", { className: "game-stats", children: [
865
+ /* @__PURE__ */ jsxs3("div", { className: "stat", children: [
866
+ /* @__PURE__ */ jsx3("span", { children: "\u0421\u0447\u0435\u0442:" }),
867
+ /* @__PURE__ */ jsx3("strong", { children: score })
868
+ ] }),
869
+ /* @__PURE__ */ jsxs3("div", { className: "stat", children: [
870
+ /* @__PURE__ */ jsx3("span", { children: "\u0425\u043E\u0434\u044B:" }),
871
+ /* @__PURE__ */ jsx3("strong", { children: moves })
872
+ ] }),
873
+ /* @__PURE__ */ jsxs3("div", { className: "stat", children: [
874
+ /* @__PURE__ */ jsx3("span", { children: "\u041D\u0430\u0439\u0434\u0435\u043D\u043E \u043F\u0430\u0440:" }),
875
+ /* @__PURE__ */ jsx3("strong", { children: solved.length / 2 })
876
+ ] })
877
+ ] }),
878
+ /* @__PURE__ */ jsx3("div", { className: "memory-board-container", children: /* @__PURE__ */ jsx3("div", { className: `memory-board ${difficulty}`, children: cards.map((card) => /* @__PURE__ */ jsxs3(
879
+ "div",
880
+ {
881
+ className: `memory-card ${getCardSize()} ${flipped.includes(card.id) || solved.includes(card.id) ? "flipped" : ""} ${solved.includes(card.id) ? "matched" : ""}`,
882
+ onClick: () => handleCardClick(card.id),
883
+ children: [
884
+ /* @__PURE__ */ jsx3("div", { className: "card-front", children: "?" }),
885
+ /* @__PURE__ */ jsx3("div", { className: "card-back", children: card.emoji })
886
+ ]
887
+ },
888
+ card.id
889
+ )) }) }),
890
+ /* @__PURE__ */ jsx3("div", { className: "game-controls", children: /* @__PURE__ */ jsx3("button", { className: "restart-btn", onClick: initializeGame, children: "\u041D\u043E\u0432\u0430\u044F \u0438\u0433\u0440\u0430" }) }),
891
+ /* @__PURE__ */ jsxs3("div", { className: "game-instructions", children: [
892
+ /* @__PURE__ */ jsx3("p", { children: "\u041D\u0430\u0439\u0434\u0438\u0442\u0435 \u0432\u0441\u0435 \u043F\u0430\u0440\u044B \u043E\u0434\u0438\u043D\u0430\u043A\u043E\u0432\u044B\u0445 \u044D\u043C\u043E\u0434\u0437\u0438!" }),
893
+ /* @__PURE__ */ jsxs3("p", { children: [
894
+ "\u0411\u043E\u043D\u0443\u0441 \u0437\u0430 \u043F\u0430\u0440\u0443: ",
895
+ difficulty === "hard" ? "15" : difficulty === "medium" ? "10" : "5",
896
+ " \u043E\u0447\u043A\u043E\u0432"
897
+ ] })
898
+ ] })
899
+ ] });
900
+ };
901
+ var MemoryGame_default = MemoryGame;
902
+
903
+ // src/games/Clicker/ClickerGame.js
904
+ import React4, { useState as useState4, useEffect as useEffect4 } from "react";
905
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
906
+ var ClickerGame = () => {
907
+ const [clicks, setClicks] = useState4(0);
908
+ const [clickPower, setClickPower] = useState4(1);
909
+ const [autoClickerLevel, setAutoClickerLevel] = useState4(0);
910
+ const [autoClickerRate, setAutoClickerRate] = useState4(0);
911
+ const [multiplier, setMultiplier] = useState4(1);
912
+ const [upgrades, setUpgrades] = useState4([
913
+ { id: 1, name: "\u{1F4AA} \u0411\u043E\u043B\u0435\u0435 \u0441\u0438\u043B\u044C\u043D\u044B\u0439 \u043F\u0430\u043B\u0435\u0446", cost: 50, power: 2, purchased: false },
914
+ { id: 2, name: "\u26A1 \u042D\u043D\u0435\u0440\u0433\u0435\u0442\u0438\u043A", cost: 200, power: 5, purchased: false },
915
+ { id: 3, name: "\u{1F680} \u041A\u043E\u0441\u043C\u0438\u0447\u0435\u0441\u043A\u0438\u0439 \u043A\u043B\u0438\u043A\u0435\u0440", cost: 1e3, power: 10, purchased: false },
916
+ { id: 4, name: "\u{1F916} \u0410\u0432\u0442\u043E\u043A\u043B\u0438\u043A\u0435\u0440 Lv1", cost: 500, rate: 1, purchased: false },
917
+ { id: 5, name: "\u{1F3ED} \u0410\u0432\u0442\u043E\u043A\u043B\u0438\u043A\u0435\u0440 Lv2", cost: 2e3, rate: 5, purchased: false },
918
+ { id: 6, name: "\u{1F680} \u0410\u0432\u0442\u043E\u043A\u043B\u0438\u043A\u0435\u0440 Lv3", cost: 1e4, rate: 10, purchased: false },
919
+ { id: 7, name: "\u2728 \u041C\u043D\u043E\u0436\u0438\u0442\u0435\u043B\u044C x2", cost: 1500, mult: 2, purchased: false },
920
+ { id: 8, name: "\u{1F308} \u041C\u043D\u043E\u0436\u0438\u0442\u0435\u043B\u044C x5", cost: 7500, mult: 5, purchased: false }
921
+ ]);
922
+ const [abilities, setAbilities] = useState4([
923
+ { id: 1, name: "\u{1F4A5} \u0412\u0437\u0440\u044B\u0432\u043D\u043E\u0439 \u043A\u043B\u0438\u043A", cost: 100, cooldown: 30, currentCd: 0, power: 100 },
924
+ { id: 2, name: "\u23F1\uFE0F \u0423\u0441\u043A\u043E\u0440\u0435\u043D\u0438\u0435 \u0432\u0440\u0435\u043C\u0435\u043D\u0438", cost: 300, cooldown: 60, currentCd: 0, effect: "double" },
925
+ { id: 3, name: "\u{1F4B0} \u0417\u043E\u043B\u043E\u0442\u0430\u044F \u043B\u0438\u0445\u043E\u0440\u0430\u0434\u043A\u0430", cost: 1e3, cooldown: 120, currentCd: 0, effect: "bonus" }
926
+ ]);
927
+ const [achievements, setAchievements] = useState4([
928
+ { id: 1, name: "\u{1F3AF} \u041F\u0435\u0440\u0432\u044B\u0435 100 \u043A\u043B\u0438\u043A\u043E\u0432", target: 100, achieved: false },
929
+ { id: 2, name: "\u{1F4AA} 1000 \u0441\u0438\u043B\u044B", target: 1e3, achieved: false },
930
+ { id: 3, name: "\u{1F680} \u0410\u0432\u0442\u043E\u043A\u043B\u0438\u043A\u0435\u0440 \u043C\u0430\u0441\u0442\u0435\u0440", target: 5, achieved: false },
931
+ { id: 4, name: "\u{1F4B0} \u041C\u0438\u043B\u043B\u0438\u043E\u043D\u0435\u0440", target: 1e6, achieved: false }
932
+ ]);
933
+ const [shopOpen, setShopOpen] = useState4(false);
934
+ useEffect4(() => {
935
+ if (autoClickerRate > 0) {
936
+ const interval = setInterval(() => {
937
+ setClicks((prev) => prev + autoClickerRate * multiplier);
938
+ }, 1e3);
939
+ return () => clearInterval(interval);
940
+ }
941
+ }, [autoClickerRate, multiplier]);
942
+ useEffect4(() => {
943
+ const abilityInterval = setInterval(() => {
944
+ setAbilities((prev) => prev.map((ability) => ({
945
+ ...ability,
946
+ currentCd: ability.currentCd > 0 ? ability.currentCd - 1 : 0
947
+ })));
948
+ }, 1e3);
949
+ return () => clearInterval(abilityInterval);
950
+ }, []);
951
+ useEffect4(() => {
952
+ checkAchievements();
953
+ const event = new CustomEvent("gameScoreUpdate", {
954
+ detail: { game: "clicker", score: clicks }
955
+ });
956
+ window.dispatchEvent(event);
957
+ }, [clicks, clickPower, autoClickerLevel]);
958
+ const handleClick = () => {
959
+ setClicks((prev) => prev + clickPower * multiplier);
960
+ };
961
+ const buyUpgrade = (upgrade) => {
962
+ if (clicks >= upgrade.cost && !upgrade.purchased) {
963
+ setClicks((prev) => prev - upgrade.cost);
964
+ if (upgrade.power) {
965
+ setClickPower((prev) => prev + upgrade.power);
966
+ } else if (upgrade.rate) {
967
+ setAutoClickerLevel((prev) => prev + 1);
968
+ setAutoClickerRate((prev) => prev + upgrade.rate);
969
+ } else if (upgrade.mult) {
970
+ setMultiplier((prev) => prev * upgrade.mult);
971
+ }
972
+ setUpgrades((prev) => prev.map(
973
+ (u) => u.id === upgrade.id ? { ...u, purchased: true } : u
974
+ ));
975
+ }
976
+ };
977
+ const activateAbility = (ability) => {
978
+ if (clicks >= ability.cost && ability.currentCd === 0) {
979
+ setClicks((prev) => prev - ability.cost);
980
+ if (ability.power) {
981
+ setClicks((prev) => prev + ability.power * multiplier);
982
+ } else if (ability.effect === "double") {
983
+ const oldMultiplier = multiplier;
984
+ setMultiplier((prev) => prev * 2);
985
+ setTimeout(() => setMultiplier(oldMultiplier), 1e4);
986
+ } else if (ability.effect === "bonus") {
987
+ setClicks((prev) => prev + 5e3 * multiplier);
988
+ }
989
+ setAbilities((prev) => prev.map(
990
+ (a) => a.id === ability.id ? { ...a, currentCd: a.cooldown } : a
991
+ ));
992
+ }
993
+ };
994
+ const checkAchievements = () => {
995
+ const newAchievements = achievements.map((achievement) => {
996
+ let achieved = false;
997
+ switch (achievement.id) {
998
+ case 1:
999
+ achieved = clicks >= achievement.target;
1000
+ break;
1001
+ case 2:
1002
+ achieved = clickPower >= achievement.target;
1003
+ break;
1004
+ case 3:
1005
+ achieved = autoClickerLevel >= achievement.target;
1006
+ break;
1007
+ case 4:
1008
+ achieved = clicks >= achievement.target;
1009
+ break;
1010
+ default:
1011
+ break;
1012
+ }
1013
+ return { ...achievement, achieved };
1014
+ });
1015
+ setAchievements(newAchievements);
1016
+ };
1017
+ const formatNumber = (num) => {
1018
+ if (num >= 1e6) return `${(num / 1e6).toFixed(1)}M`;
1019
+ if (num >= 1e3) return `${(num / 1e3).toFixed(1)}K`;
1020
+ return num.toString();
1021
+ };
1022
+ return /* @__PURE__ */ jsxs4("div", { className: "clicker-game", children: [
1023
+ /* @__PURE__ */ jsx4("div", { className: "game-header", children: /* @__PURE__ */ jsx4("div", { className: "header-controls", children: /* @__PURE__ */ jsx4(
1024
+ "button",
1025
+ {
1026
+ className: `shop-toggle ${shopOpen ? "active" : ""}`,
1027
+ onClick: () => setShopOpen(!shopOpen),
1028
+ children: shopOpen ? "\u2716\uFE0F \u0417\u0430\u043A\u0440\u044B\u0442\u044C \u043C\u0430\u0433\u0430\u0437\u0438\u043D" : "\u{1F6D2} \u041C\u0430\u0433\u0430\u0437\u0438\u043D"
1029
+ }
1030
+ ) }) }),
1031
+ /* @__PURE__ */ jsxs4("div", { className: "game-stats", children: [
1032
+ /* @__PURE__ */ jsxs4("div", { className: "stat-box", children: [
1033
+ /* @__PURE__ */ jsx4("div", { className: "stat-label", children: "\u041A\u043B\u0438\u043A\u043E\u0432" }),
1034
+ /* @__PURE__ */ jsx4("div", { className: "stat-value click-count", children: formatNumber(clicks) })
1035
+ ] }),
1036
+ /* @__PURE__ */ jsxs4("div", { className: "stat-box", children: [
1037
+ /* @__PURE__ */ jsx4("div", { className: "stat-label", children: "\u0421\u0438\u043B\u0430 \u043A\u043B\u0438\u043A\u0430" }),
1038
+ /* @__PURE__ */ jsx4("div", { className: "stat-value", children: clickPower })
1039
+ ] }),
1040
+ /* @__PURE__ */ jsxs4("div", { className: "stat-box", children: [
1041
+ /* @__PURE__ */ jsx4("div", { className: "stat-label", children: "\u041C\u043D\u043E\u0436\u0438\u0442\u0435\u043B\u044C" }),
1042
+ /* @__PURE__ */ jsxs4("div", { className: "stat-value multiplier", children: [
1043
+ "x",
1044
+ multiplier
1045
+ ] })
1046
+ ] }),
1047
+ /* @__PURE__ */ jsxs4("div", { className: "stat-box", children: [
1048
+ /* @__PURE__ */ jsx4("div", { className: "stat-label", children: "\u0410\u0432\u0442\u043E\u043A\u043B\u0438\u043A\u0435\u0440" }),
1049
+ /* @__PURE__ */ jsxs4("div", { className: "stat-value", children: [
1050
+ autoClickerRate,
1051
+ "/\u0441\u0435\u043A"
1052
+ ] })
1053
+ ] })
1054
+ ] }),
1055
+ /* @__PURE__ */ jsxs4("div", { className: "main-game-area", children: [
1056
+ /* @__PURE__ */ jsxs4("div", { className: "clicker-container", children: [
1057
+ /* @__PURE__ */ jsx4(
1058
+ "button",
1059
+ {
1060
+ className: "click-button",
1061
+ onClick: handleClick,
1062
+ children: /* @__PURE__ */ jsxs4("div", { className: "click-button-text", children: [
1063
+ /* @__PURE__ */ jsxs4("div", { className: "click-power", children: [
1064
+ "+",
1065
+ clickPower * multiplier
1066
+ ] }),
1067
+ /* @__PURE__ */ jsx4("div", { className: "click-label", children: "\u041A\u041B\u0418\u041A\u0410\u0419!" }),
1068
+ /* @__PURE__ */ jsx4("div", { className: "click-hint", children: "\u0417\u0430\u0440\u0430\u0431\u0430\u0442\u044B\u0432\u0430\u0439 \u043E\u0447\u043A\u0438" })
1069
+ ] })
1070
+ }
1071
+ ),
1072
+ /* @__PURE__ */ jsxs4("div", { className: "abilities", children: [
1073
+ /* @__PURE__ */ jsx4("h3", { children: "\u{1F52E} \u0421\u043F\u043E\u0441\u043E\u0431\u043D\u043E\u0441\u0442\u0438" }),
1074
+ /* @__PURE__ */ jsx4("div", { className: "abilities-grid", children: abilities.map((ability) => /* @__PURE__ */ jsxs4(
1075
+ "button",
1076
+ {
1077
+ className: `ability-btn ${ability.currentCd > 0 ? "cooldown" : ""}`,
1078
+ onClick: () => activateAbility(ability),
1079
+ disabled: ability.currentCd > 0 || clicks < ability.cost,
1080
+ children: [
1081
+ /* @__PURE__ */ jsx4("div", { className: "ability-name", children: ability.name }),
1082
+ /* @__PURE__ */ jsxs4("div", { className: "ability-cost", children: [
1083
+ "\u{1F4B0} ",
1084
+ formatNumber(ability.cost)
1085
+ ] }),
1086
+ /* @__PURE__ */ jsx4("div", { className: "ability-cooldown", children: ability.currentCd > 0 ? `${ability.currentCd}\u0441` : "\u0413\u043E\u0442\u043E\u0432\u043E!" })
1087
+ ]
1088
+ },
1089
+ ability.id
1090
+ )) })
1091
+ ] })
1092
+ ] }),
1093
+ shopOpen && /* @__PURE__ */ jsxs4("div", { className: "shop-panel", children: [
1094
+ /* @__PURE__ */ jsx4("h3", { children: "\u{1F6D2} \u041C\u0430\u0433\u0430\u0437\u0438\u043D \u0443\u043B\u0443\u0447\u0448\u0435\u043D\u0438\u0439" }),
1095
+ /* @__PURE__ */ jsx4("div", { className: "upgrades-grid", children: upgrades.map((upgrade) => /* @__PURE__ */ jsxs4(
1096
+ "div",
1097
+ {
1098
+ className: `upgrade-item ${upgrade.purchased ? "purchased" : ""} ${clicks >= upgrade.cost ? "affordable" : ""}`,
1099
+ children: [
1100
+ /* @__PURE__ */ jsxs4("div", { className: "upgrade-content", children: [
1101
+ /* @__PURE__ */ jsx4("div", { className: "upgrade-name", children: upgrade.name }),
1102
+ /* @__PURE__ */ jsxs4("div", { className: "upgrade-stats", children: [
1103
+ upgrade.power && /* @__PURE__ */ jsxs4("span", { children: [
1104
+ "+",
1105
+ upgrade.power,
1106
+ " \u0441\u0438\u043B\u044B"
1107
+ ] }),
1108
+ upgrade.rate && /* @__PURE__ */ jsxs4("span", { children: [
1109
+ "+",
1110
+ upgrade.rate,
1111
+ "/\u0441\u0435\u043A"
1112
+ ] }),
1113
+ upgrade.mult && /* @__PURE__ */ jsxs4("span", { children: [
1114
+ "x",
1115
+ upgrade.mult,
1116
+ " \u043C\u043D\u043E\u0436\u0438\u0442\u0435\u043B\u044C"
1117
+ ] })
1118
+ ] }),
1119
+ /* @__PURE__ */ jsxs4("div", { className: "upgrade-cost", children: [
1120
+ "\u{1F4B0} ",
1121
+ formatNumber(upgrade.cost)
1122
+ ] })
1123
+ ] }),
1124
+ /* @__PURE__ */ jsx4(
1125
+ "button",
1126
+ {
1127
+ className: "buy-btn",
1128
+ onClick: () => buyUpgrade(upgrade),
1129
+ disabled: upgrade.purchased || clicks < upgrade.cost,
1130
+ children: upgrade.purchased ? "\u041A\u0443\u043F\u043B\u0435\u043D\u043E" : "\u041A\u0443\u043F\u0438\u0442\u044C"
1131
+ }
1132
+ )
1133
+ ]
1134
+ },
1135
+ upgrade.id
1136
+ )) })
1137
+ ] })
1138
+ ] }),
1139
+ /* @__PURE__ */ jsxs4("div", { className: "achievements", children: [
1140
+ /* @__PURE__ */ jsx4("h3", { children: "\u{1F3C6} \u0414\u043E\u0441\u0442\u0438\u0436\u0435\u043D\u0438\u044F" }),
1141
+ /* @__PURE__ */ jsx4("div", { className: "achievements-grid", children: achievements.map((achievement) => /* @__PURE__ */ jsxs4(
1142
+ "div",
1143
+ {
1144
+ className: `achievement ${achievement.achieved ? "achieved" : ""}`,
1145
+ children: [
1146
+ /* @__PURE__ */ jsx4("div", { className: "achievement-icon", children: achievement.achieved ? "\u2705" : "\u{1F512}" }),
1147
+ /* @__PURE__ */ jsxs4("div", { className: "achievement-info", children: [
1148
+ /* @__PURE__ */ jsx4("div", { className: "achievement-name", children: achievement.name }),
1149
+ /* @__PURE__ */ jsxs4("div", { className: "achievement-progress", children: [
1150
+ "\u041F\u0440\u043E\u0433\u0440\u0435\u0441\u0441: ",
1151
+ formatNumber(achievement.target)
1152
+ ] })
1153
+ ] })
1154
+ ]
1155
+ },
1156
+ achievement.id
1157
+ )) })
1158
+ ] }),
1159
+ /* @__PURE__ */ jsxs4("div", { className: "game-instructions", children: [
1160
+ /* @__PURE__ */ jsxs4("p", { children: [
1161
+ "\u{1F4A1} ",
1162
+ /* @__PURE__ */ jsx4("strong", { children: "\u0421\u043E\u0432\u0435\u0442:" }),
1163
+ " \u041F\u043E\u043A\u0443\u043F\u0430\u0439\u0442\u0435 \u0430\u0432\u0442\u043E\u043A\u043B\u0438\u043A\u0435\u0440\u044B \u0447\u0442\u043E\u0431\u044B \u0437\u0430\u0440\u0430\u0431\u0430\u0442\u044B\u0432\u0430\u0442\u044C \u043F\u0430\u0441\u0441\u0438\u0432\u043D\u043E!"
1164
+ ] }),
1165
+ /* @__PURE__ */ jsx4("p", { children: "\u{1F3AF} \u0418\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0439\u0442\u0435 \u0441\u043F\u043E\u0441\u043E\u0431\u043D\u043E\u0441\u0442\u0438 \u0434\u043B\u044F \u0431\u043E\u043B\u044C\u0448\u0438\u0445 \u0440\u044B\u0432\u043A\u043E\u0432!" })
1166
+ ] })
1167
+ ] });
1168
+ };
1169
+ var ClickerGame_default = ClickerGame;
1170
+
1171
+ // src/games/Pong/Pong.js
1172
+ import React5, { useState as useState5, useEffect as useEffect5, useRef, useCallback as useCallback3 } from "react";
1173
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1174
+ var Pong = () => {
1175
+ const [gameActive, setGameActive] = useState5(false);
1176
+ const [playerScore, setPlayerScore] = useState5(0);
1177
+ const [computerScore, setComputerScore] = useState5(0);
1178
+ const [roundSpeed, setRoundSpeed] = useState5(1);
1179
+ const [winner, setWinner] = useState5(null);
1180
+ const [highScore, setHighScore] = useState5(0);
1181
+ const [winStreak, setWinStreak] = useState5(0);
1182
+ const [roundTime, setRoundTime] = useState5(0);
1183
+ const canvasRef = useRef(null);
1184
+ const animationRef = useRef(null);
1185
+ const roundStartTimeRef = useRef(0);
1186
+ const timeIntervalRef = useRef(null);
1187
+ const gameStateRef = useRef({
1188
+ player: { x: 0, y: 180, width: 10, height: 60, score: 0 },
1189
+ computer: { x: 390, y: 180, width: 10, height: 60, score: 0 },
1190
+ ball: { x: 200, y: 200, radius: 8, speed: 2, velocityX: 2, velocityY: 2 },
1191
+ // Уменьшена начальная скорость
1192
+ canvasWidth: 400,
1193
+ canvasHeight: 300
1194
+ });
1195
+ useEffect5(() => {
1196
+ const saved = localStorage.getItem("pongHighScore");
1197
+ if (saved) setHighScore(parseInt(saved));
1198
+ }, []);
1199
+ useEffect5(() => {
1200
+ const event = new CustomEvent("gameScoreUpdate", {
1201
+ detail: { game: "pong", score: playerScore }
1202
+ });
1203
+ window.dispatchEvent(event);
1204
+ }, [playerScore]);
1205
+ useEffect5(() => {
1206
+ if (gameActive) {
1207
+ roundStartTimeRef.current = Date.now();
1208
+ timeIntervalRef.current = setInterval(() => {
1209
+ const elapsed = Math.floor((Date.now() - roundStartTimeRef.current) / 1e3);
1210
+ setRoundTime(elapsed);
1211
+ const newSpeed = 1 + elapsed / 10 * 0.15;
1212
+ setRoundSpeed(Math.min(newSpeed, 1.8));
1213
+ }, 1e3);
1214
+ } else {
1215
+ if (timeIntervalRef.current) {
1216
+ clearInterval(timeIntervalRef.current);
1217
+ timeIntervalRef.current = null;
1218
+ }
1219
+ setRoundTime(0);
1220
+ setRoundSpeed(1);
1221
+ }
1222
+ return () => {
1223
+ if (timeIntervalRef.current) {
1224
+ clearInterval(timeIntervalRef.current);
1225
+ }
1226
+ };
1227
+ }, [gameActive]);
1228
+ const handleGameEnd = useCallback3((winner2) => {
1229
+ setWinner(winner2);
1230
+ setGameActive(false);
1231
+ if (winner2 === "player") {
1232
+ const newStreak = winStreak + 1;
1233
+ setWinStreak(newStreak);
1234
+ if (newStreak > highScore) {
1235
+ setHighScore(newStreak);
1236
+ localStorage.setItem("pongHighScore", newStreak.toString());
1237
+ }
1238
+ } else if (winner2 === "computer") {
1239
+ setWinStreak(0);
1240
+ }
1241
+ }, [winStreak, highScore]);
1242
+ const resetHighScore = useCallback3(() => {
1243
+ localStorage.removeItem("pongHighScore");
1244
+ setHighScore(0);
1245
+ setWinStreak(0);
1246
+ alert("\u0420\u0435\u043A\u043E\u0440\u0434 \u0441\u0431\u0440\u043E\u0448\u0435\u043D!");
1247
+ }, []);
1248
+ useEffect5(() => {
1249
+ return () => {
1250
+ if (animationRef.current) cancelAnimationFrame(animationRef.current);
1251
+ if (timeIntervalRef.current) clearInterval(timeIntervalRef.current);
1252
+ };
1253
+ }, []);
1254
+ const drawRect = useCallback3((ctx, x, y, w, h, color) => {
1255
+ ctx.fillStyle = color;
1256
+ ctx.fillRect(x, y, w, h);
1257
+ }, []);
1258
+ const drawCircle = useCallback3((ctx, x, y, r, color) => {
1259
+ ctx.fillStyle = color;
1260
+ ctx.beginPath();
1261
+ ctx.arc(x, y, r, 0, Math.PI * 2);
1262
+ ctx.closePath();
1263
+ ctx.fill();
1264
+ }, []);
1265
+ const drawText = useCallback3((ctx, text, x, y, color, size = "24px") => {
1266
+ ctx.fillStyle = color;
1267
+ ctx.font = `bold ${size} 'Courier New', monospace`;
1268
+ ctx.fillText(text, x, y);
1269
+ }, []);
1270
+ const drawNet = useCallback3((ctx, canvasWidth, canvasHeight) => {
1271
+ for (let i = 0; i <= canvasHeight; i += 15) {
1272
+ drawRect(ctx, canvasWidth / 2 - 1, i, 2, 8, "#667eea");
1273
+ }
1274
+ }, [drawRect]);
1275
+ const collision = useCallback3((ball, paddle) => {
1276
+ const b = {
1277
+ top: ball.y - ball.radius,
1278
+ bottom: ball.y + ball.radius,
1279
+ left: ball.x - ball.radius,
1280
+ right: ball.x + ball.radius
1281
+ };
1282
+ const p = {
1283
+ top: paddle.y,
1284
+ bottom: paddle.y + paddle.height,
1285
+ left: paddle.x,
1286
+ right: paddle.x + paddle.width
1287
+ };
1288
+ const isColliding = b.right > p.left && b.bottom > p.top && b.left < p.right && b.top < p.bottom;
1289
+ if (isColliding) {
1290
+ const randomAngle = (Math.random() - 0.5) * 0.3;
1291
+ return randomAngle;
1292
+ }
1293
+ return null;
1294
+ }, []);
1295
+ const resetBallForRound = useCallback3(() => {
1296
+ const state = gameStateRef.current;
1297
+ state.ball.x = state.canvasWidth / 2;
1298
+ state.ball.y = state.canvasHeight / 2;
1299
+ const direction = Math.random() > 0.5 ? 1 : -1;
1300
+ const angle = Math.random() * Math.PI / 3 - Math.PI / 6;
1301
+ state.ball.speed = 2;
1302
+ state.ball.velocityX = direction * state.ball.speed * Math.cos(angle);
1303
+ state.ball.velocityY = state.ball.speed * Math.sin(angle);
1304
+ }, []);
1305
+ const update = useCallback3(() => {
1306
+ const state = gameStateRef.current;
1307
+ const { ball, player, computer, canvasWidth, canvasHeight } = state;
1308
+ if (ball.x - ball.radius < 0) {
1309
+ computer.score++;
1310
+ setComputerScore(computer.score);
1311
+ resetBallForRound();
1312
+ setRoundSpeed(1);
1313
+ setRoundTime(0);
1314
+ roundStartTimeRef.current = Date.now();
1315
+ if (computer.score >= 5) {
1316
+ handleGameEnd("computer");
1317
+ }
1318
+ return;
1319
+ } else if (ball.x + ball.radius > canvasWidth) {
1320
+ player.score++;
1321
+ setPlayerScore(player.score);
1322
+ resetBallForRound();
1323
+ setRoundSpeed(1);
1324
+ setRoundTime(0);
1325
+ roundStartTimeRef.current = Date.now();
1326
+ if (player.score >= 5) {
1327
+ handleGameEnd("player");
1328
+ }
1329
+ return;
1330
+ }
1331
+ ball.x += ball.velocityX * roundSpeed;
1332
+ ball.y += ball.velocityY * roundSpeed;
1333
+ const computerCenter = computer.y + computer.height / 2;
1334
+ const predictionY = ball.y + ball.velocityY * 5;
1335
+ const targetY = predictionY - computer.height / 2;
1336
+ const diff = targetY - computer.y;
1337
+ let moveAmount = diff * 0.07 * Math.min(roundSpeed, 1.3);
1338
+ const maxComputerSpeed = 1.5;
1339
+ if (Math.abs(moveAmount) > maxComputerSpeed) {
1340
+ moveAmount = Math.sign(moveAmount) * maxComputerSpeed;
1341
+ }
1342
+ computer.y += moveAmount;
1343
+ computer.y = Math.max(0, Math.min(canvasHeight - computer.height, computer.y));
1344
+ player.y = Math.max(0, Math.min(canvasHeight - player.height, player.y));
1345
+ if (ball.y - ball.radius < 0) {
1346
+ ball.y = ball.radius + 1;
1347
+ ball.velocityY = Math.abs(ball.velocityY) * 0.9;
1348
+ ball.velocityY += Math.random() * 0.15;
1349
+ } else if (ball.y + ball.radius > canvasHeight) {
1350
+ ball.y = canvasHeight - ball.radius - 1;
1351
+ ball.velocityY = -Math.abs(ball.velocityY) * 0.9;
1352
+ ball.velocityY -= Math.random() * 0.15;
1353
+ }
1354
+ const minVerticalSpeed = 0.4;
1355
+ if (Math.abs(ball.velocityY) < minVerticalSpeed) {
1356
+ const sign = ball.velocityY >= 0 ? 1 : -1;
1357
+ ball.velocityY = sign * minVerticalSpeed;
1358
+ }
1359
+ const isHorizontalStuck = Math.abs(ball.velocityY) < 0.2 && Math.abs(ball.velocityX) > 1;
1360
+ if (isHorizontalStuck) {
1361
+ ball.velocityY += (Math.random() - 0.5) * 0.5;
1362
+ }
1363
+ const playerCollision = collision(ball, player);
1364
+ const computerCollision = collision(ball, computer);
1365
+ let collisionAngle = null;
1366
+ let paddle = null;
1367
+ if (playerCollision !== null && ball.velocityX < 0) {
1368
+ collisionAngle = playerCollision;
1369
+ paddle = player;
1370
+ } else if (computerCollision !== null && ball.velocityX > 0) {
1371
+ collisionAngle = computerCollision;
1372
+ paddle = computer;
1373
+ }
1374
+ if (collisionAngle !== null && paddle) {
1375
+ let collidePoint = (ball.y - (paddle.y + paddle.height / 2)) / (paddle.height / 2);
1376
+ collidePoint = Math.max(-0.8, Math.min(0.8, collidePoint));
1377
+ let angleRad = Math.PI / 4 * collidePoint;
1378
+ angleRad += collisionAngle;
1379
+ let direction = ball.x < canvasWidth / 2 ? 1 : -1;
1380
+ const currentSpeed = Math.sqrt(ball.velocityX * ball.velocityX + ball.velocityY * ball.velocityY);
1381
+ const newSpeed = Math.min(currentSpeed * 1.03, 5);
1382
+ ball.speed = newSpeed;
1383
+ ball.velocityX = direction * ball.speed * Math.cos(angleRad);
1384
+ ball.velocityY = ball.speed * Math.sin(angleRad);
1385
+ if (direction > 0) {
1386
+ ball.x = paddle.x + paddle.width + ball.radius + 1;
1387
+ } else {
1388
+ ball.x = paddle.x - ball.radius - 1;
1389
+ }
1390
+ }
1391
+ }, [roundSpeed, collision, resetBallForRound, handleGameEnd]);
1392
+ const render = useCallback3(() => {
1393
+ const canvas = canvasRef.current;
1394
+ if (!canvas) return;
1395
+ const ctx = canvas.getContext("2d");
1396
+ const state = gameStateRef.current;
1397
+ const { canvasWidth, canvasHeight, player, computer, ball } = state;
1398
+ ctx.clearRect(0, 0, canvasWidth, canvasHeight);
1399
+ drawRect(ctx, 0, 0, canvasWidth, canvasHeight, "#1a1a2e");
1400
+ drawNet(ctx, canvasWidth, canvasHeight);
1401
+ drawText(ctx, player.score.toString(), canvasWidth / 4, 40, "#667eea", "32px");
1402
+ drawText(ctx, computer.score.toString(), 3 * canvasWidth / 4, 40, "#ff5e62", "32px");
1403
+ drawText(ctx, `${formatTime(roundTime)}`, canvasWidth / 2 - 40, 25, "#ff9966", "14px");
1404
+ if (roundSpeed > 1.1) {
1405
+ drawText(ctx, `x${roundSpeed.toFixed(1)}`, canvasWidth / 2 + 20, 25, "#00b09b", "14px");
1406
+ }
1407
+ drawRect(ctx, player.x, player.y, player.width, player.height, "#667eea");
1408
+ drawRect(ctx, computer.x, computer.y, computer.width, computer.height, "#ff5e62");
1409
+ drawCircle(ctx, ball.x, ball.y, ball.radius, "#00b09b");
1410
+ ctx.strokeStyle = "rgba(255,255,255,0.1)";
1411
+ ctx.setLineDash([5, 10]);
1412
+ ctx.beginPath();
1413
+ ctx.moveTo(canvasWidth / 2, 0);
1414
+ ctx.lineTo(canvasWidth / 2, canvasHeight);
1415
+ ctx.stroke();
1416
+ ctx.setLineDash([]);
1417
+ }, [drawRect, drawCircle, drawText, drawNet, roundTime, roundSpeed]);
1418
+ const formatTime = useCallback3((seconds) => {
1419
+ const mins = Math.floor(seconds / 60);
1420
+ const secs = seconds % 60;
1421
+ return `${mins}:${secs.toString().padStart(2, "0")}`;
1422
+ }, []);
1423
+ const gameLoop = useCallback3(() => {
1424
+ if (!gameActive) return;
1425
+ update();
1426
+ render();
1427
+ animationRef.current = requestAnimationFrame(gameLoop);
1428
+ }, [gameActive, update, render]);
1429
+ useEffect5(() => {
1430
+ if (gameActive) {
1431
+ animationRef.current = requestAnimationFrame(gameLoop);
1432
+ } else {
1433
+ if (animationRef.current) {
1434
+ cancelAnimationFrame(animationRef.current);
1435
+ }
1436
+ }
1437
+ return () => {
1438
+ if (animationRef.current) {
1439
+ cancelAnimationFrame(animationRef.current);
1440
+ }
1441
+ };
1442
+ }, [gameActive, gameLoop]);
1443
+ const handleMouseMove = useCallback3((e) => {
1444
+ if (!gameActive || !canvasRef.current) return;
1445
+ const canvas = canvasRef.current;
1446
+ const rect = canvas.getBoundingClientRect();
1447
+ const state = gameStateRef.current;
1448
+ const y = e.clientY - rect.top - state.player.height / 2;
1449
+ state.player.y = Math.max(0, Math.min(state.canvasHeight - state.player.height, y));
1450
+ }, [gameActive]);
1451
+ const handleTouchMove = useCallback3((e) => {
1452
+ if (!gameActive || !canvasRef.current) return;
1453
+ const touch = e.touches[0];
1454
+ handleMouseMove(touch);
1455
+ }, [gameActive, handleMouseMove]);
1456
+ const startGame = useCallback3(() => {
1457
+ setGameActive(true);
1458
+ setPlayerScore(0);
1459
+ setComputerScore(0);
1460
+ setWinner(null);
1461
+ setRoundSpeed(1);
1462
+ setRoundTime(0);
1463
+ gameStateRef.current = {
1464
+ player: { x: 0, y: 180, width: 10, height: 60, score: 0 },
1465
+ computer: { x: 390, y: 180, width: 10, height: 60, score: 0 },
1466
+ ball: { x: 200, y: 200, radius: 8, speed: 2, velocityX: 2, velocityY: 0 },
1467
+ // Уменьшена начальная скорость
1468
+ canvasWidth: 400,
1469
+ canvasHeight: 300
1470
+ };
1471
+ resetBallForRound();
1472
+ }, [resetBallForRound]);
1473
+ const resetGame = useCallback3(() => {
1474
+ setGameActive(false);
1475
+ setPlayerScore(0);
1476
+ setComputerScore(0);
1477
+ setWinner(null);
1478
+ setWinStreak(0);
1479
+ setRoundSpeed(1);
1480
+ setRoundTime(0);
1481
+ if (animationRef.current) {
1482
+ cancelAnimationFrame(animationRef.current);
1483
+ }
1484
+ }, []);
1485
+ useEffect5(() => {
1486
+ const canvas = canvasRef.current;
1487
+ if (!canvas) return;
1488
+ canvas.addEventListener("mousemove", handleMouseMove);
1489
+ canvas.addEventListener("touchmove", handleTouchMove, { passive: false });
1490
+ return () => {
1491
+ canvas.removeEventListener("mousemove", handleMouseMove);
1492
+ canvas.removeEventListener("touchmove", handleTouchMove);
1493
+ };
1494
+ }, [handleMouseMove, handleTouchMove]);
1495
+ return /* @__PURE__ */ jsxs5("div", { className: "pong-game", children: [
1496
+ /* @__PURE__ */ jsx5("div", { className: "game-header", children: /* @__PURE__ */ jsxs5("div", { className: "game-controls", children: [
1497
+ /* @__PURE__ */ jsx5(
1498
+ "button",
1499
+ {
1500
+ className: "control-btn",
1501
+ onClick: gameActive ? resetGame : startGame,
1502
+ children: gameActive ? "\u{1F504} \u041D\u0430\u0447\u0430\u0442\u044C \u0437\u0430\u043D\u043E\u0432\u043E" : "\u25B6\uFE0F \u041D\u0430\u0447\u0430\u0442\u044C \u0438\u0433\u0440\u0443"
1503
+ }
1504
+ ),
1505
+ /* @__PURE__ */ jsx5(
1506
+ "button",
1507
+ {
1508
+ className: "control-btn reset-btn",
1509
+ onClick: resetHighScore,
1510
+ disabled: gameActive,
1511
+ children: "\u{1F5D1}\uFE0F \u0421\u0431\u0440\u043E\u0441\u0438\u0442\u044C \u0440\u0435\u043A\u043E\u0440\u0434"
1512
+ }
1513
+ )
1514
+ ] }) }),
1515
+ /* @__PURE__ */ jsxs5("div", { className: "game-stats", children: [
1516
+ /* @__PURE__ */ jsxs5("div", { className: "stat-box", children: [
1517
+ /* @__PURE__ */ jsx5("div", { className: "stat-label", children: "\u0418\u0433\u0440\u043E\u043A" }),
1518
+ /* @__PURE__ */ jsx5("div", { className: "stat-value player-score", children: playerScore })
1519
+ ] }),
1520
+ /* @__PURE__ */ jsxs5("div", { className: "stat-box", children: [
1521
+ /* @__PURE__ */ jsx5("div", { className: "stat-label", children: "\u041A\u043E\u043C\u043F\u044C\u044E\u0442\u0435\u0440" }),
1522
+ /* @__PURE__ */ jsx5("div", { className: "stat-value computer-score", children: computerScore })
1523
+ ] }),
1524
+ /* @__PURE__ */ jsxs5("div", { className: "stat-box", children: [
1525
+ /* @__PURE__ */ jsx5("div", { className: "stat-label", children: "\u0420\u0435\u043A\u043E\u0440\u0434" }),
1526
+ /* @__PURE__ */ jsxs5("div", { className: "stat-value", children: [
1527
+ highScore,
1528
+ " \u{1F525}"
1529
+ ] })
1530
+ ] }),
1531
+ /* @__PURE__ */ jsxs5("div", { className: "stat-box", children: [
1532
+ /* @__PURE__ */ jsx5("div", { className: "stat-label", children: "\u0420\u0430\u0443\u043D\u0434" }),
1533
+ /* @__PURE__ */ jsx5("div", { className: "stat-value time-value", children: formatTime(roundTime) })
1534
+ ] })
1535
+ ] }),
1536
+ /* @__PURE__ */ jsxs5("div", { className: "game-area", children: [
1537
+ /* @__PURE__ */ jsxs5("div", { className: "pong-container", children: [
1538
+ /* @__PURE__ */ jsx5(
1539
+ "canvas",
1540
+ {
1541
+ ref: canvasRef,
1542
+ width: 400,
1543
+ height: 300,
1544
+ className: "pong-canvas"
1545
+ }
1546
+ ),
1547
+ !gameActive && winner && /* @__PURE__ */ jsx5("div", { className: "game-over-screen", children: /* @__PURE__ */ jsxs5("div", { className: "game-over-message", children: [
1548
+ /* @__PURE__ */ jsx5("h3", { children: winner === "player" ? "\u{1F3C6} \u041F\u043E\u0431\u0435\u0434\u0430!" : "\u{1F480} \u041F\u043E\u0440\u0430\u0436\u0435\u043D\u0438\u0435" }),
1549
+ /* @__PURE__ */ jsxs5("div", { className: "final-score", children: [
1550
+ /* @__PURE__ */ jsxs5("p", { children: [
1551
+ "\u0421\u0447\u0435\u0442: ",
1552
+ /* @__PURE__ */ jsxs5("strong", { children: [
1553
+ playerScore,
1554
+ ":",
1555
+ computerScore
1556
+ ] })
1557
+ ] }),
1558
+ /* @__PURE__ */ jsxs5("p", { children: [
1559
+ "\u0421\u0435\u0440\u0438\u044F \u043F\u043E\u0431\u0435\u0434: ",
1560
+ /* @__PURE__ */ jsx5("strong", { children: winStreak })
1561
+ ] }),
1562
+ winner === "player" && winStreak > 1 && /* @__PURE__ */ jsxs5("p", { className: "streak-message", children: [
1563
+ "\u{1F525} \u0412\u044B\u0438\u0433\u0440\u0430\u043D\u043E ",
1564
+ winStreak,
1565
+ " \u043F\u0430\u0440\u0442\u0438\u0439 \u043F\u043E\u0434\u0440\u044F\u0434!"
1566
+ ] })
1567
+ ] }),
1568
+ /* @__PURE__ */ jsx5("button", { className: "play-again-btn", onClick: startGame, children: "\u{1F3AE} \u041D\u043E\u0432\u0430\u044F \u043F\u0430\u0440\u0442\u0438\u044F" })
1569
+ ] }) }),
1570
+ !gameActive && !winner && /* @__PURE__ */ jsx5("div", { className: "start-screen", children: /* @__PURE__ */ jsxs5("div", { className: "start-message", children: [
1571
+ /* @__PURE__ */ jsx5("h3", { children: "\u{1F3D3} \u041F\u0438\u043D\u0433-\u041F\u043E\u043D\u0433" }),
1572
+ /* @__PURE__ */ jsx5("p", { children: "\u0414\u0432\u0438\u0433\u0430\u0439\u0442\u0435 \u043C\u044B\u0448\u044C\u044E \u0434\u043B\u044F \u0443\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0438\u044F \u0440\u0430\u043A\u0435\u0442\u043A\u043E\u0439" }),
1573
+ /* @__PURE__ */ jsx5("p", { children: "\u041F\u0430\u0440\u0442\u0438\u044F: \u043F\u0435\u0440\u0432\u044B\u0439 \u0434\u043E 5 \u043E\u0447\u043A\u043E\u0432" }),
1574
+ highScore > 0 && /* @__PURE__ */ jsxs5("p", { className: "record-info", children: [
1575
+ "\u{1F3C6} \u0420\u0435\u043A\u043E\u0440\u0434: ",
1576
+ highScore,
1577
+ " \u043F\u043E\u0431\u0435\u0434 \u043F\u043E\u0434\u0440\u044F\u0434"
1578
+ ] }),
1579
+ /* @__PURE__ */ jsx5("button", { className: "start-btn-big", onClick: startGame, children: "\u041D\u0410\u0427\u0410\u0422\u042C \u041F\u0410\u0420\u0422\u0418\u042E" })
1580
+ ] }) })
1581
+ ] }),
1582
+ /* @__PURE__ */ jsxs5("div", { className: "speed-info", children: [
1583
+ /* @__PURE__ */ jsxs5("div", { className: "speed-display", children: [
1584
+ /* @__PURE__ */ jsx5("span", { className: "speed-label", children: "\u0421\u043A\u043E\u0440\u043E\u0441\u0442\u044C \u0440\u0430\u0443\u043D\u0434\u0430: " }),
1585
+ /* @__PURE__ */ jsxs5("span", { className: "speed-value", children: [
1586
+ "x",
1587
+ roundSpeed.toFixed(1)
1588
+ ] })
1589
+ ] }),
1590
+ /* @__PURE__ */ jsx5("div", { className: "speed-hint", children: "\u26A1 \u0421\u043A\u043E\u0440\u043E\u0441\u0442\u044C \u043C\u0435\u0434\u043B\u0435\u043D\u043D\u043E \u0440\u0430\u0441\u0442\u0435\u0442 \u0438 \u0441\u0431\u0440\u0430\u0441\u044B\u0432\u0430\u0435\u0442\u0441\u044F \u043F\u0440\u0438 \u0433\u043E\u043B\u0435" })
1591
+ ] })
1592
+ ] }),
1593
+ /* @__PURE__ */ jsxs5("div", { className: "game-instructions", children: [
1594
+ /* @__PURE__ */ jsxs5("div", { className: "instructions-row", children: [
1595
+ /* @__PURE__ */ jsxs5("p", { children: [
1596
+ /* @__PURE__ */ jsx5("strong", { children: "\u0423\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0438\u0435:" }),
1597
+ " \u0414\u0432\u0438\u0433\u0430\u0439\u0442\u0435 \u043C\u044B\u0448\u044C\u044E \u0432\u0432\u0435\u0440\u0445/\u0432\u043D\u0438\u0437"
1598
+ ] }),
1599
+ /* @__PURE__ */ jsxs5("p", { children: [
1600
+ /* @__PURE__ */ jsx5("strong", { children: "\u0426\u0435\u043B\u044C:" }),
1601
+ " \u0412\u044B\u0438\u0433\u0440\u0430\u0439\u0442\u0435 5 \u0440\u0430\u0443\u043D\u0434\u043E\u0432"
1602
+ ] })
1603
+ ] }),
1604
+ /* @__PURE__ */ jsxs5("div", { className: "legend", children: [
1605
+ /* @__PURE__ */ jsxs5("div", { className: "legend-item", children: [
1606
+ /* @__PURE__ */ jsx5("div", { className: "legend-color player-color" }),
1607
+ /* @__PURE__ */ jsx5("span", { children: "\u0418\u0433\u0440\u043E\u043A (\u0441\u0438\u043D\u0438\u0439)" })
1608
+ ] }),
1609
+ /* @__PURE__ */ jsxs5("div", { className: "legend-item", children: [
1610
+ /* @__PURE__ */ jsx5("div", { className: "legend-color computer-color" }),
1611
+ /* @__PURE__ */ jsx5("span", { children: "\u041A\u043E\u043C\u043F\u044C\u044E\u0442\u0435\u0440 (\u043A\u0440\u0430\u0441\u043D\u044B\u0439)" })
1612
+ ] }),
1613
+ /* @__PURE__ */ jsxs5("div", { className: "legend-item", children: [
1614
+ /* @__PURE__ */ jsx5("div", { className: "legend-color ball-color" }),
1615
+ /* @__PURE__ */ jsx5("span", { children: "\u041C\u044F\u0447" })
1616
+ ] })
1617
+ ] })
1618
+ ] })
1619
+ ] });
1620
+ };
1621
+ var Pong_default = Pong;
1622
+
1623
+ // src/games/Platformer/PlatformerGame.js
1624
+ import React6, { useState as useState6, useEffect as useEffect6, useRef as useRef2, useCallback as useCallback4 } from "react";
1625
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1626
+ var PlatformerGame = () => {
1627
+ const [score, setScore] = useState6(0);
1628
+ const [gameActive, setGameActive] = useState6(false);
1629
+ const [level, setLevel] = useState6(0);
1630
+ const [lives, setLives] = useState6(3);
1631
+ const [gameStatus, setGameStatus] = useState6("ready");
1632
+ const [coinsCollected, setCoinsCollected] = useState6(0);
1633
+ const [totalCoins, setTotalCoins] = useState6(0);
1634
+ const gameContainerRef = useRef2(null);
1635
+ const canvasRef = useRef2(null);
1636
+ const animationRef = useRef2(null);
1637
+ const gameStateRef = useRef2(null);
1638
+ const keysRef = useRef2({ left: false, right: false, up: false });
1639
+ const deathAnimationRef = useRef2({ active: false, time: 0, scale: 1 });
1640
+ const LEVELS = [
1641
+ [
1642
+ " ",
1643
+ " ",
1644
+ " ",
1645
+ " ",
1646
+ " ",
1647
+ " ",
1648
+ " xxx ",
1649
+ " xx xx xx!xx ",
1650
+ " o o xx x!!!x ",
1651
+ " xx!xx ",
1652
+ " xxxxx xvx ",
1653
+ " xx ",
1654
+ " xx o o x ",
1655
+ " x o x ",
1656
+ " x xxxxx o x ",
1657
+ " x xxxx o x ",
1658
+ " x @ x x xxxxx x ",
1659
+ " xxxxxxxxxxxx xxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxx xxxxxxx xxxxxxxxx ",
1660
+ " x x x x ",
1661
+ " x!!!x x!!!!!x ",
1662
+ " x!!!x x!!!!!x ",
1663
+ " xxxxx xxxxxxx ",
1664
+ " ",
1665
+ " "
1666
+ ],
1667
+ [
1668
+ " x!!x xxxxxxx x!x ",
1669
+ " x!!x xxxx xxxx x!x ",
1670
+ " x!!xxxxxxxxxx xx xx x!x ",
1671
+ " xx!!!!!!!!!!xx xx xx x!x ",
1672
+ " xxxxxxxxxx!!x x o o o x!x ",
1673
+ " xx!x x o o xx!x ",
1674
+ " x!x x xxxxxxxxxxxxxxx!!x ",
1675
+ " xvx x x x !!!!!!!!!!!!!!xx ",
1676
+ " xx | | | xx xxxxxxxxxxxxxxxxxxxxx ",
1677
+ " xx!!!!!!!!!!!xx v ",
1678
+ " xxxx!!!!!xxxx ",
1679
+ " x x xxxxxxx xxx xxx ",
1680
+ " x x x x x x ",
1681
+ " x x x x ",
1682
+ " x x xx x ",
1683
+ " xx x x x ",
1684
+ " x x o o x x x x ",
1685
+ " xxxxxxx xxx xxx x x x x x x ",
1686
+ " xx xx x x x x xxxxxx x x xxxxxxxxx x ",
1687
+ " xx xx x o x x xx x x x x ",
1688
+ " @ x x x x x x x x x x ",
1689
+ " xxx x x x x x x x xxxxx xxxxxx x ",
1690
+ " x x x x xx o xx x x x o x x x ",
1691
+ "!!!!x x!!!!!!x x!!!!!!xx xx!!!!!!!!xx x!!!!!!!!!! x = x x x ",
1692
+ "!!!!x x!!!!!!x x!!!!!xx xxxxxxxxxx x!!!!!!!xx! xxxxxxxxxxxxx xx o o xx ",
1693
+ "!!!!x x!!!!!!x x!!!!!x o xx!!!!!!xx ! xx xx ",
1694
+ "!!!!x x!!!!!!x x!!!!!x xx!!!!!!xx ! xxxxxxx ",
1695
+ "!!!!x x!!!!!!x x!!!!!xx xxxxxxxxxxxxxx!!!!!!xx ! ",
1696
+ "!!!!x x!!!!!!x x!!!!!!xxxxxxxxx!!!!!!!!!!!!!!!!!!xx ! ",
1697
+ "!!!!x x!!!!!!x x!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!xx ! "
1698
+ ],
1699
+ // Третий уровень (прототип)
1700
+ [
1701
+ " ",
1702
+ " ",
1703
+ " ",
1704
+ " ",
1705
+ " ",
1706
+ " o ",
1707
+ " ",
1708
+ " x ",
1709
+ " x ",
1710
+ " x ",
1711
+ " x ",
1712
+ " xxx ",
1713
+ " x x !!! !!! xxx ",
1714
+ " x x !x! !x! ",
1715
+ " xxx xxx x x ",
1716
+ " x x x oooo x xxx ",
1717
+ " x x x x x!!!x ",
1718
+ " x x xxxxxxxxxxxx xxx ",
1719
+ " xx xx x x x ",
1720
+ " x xxxxxxxxx xxxxxxxx x x ",
1721
+ " x x x x!!!x ",
1722
+ " x x x xxx ",
1723
+ " xx xx x ",
1724
+ " x x= = = = x xxx ",
1725
+ " x x x x!!!x ",
1726
+ " x x = = = =x o xxx xxx ",
1727
+ " xx xx x x!!!x ",
1728
+ " o o x x x x xxv xxx ",
1729
+ " x x x x x!!!x ",
1730
+ " xxx xxx xxx xxx o o x!!!!!!!!!!!!!!x vx ",
1731
+ " x xxx x x xxx x x!!!!!!!!!!!!!!x ",
1732
+ " x x xxxxxxxxxxxxxxxxxxxxxxx ",
1733
+ " xx xx xxx ",
1734
+ " xxx x x x x!!!x xxx ",
1735
+ " x x x xxx x xxx x x ",
1736
+ " x x xxx xxxxxxx xxxxx x ",
1737
+ " x x x x x x ",
1738
+ " x xx x x x x x ",
1739
+ " x x |xxxx| |xxxx| xxx xxx x ",
1740
+ " x xxx o o x x xxx x ",
1741
+ " x xxxxx xx x xxx x!!!x x x ",
1742
+ " x oxxxo x xxx x x x xxx xxx x ",
1743
+ " x xxx xxxxxxxxxxxxx x oo x x oo x x oo xx xx xxx x ",
1744
+ " x @ x x x!!x x!!!!x x!!!!x xx xx x x ",
1745
+ " xxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ",
1746
+ " ",
1747
+ " "
1748
+ ]
1749
+ ];
1750
+ class Vector {
1751
+ constructor(x, y) {
1752
+ this.x = x;
1753
+ this.y = y;
1754
+ }
1755
+ plus(other) {
1756
+ return new Vector(this.x + other.x, this.y + other.y);
1757
+ }
1758
+ times(scale) {
1759
+ return new Vector(this.x * scale, this.y * scale);
1760
+ }
1761
+ }
1762
+ class Player {
1763
+ constructor(pos) {
1764
+ this.pos = pos.plus(new Vector(0, -0.5));
1765
+ this.size = new Vector(0.5, 1);
1766
+ this.speed = new Vector(0, 0);
1767
+ this.type = "player";
1768
+ this.alive = true;
1769
+ this.canJump = false;
1770
+ this.jumpCooldown = 0;
1771
+ }
1772
+ moveX(step, level2, keys) {
1773
+ const playerXSpeed = 10;
1774
+ this.speed.x = 0;
1775
+ if (keys.left) this.speed.x -= playerXSpeed;
1776
+ if (keys.right) this.speed.x += playerXSpeed;
1777
+ const motion = new Vector(this.speed.x * step, 0);
1778
+ const newPos = this.pos.plus(motion);
1779
+ const obstacle = level2.obstacleAt(newPos, this.size);
1780
+ if (!obstacle || obstacle === "lava") {
1781
+ this.pos = newPos;
1782
+ }
1783
+ }
1784
+ moveY(step, level2, keys) {
1785
+ const gravity = 30;
1786
+ const jumpSpeed = 17;
1787
+ this.speed.y += step * gravity;
1788
+ const motion = new Vector(0, this.speed.y * step);
1789
+ const newPos = this.pos.plus(motion);
1790
+ const obstacle = level2.obstacleAt(newPos, this.size);
1791
+ if (obstacle) {
1792
+ if (obstacle === "lava") {
1793
+ this.alive = false;
1794
+ level2.status = "lost";
1795
+ this.speed.y = 0;
1796
+ return;
1797
+ }
1798
+ if (keys.up && this.canJump && this.jumpCooldown <= 0) {
1799
+ this.speed.y = -jumpSpeed;
1800
+ this.canJump = false;
1801
+ this.jumpCooldown = 0.2;
1802
+ } else {
1803
+ this.speed.y = 0;
1804
+ this.canJump = true;
1805
+ }
1806
+ } else {
1807
+ this.pos = newPos;
1808
+ this.canJump = false;
1809
+ }
1810
+ if (this.jumpCooldown > 0) {
1811
+ this.jumpCooldown -= step;
1812
+ }
1813
+ }
1814
+ act(step, level2, keys) {
1815
+ if (!this.alive) return;
1816
+ this.moveX(step, level2, keys);
1817
+ this.moveY(step, level2, keys);
1818
+ if (this.pos.y > level2.height) {
1819
+ this.alive = false;
1820
+ level2.status = "lost";
1821
+ }
1822
+ }
1823
+ }
1824
+ class Coin {
1825
+ constructor(pos) {
1826
+ this.basePos = this.pos = pos;
1827
+ this.size = new Vector(0.6, 0.6);
1828
+ this.wobble = Math.random() * Math.PI * 2;
1829
+ this.type = "coin";
1830
+ this.collected = false;
1831
+ }
1832
+ act(step) {
1833
+ if (this.collected) return;
1834
+ const wobbleSpeed = 8;
1835
+ const wobbleDist = 0.07;
1836
+ this.wobble += step * wobbleSpeed;
1837
+ const wobblePos = Math.sin(this.wobble) * wobbleDist;
1838
+ this.pos = this.basePos.plus(new Vector(0, wobblePos));
1839
+ }
1840
+ }
1841
+ class Lava {
1842
+ constructor(pos, ch) {
1843
+ this.pos = pos;
1844
+ this.size = new Vector(1, 1);
1845
+ this.type = "lava";
1846
+ this.char = ch;
1847
+ if (ch === "=") {
1848
+ this.speed = new Vector(2, 0);
1849
+ } else if (ch === "|") {
1850
+ this.speed = new Vector(0, 2);
1851
+ } else if (ch === "v") {
1852
+ this.speed = new Vector(0, 3);
1853
+ this.repeatPos = pos;
1854
+ } else if (ch === "!") {
1855
+ this.speed = new Vector(0, 0);
1856
+ }
1857
+ }
1858
+ act(step, level2) {
1859
+ if (this.char === "=" || this.char === "|" || this.char === "v") {
1860
+ const newPos = this.pos.plus(this.speed.times(step));
1861
+ if (!level2.obstacleAt(newPos, this.size)) {
1862
+ this.pos = newPos;
1863
+ } else if (this.repeatPos) {
1864
+ this.pos = this.repeatPos;
1865
+ } else {
1866
+ this.speed = this.speed.times(-1);
1867
+ }
1868
+ }
1869
+ }
1870
+ }
1871
+ class Level {
1872
+ constructor(plan) {
1873
+ this.width = plan[0].length;
1874
+ this.height = plan.length;
1875
+ this.grid = [];
1876
+ this.actors = [];
1877
+ this.coins = 0;
1878
+ this.coinsCollected = 0;
1879
+ const actorChars = {
1880
+ "@": Player,
1881
+ "o": Coin,
1882
+ "=": Lava,
1883
+ "|": Lava,
1884
+ "v": Lava,
1885
+ "!": Lava
1886
+ };
1887
+ for (let y = 0; y < this.height; y++) {
1888
+ const line = plan[y];
1889
+ const gridLine = [];
1890
+ for (let x = 0; x < this.width; x++) {
1891
+ const ch = line[x];
1892
+ const ActorClass = actorChars[ch];
1893
+ let fieldType = null;
1894
+ if (ActorClass) {
1895
+ const actor = new ActorClass(new Vector(x, y), ch);
1896
+ this.actors.push(actor);
1897
+ if (actor.type === "coin") this.coins++;
1898
+ } else if (ch === "x") {
1899
+ fieldType = "wall";
1900
+ } else if (ch === "!" || ch === "|" || ch === "=" || ch === "v") {
1901
+ fieldType = "lava";
1902
+ }
1903
+ gridLine.push(fieldType);
1904
+ }
1905
+ this.grid.push(gridLine);
1906
+ }
1907
+ this.player = this.actors.find((actor) => actor.type === "player");
1908
+ this.status = null;
1909
+ this.coinsTotal = this.coins;
1910
+ this.coinsCollected = 0;
1911
+ }
1912
+ obstacleAt(pos, size) {
1913
+ const xStart = Math.floor(pos.x);
1914
+ const xEnd = Math.ceil(pos.x + size.x);
1915
+ const yStart = Math.floor(pos.y);
1916
+ const yEnd = Math.ceil(pos.y + size.y);
1917
+ if (xStart < 0 || xEnd > this.width || yStart < 0) return "wall";
1918
+ if (yEnd > this.height) return "lava";
1919
+ for (let y = yStart; y < yEnd; y++) {
1920
+ for (let x = xStart; x < xEnd; x++) {
1921
+ const fieldType = this.grid[y][x];
1922
+ if (fieldType) return fieldType;
1923
+ }
1924
+ }
1925
+ return null;
1926
+ }
1927
+ actorAt(actor) {
1928
+ for (const other of this.actors) {
1929
+ if (other !== actor && other.type !== "lava" && actor.pos.x + actor.size.x > other.pos.x && actor.pos.x < other.pos.x + other.size.x && actor.pos.y + actor.size.y > other.pos.y && actor.pos.y < other.pos.y + other.size.y) {
1930
+ return other;
1931
+ }
1932
+ }
1933
+ return null;
1934
+ }
1935
+ checkCollisions() {
1936
+ if (!this.player || !this.player.alive) return;
1937
+ const playerGridX = Math.floor(this.player.pos.x);
1938
+ const playerGridY = Math.floor(this.player.pos.y);
1939
+ const playerEndX = Math.ceil(this.player.pos.x + this.player.size.x);
1940
+ const playerEndY = Math.ceil(this.player.pos.y + this.player.size.y);
1941
+ for (let y = playerGridY; y < playerEndY; y++) {
1942
+ if (y < 0 || y >= this.height) continue;
1943
+ for (let x = playerGridX; x < playerEndX; x++) {
1944
+ if (x < 0 || x >= this.width) continue;
1945
+ if (this.grid[y][x] === "lava") {
1946
+ this.player.alive = false;
1947
+ this.status = "lost";
1948
+ deathAnimationRef.current = { active: true, time: 0, scale: 1 };
1949
+ return;
1950
+ }
1951
+ }
1952
+ }
1953
+ for (const actor of this.actors) {
1954
+ if (actor.type === "lava" && this.player.pos.x + this.player.size.x > actor.pos.x && this.player.pos.x < actor.pos.x + actor.size.x && this.player.pos.y + this.player.size.y > actor.pos.y && this.player.pos.y < actor.pos.y + actor.size.y) {
1955
+ this.player.alive = false;
1956
+ this.status = "lost";
1957
+ deathAnimationRef.current = { active: true, time: 0, scale: 1 };
1958
+ return;
1959
+ }
1960
+ }
1961
+ for (const actor of this.actors) {
1962
+ if (actor.type === "coin" && !actor.collected && this.player.pos.x + this.player.size.x > actor.pos.x && this.player.pos.x < actor.pos.x + actor.size.x && this.player.pos.y + this.player.size.y > actor.pos.y && this.player.pos.y < actor.pos.y + actor.size.y) {
1963
+ actor.collected = true;
1964
+ this.coinsCollected++;
1965
+ setScore((prev) => prev + 100);
1966
+ setCoinsCollected(this.coinsCollected);
1967
+ if (this.coinsCollected >= this.coinsTotal) {
1968
+ this.status = "won";
1969
+ }
1970
+ break;
1971
+ }
1972
+ }
1973
+ }
1974
+ animate(step, keys) {
1975
+ const maxStep = 0.05;
1976
+ while (step > 0) {
1977
+ const thisStep = Math.min(step, maxStep);
1978
+ this.actors.forEach((actor) => {
1979
+ if (actor.type === "player") {
1980
+ actor.act(thisStep, this, keys);
1981
+ } else if (actor.type === "lava") {
1982
+ actor.act(thisStep, this);
1983
+ } else if (actor.type === "coin" && !actor.collected) {
1984
+ actor.act(thisStep);
1985
+ }
1986
+ });
1987
+ this.checkCollisions();
1988
+ if (deathAnimationRef.current.active) {
1989
+ deathAnimationRef.current.time += thisStep;
1990
+ deathAnimationRef.current.scale = Math.max(0, 1 - deathAnimationRef.current.time * 2);
1991
+ if (deathAnimationRef.current.time > 0.5) {
1992
+ deathAnimationRef.current.active = false;
1993
+ }
1994
+ }
1995
+ step -= thisStep;
1996
+ }
1997
+ }
1998
+ isFinished() {
1999
+ return this.status === "won" || this.status === "lost";
2000
+ }
2001
+ }
2002
+ const initGame = useCallback4(() => {
2003
+ if (!canvasRef.current) return;
2004
+ const currentLevel = new Level(LEVELS[level]);
2005
+ setTotalCoins(currentLevel.coinsTotal);
2006
+ setCoinsCollected(0);
2007
+ gameStateRef.current = {
2008
+ level: currentLevel,
2009
+ lastTime: null,
2010
+ gameRunning: true,
2011
+ cameraX: 0,
2012
+ cameraY: 0
2013
+ };
2014
+ deathAnimationRef.current = { active: false, time: 0, scale: 1 };
2015
+ setGameStatus("playing");
2016
+ setGameActive(true);
2017
+ }, [level]);
2018
+ const updateCamera = useCallback4((playerPos, canvasWidth, canvasHeight, levelWidth, levelHeight) => {
2019
+ if (!gameStateRef.current || !playerPos) return;
2020
+ const scale = 12;
2021
+ const viewportWidth = canvasWidth / scale;
2022
+ const viewportHeight = canvasHeight / scale;
2023
+ gameStateRef.current.cameraX = Math.max(0, Math.min(
2024
+ playerPos.x - viewportWidth / 2,
2025
+ levelWidth - viewportWidth
2026
+ ));
2027
+ gameStateRef.current.cameraY = Math.max(0, Math.min(
2028
+ playerPos.y - viewportHeight / 2,
2029
+ levelHeight - viewportHeight
2030
+ ));
2031
+ }, []);
2032
+ const gameLoop = useCallback4((timestamp) => {
2033
+ if (!gameActive || !gameStateRef.current) {
2034
+ return;
2035
+ }
2036
+ const state = gameStateRef.current;
2037
+ if (state.lastTime === null) {
2038
+ state.lastTime = timestamp;
2039
+ }
2040
+ const timeStep = Math.min(timestamp - state.lastTime, 100) / 1e3;
2041
+ state.lastTime = timestamp;
2042
+ if (state.level && state.gameRunning) {
2043
+ state.level.animate(timeStep, keysRef.current);
2044
+ if (state.level.player && state.level.player.alive) {
2045
+ updateCamera(
2046
+ state.level.player.pos,
2047
+ canvasRef.current.width,
2048
+ canvasRef.current.height,
2049
+ state.level.width,
2050
+ state.level.height
2051
+ );
2052
+ }
2053
+ if (state.level.isFinished()) {
2054
+ if (state.level.status === "won") {
2055
+ setGameStatus("won");
2056
+ if (level < LEVELS.length - 1) {
2057
+ setTimeout(() => {
2058
+ setLevel((prev) => prev + 1);
2059
+ setGameStatus("ready");
2060
+ setGameActive(false);
2061
+ }, 1500);
2062
+ }
2063
+ } else if (state.level.status === "lost") {
2064
+ setLives((prev) => {
2065
+ const newLives = prev - 1;
2066
+ if (newLives <= 0) {
2067
+ setGameStatus("lost");
2068
+ state.gameRunning = false;
2069
+ return 0;
2070
+ }
2071
+ setTimeout(() => {
2072
+ setGameStatus("ready");
2073
+ setGameActive(false);
2074
+ }, 1e3);
2075
+ return newLives;
2076
+ });
2077
+ }
2078
+ state.gameRunning = false;
2079
+ }
2080
+ drawGame(state.level, state.cameraX, state.cameraY);
2081
+ }
2082
+ if (gameActive) {
2083
+ animationRef.current = requestAnimationFrame(gameLoop);
2084
+ }
2085
+ }, [gameActive, level, updateCamera]);
2086
+ const drawGame = useCallback4((gameLevel, cameraX, cameraY) => {
2087
+ const canvas = canvasRef.current;
2088
+ if (!canvas) return;
2089
+ const ctx = canvas.getContext("2d");
2090
+ const scale = 12;
2091
+ ctx.fillStyle = "#222";
2092
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
2093
+ const viewStartX = Math.floor(cameraX);
2094
+ const viewEndX = Math.ceil(cameraX + canvas.width / scale);
2095
+ const viewStartY = Math.floor(cameraY);
2096
+ const viewEndY = Math.ceil(cameraY + canvas.height / scale);
2097
+ for (let y = viewStartY; y < viewEndY; y++) {
2098
+ if (y < 0 || y >= gameLevel.height) continue;
2099
+ for (let x = viewStartX; x < viewEndX; x++) {
2100
+ if (x < 0 || x >= gameLevel.width) continue;
2101
+ const cell = gameLevel.grid[y][x];
2102
+ const screenX = (x - cameraX) * scale;
2103
+ const screenY = (y - cameraY) * scale;
2104
+ if (cell === "wall") {
2105
+ ctx.fillStyle = "#444";
2106
+ ctx.fillRect(screenX, screenY, scale, scale);
2107
+ ctx.strokeStyle = "#333";
2108
+ ctx.lineWidth = 2;
2109
+ ctx.strokeRect(screenX, screenY, scale, scale);
2110
+ } else if (cell === "lava") {
2111
+ const pulse = Math.sin(Date.now() / 200) * 0.2 + 0.8;
2112
+ ctx.fillStyle = `rgb(${Math.floor(229 * pulse)}, ${Math.floor(85 * pulse)}, ${Math.floor(85 * pulse)})`;
2113
+ ctx.fillRect(screenX, screenY, scale, scale);
2114
+ if (Date.now() % 500 < 250) {
2115
+ ctx.fillStyle = "rgba(255, 100, 100, 0.3)";
2116
+ ctx.fillRect(screenX + 2, screenY + 2, scale - 4, scale - 4);
2117
+ }
2118
+ }
2119
+ }
2120
+ }
2121
+ gameLevel.actors.forEach((actor) => {
2122
+ if (actor.type === "coin" && actor.collected) return;
2123
+ const screenX = (actor.pos.x - cameraX) * scale;
2124
+ const screenY = (actor.pos.y - cameraY) * scale;
2125
+ if (screenX + actor.size.x * scale < 0 || screenX > canvas.width || screenY + actor.size.y * scale < 0 || screenY > canvas.height) {
2126
+ return;
2127
+ }
2128
+ if (actor.type === "player") {
2129
+ if (!actor.alive) {
2130
+ const deathScale = deathAnimationRef.current.scale;
2131
+ ctx.save();
2132
+ ctx.translate(screenX + actor.size.x * scale / 2, screenY + actor.size.y * scale / 2);
2133
+ ctx.scale(deathScale, deathScale);
2134
+ ctx.fillStyle = "#a04040";
2135
+ ctx.fillRect(
2136
+ -actor.size.x * scale / 2,
2137
+ -actor.size.y * scale / 2,
2138
+ actor.size.x * scale,
2139
+ actor.size.y * scale
2140
+ );
2141
+ ctx.strokeStyle = "#800000";
2142
+ ctx.lineWidth = 2;
2143
+ ctx.beginPath();
2144
+ ctx.moveTo(-actor.size.x * scale / 3, -actor.size.y * scale / 3);
2145
+ ctx.lineTo(actor.size.x * scale / 3, actor.size.y * scale / 3);
2146
+ ctx.moveTo(actor.size.x * scale / 3, -actor.size.y * scale / 3);
2147
+ ctx.lineTo(-actor.size.x * scale / 3, actor.size.y * scale / 3);
2148
+ ctx.stroke();
2149
+ ctx.restore();
2150
+ } else {
2151
+ ctx.fillStyle = gameLevel.status === "won" ? "#4CAF50" : "#335699";
2152
+ ctx.fillRect(screenX, screenY, actor.size.x * scale, actor.size.y * scale);
2153
+ ctx.fillStyle = "white";
2154
+ const eyeSize = scale * 0.2;
2155
+ const eyeOffset = actor.speed.x > 0 ? scale * 0.3 : scale * 0.1;
2156
+ ctx.fillRect(screenX + eyeOffset, screenY + scale * 0.2, eyeSize, eyeSize);
2157
+ ctx.fillRect(screenX + eyeOffset + scale * 0.2, screenY + scale * 0.2, eyeSize, eyeSize);
2158
+ ctx.strokeStyle = "white";
2159
+ ctx.lineWidth = 1;
2160
+ ctx.beginPath();
2161
+ ctx.arc(
2162
+ screenX + actor.size.x * scale / 2,
2163
+ screenY + scale * 0.6,
2164
+ scale * 0.2,
2165
+ 0.2,
2166
+ Math.PI - 0.2
2167
+ );
2168
+ ctx.stroke();
2169
+ }
2170
+ } else if (actor.type === "coin" && !actor.collected) {
2171
+ const wobble = Math.sin(actor.wobble) * 0.1;
2172
+ ctx.fillStyle = "#e2e838";
2173
+ ctx.beginPath();
2174
+ ctx.arc(
2175
+ screenX + actor.size.x * scale / 2,
2176
+ screenY + actor.size.y * scale / 2 + wobble * scale,
2177
+ actor.size.x * scale / 2,
2178
+ 0,
2179
+ Math.PI * 2
2180
+ );
2181
+ ctx.fill();
2182
+ ctx.fillStyle = "rgba(255, 255, 255, 0.8)";
2183
+ ctx.beginPath();
2184
+ ctx.arc(
2185
+ screenX + actor.size.x * scale / 2 - scale * 0.15,
2186
+ screenY + actor.size.y * scale / 2 - scale * 0.15 + wobble * scale,
2187
+ scale * 0.1,
2188
+ 0,
2189
+ Math.PI * 2
2190
+ );
2191
+ ctx.fill();
2192
+ ctx.strokeStyle = "rgba(255, 255, 200, 0.6)";
2193
+ ctx.lineWidth = 1;
2194
+ for (let i = 0; i < 4; i++) {
2195
+ const angle = actor.wobble + i * Math.PI / 2;
2196
+ const length = scale * 0.4;
2197
+ ctx.beginPath();
2198
+ ctx.moveTo(
2199
+ screenX + actor.size.x * scale / 2,
2200
+ screenY + actor.size.y * scale / 2 + wobble * scale
2201
+ );
2202
+ ctx.lineTo(
2203
+ screenX + actor.size.x * scale / 2 + Math.cos(angle) * length,
2204
+ screenY + actor.size.y * scale / 2 + wobble * scale + Math.sin(angle) * length
2205
+ );
2206
+ ctx.stroke();
2207
+ }
2208
+ } else if (actor.type === "lava") {
2209
+ if (actor.char === "=" || actor.char === "|" || actor.char === "v") {
2210
+ const pulse = Math.sin(Date.now() / 150) * 0.3 + 0.7;
2211
+ ctx.fillStyle = `rgb(${Math.floor(255 * pulse)}, ${Math.floor(100 * pulse)}, ${Math.floor(100 * pulse)})`;
2212
+ } else {
2213
+ const pulse = Math.sin(Date.now() / 300) * 0.2 + 0.8;
2214
+ ctx.fillStyle = `rgb(${Math.floor(229 * pulse)}, ${Math.floor(85 * pulse)}, ${Math.floor(85 * pulse)})`;
2215
+ }
2216
+ ctx.fillRect(screenX, screenY, actor.size.x * scale, actor.size.y * scale);
2217
+ if (Math.random() < 0.02) {
2218
+ const bubbleX = screenX + Math.random() * actor.size.x * scale;
2219
+ const bubbleY = screenY + Math.random() * actor.size.y * scale;
2220
+ const bubbleSize = Math.random() * 3 + 1;
2221
+ ctx.fillStyle = "rgba(255, 200, 200, 0.6)";
2222
+ ctx.beginPath();
2223
+ ctx.arc(bubbleX, bubbleY, bubbleSize, 0, Math.PI * 2);
2224
+ ctx.fill();
2225
+ }
2226
+ }
2227
+ });
2228
+ ctx.strokeStyle = "rgba(255, 255, 255, 0.1)";
2229
+ ctx.lineWidth = 1;
2230
+ ctx.strokeRect(-cameraX * scale, -cameraY * scale, gameLevel.width * scale, gameLevel.height * scale);
2231
+ }, []);
2232
+ useEffect6(() => {
2233
+ const handleKeyDown = (e) => {
2234
+ if (e.key.toLowerCase() === "r") {
2235
+ e.preventDefault();
2236
+ resetGame();
2237
+ return;
2238
+ }
2239
+ if (!gameActive && e.key === "Enter") {
2240
+ startGame();
2241
+ return;
2242
+ }
2243
+ if (!gameActive) return;
2244
+ switch (e.key.toLowerCase()) {
2245
+ case "arrowleft":
2246
+ case "a":
2247
+ keysRef.current.left = true;
2248
+ break;
2249
+ case "arrowright":
2250
+ case "d":
2251
+ keysRef.current.right = true;
2252
+ break;
2253
+ case "arrowup":
2254
+ case "w":
2255
+ case " ":
2256
+ keysRef.current.up = true;
2257
+ break;
2258
+ }
2259
+ };
2260
+ const handleKeyUp = (e) => {
2261
+ if (!gameActive) return;
2262
+ switch (e.key.toLowerCase()) {
2263
+ case "arrowleft":
2264
+ case "a":
2265
+ keysRef.current.left = false;
2266
+ break;
2267
+ case "arrowright":
2268
+ case "d":
2269
+ keysRef.current.right = false;
2270
+ break;
2271
+ case "arrowup":
2272
+ case "w":
2273
+ case " ":
2274
+ keysRef.current.up = false;
2275
+ break;
2276
+ }
2277
+ };
2278
+ window.addEventListener("keydown", handleKeyDown);
2279
+ window.addEventListener("keyup", handleKeyUp);
2280
+ return () => {
2281
+ window.removeEventListener("keydown", handleKeyDown);
2282
+ window.removeEventListener("keyup", handleKeyUp);
2283
+ };
2284
+ }, [gameActive]);
2285
+ useEffect6(() => {
2286
+ if (gameActive) {
2287
+ animationRef.current = requestAnimationFrame(gameLoop);
2288
+ } else {
2289
+ if (animationRef.current) {
2290
+ cancelAnimationFrame(animationRef.current);
2291
+ }
2292
+ }
2293
+ return () => {
2294
+ if (animationRef.current) {
2295
+ cancelAnimationFrame(animationRef.current);
2296
+ }
2297
+ };
2298
+ }, [gameActive, gameLoop]);
2299
+ const resetGame = useCallback4(() => {
2300
+ setScore(0);
2301
+ setLevel(0);
2302
+ setLives(3);
2303
+ setCoinsCollected(0);
2304
+ setGameStatus("ready");
2305
+ setGameActive(false);
2306
+ if (animationRef.current) {
2307
+ cancelAnimationFrame(animationRef.current);
2308
+ }
2309
+ deathAnimationRef.current = { active: false, time: 0, scale: 1 };
2310
+ setTimeout(() => {
2311
+ if (canvasRef.current) {
2312
+ const ctx = canvasRef.current.getContext("2d");
2313
+ ctx.fillStyle = "#222";
2314
+ ctx.fillRect(0, 0, canvasRef.current.width, canvasRef.current.height);
2315
+ }
2316
+ }, 100);
2317
+ }, []);
2318
+ const startGame = () => {
2319
+ if (gameStatus === "lost") {
2320
+ resetGame();
2321
+ }
2322
+ initGame();
2323
+ };
2324
+ useEffect6(() => {
2325
+ if (score > 0) {
2326
+ const event = new CustomEvent("gameScoreUpdate", {
2327
+ detail: { game: "platformer", score }
2328
+ });
2329
+ window.dispatchEvent(event);
2330
+ }
2331
+ }, [score]);
2332
+ return /* @__PURE__ */ jsxs6("div", { className: "platformer-game", children: [
2333
+ /* @__PURE__ */ jsxs6("div", { className: "game-header", children: [
2334
+ /* @__PURE__ */ jsx6("h2", { children: "\u{1F3AE} \u041F\u043B\u0430\u0442\u0444\u043E\u0440\u043C\u0435\u0440" }),
2335
+ /* @__PURE__ */ jsxs6("div", { className: "game-controls", children: [
2336
+ !gameActive ? /* @__PURE__ */ jsx6("button", { className: "control-btn start-btn", onClick: startGame, children: gameStatus === "ready" ? "\u25B6\uFE0F \u041D\u0430\u0447\u0430\u0442\u044C \u0438\u0433\u0440\u0443" : gameStatus === "won" ? "\u{1F389} \u0421\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u0439 \u0443\u0440\u043E\u0432\u0435\u043D\u044C" : "\u{1F504} \u0418\u0433\u0440\u0430\u0442\u044C \u0441\u043D\u043E\u0432\u0430" }) : /* @__PURE__ */ jsx6("button", { className: "control-btn pause-btn", onClick: () => setGameActive(false), children: "\u23F8\uFE0F \u041F\u0430\u0443\u0437\u0430" }),
2337
+ /* @__PURE__ */ jsx6("button", { className: "control-btn reset-btn", onClick: resetGame, children: "\u{1F504} \u0421\u0431\u0440\u043E\u0441 (R)" })
2338
+ ] })
2339
+ ] }),
2340
+ /* @__PURE__ */ jsxs6("div", { className: "game-stats", children: [
2341
+ /* @__PURE__ */ jsxs6("div", { className: "stat-box", children: [
2342
+ /* @__PURE__ */ jsx6("div", { className: "stat-label", children: "\u0421\u0447\u0435\u0442" }),
2343
+ /* @__PURE__ */ jsx6("div", { className: "stat-value", children: score })
2344
+ ] }),
2345
+ /* @__PURE__ */ jsxs6("div", { className: "stat-box", children: [
2346
+ /* @__PURE__ */ jsx6("div", { className: "stat-label", children: "\u0423\u0440\u043E\u0432\u0435\u043D\u044C" }),
2347
+ /* @__PURE__ */ jsxs6("div", { className: "stat-value", children: [
2348
+ level + 1,
2349
+ " / ",
2350
+ LEVELS.length
2351
+ ] })
2352
+ ] }),
2353
+ /* @__PURE__ */ jsxs6("div", { className: "stat-box", children: [
2354
+ /* @__PURE__ */ jsx6("div", { className: "stat-label", children: "\u0416\u0438\u0437\u043D\u0438" }),
2355
+ /* @__PURE__ */ jsx6("div", { className: "stat-value", children: /* @__PURE__ */ jsx6("span", { style: { color: lives <= 1 ? "#ff5e62" : "#667eea" }, children: "\u2764\uFE0F".repeat(Math.max(0, lives)) }) })
2356
+ ] }),
2357
+ /* @__PURE__ */ jsxs6("div", { className: "stat-box", children: [
2358
+ /* @__PURE__ */ jsx6("div", { className: "stat-label", children: "\u041C\u043E\u043D\u0435\u0442\u044B" }),
2359
+ /* @__PURE__ */ jsxs6("div", { className: "stat-value", children: [
2360
+ coinsCollected,
2361
+ " / ",
2362
+ totalCoins,
2363
+ " \u{1F7E1}"
2364
+ ] })
2365
+ ] })
2366
+ ] }),
2367
+ /* @__PURE__ */ jsx6("div", { className: "game-area", children: /* @__PURE__ */ jsxs6("div", { className: "platformer-container", ref: gameContainerRef, children: [
2368
+ /* @__PURE__ */ jsx6(
2369
+ "canvas",
2370
+ {
2371
+ ref: canvasRef,
2372
+ width: 800,
2373
+ height: 400,
2374
+ className: "platformer-canvas"
2375
+ }
2376
+ ),
2377
+ !gameActive && gameStatus === "ready" && /* @__PURE__ */ jsx6("div", { className: "start-screen", children: /* @__PURE__ */ jsxs6("div", { className: "start-message", children: [
2378
+ /* @__PURE__ */ jsx6("h3", { children: "\u041F\u043B\u0430\u0442\u0444\u043E\u0440\u043C\u0435\u0440" }),
2379
+ /* @__PURE__ */ jsx6("p", { children: "\u0421\u043E\u0431\u0438\u0440\u0430\u0439\u0442\u0435 \u043C\u043E\u043D\u0435\u0442\u044B \u{1F7E1}, \u0438\u0437\u0431\u0435\u0433\u0430\u0439\u0442\u0435 \u043B\u0430\u0432\u044B \u{1F534}" }),
2380
+ /* @__PURE__ */ jsx6("p", { children: "\u0423\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0438\u0435: \u2190 \u2192 (\u0438\u043B\u0438 A, D) \u0438 \u041F\u0420\u041E\u0411\u0415\u041B \u0434\u043B\u044F \u043F\u0440\u044B\u0436\u043A\u0430" }),
2381
+ /* @__PURE__ */ jsx6("p", { children: "\u041D\u0430\u0436\u043C\u0438\u0442\u0435 ENTER \u0434\u043B\u044F \u0441\u0442\u0430\u0440\u0442\u0430, R \u0434\u043B\u044F \u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A\u0430" }),
2382
+ /* @__PURE__ */ jsx6("button", { className: "start-btn-big", onClick: startGame, children: "\u041D\u0410\u0427\u0410\u0422\u042C \u0418\u0413\u0420\u0423" })
2383
+ ] }) }),
2384
+ gameStatus === "won" && /* @__PURE__ */ jsx6("div", { className: "win-screen", children: /* @__PURE__ */ jsxs6("div", { className: "win-message", children: [
2385
+ /* @__PURE__ */ jsx6("h3", { children: "\u{1F389} \u0423\u0420\u041E\u0412\u0415\u041D\u042C \u041F\u0420\u041E\u0419\u0414\u0415\u041D!" }),
2386
+ /* @__PURE__ */ jsxs6("p", { children: [
2387
+ "\u041D\u0430\u0431\u0440\u0430\u043D\u043E \u043E\u0447\u043A\u043E\u0432: ",
2388
+ score
2389
+ ] }),
2390
+ /* @__PURE__ */ jsxs6("p", { children: [
2391
+ "\u0421\u043E\u0431\u0440\u0430\u043D\u043E \u043C\u043E\u043D\u0435\u0442: ",
2392
+ coinsCollected,
2393
+ "/",
2394
+ totalCoins
2395
+ ] }),
2396
+ level < LEVELS.length - 1 ? /* @__PURE__ */ jsx6("button", { className: "next-level-btn", onClick: startGame, children: "\u0421\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u0439 \u0443\u0440\u043E\u0432\u0435\u043D\u044C \u2192" }) : /* @__PURE__ */ jsx6("button", { className: "restart-btn", onClick: resetGame, children: "\u{1F3C6} \u041F\u043E\u0437\u0434\u0440\u0430\u0432\u043B\u044F\u0435\u043C! \u041D\u0430\u0447\u0430\u0442\u044C \u0437\u0430\u043D\u043E\u0432\u043E" })
2397
+ ] }) }),
2398
+ gameStatus === "lost" && /* @__PURE__ */ jsx6("div", { className: "lose-screen", children: /* @__PURE__ */ jsxs6("div", { className: "lose-message", children: [
2399
+ /* @__PURE__ */ jsx6("h3", { children: "\u{1F480} \u0418\u0413\u0420\u0410 \u041E\u041A\u041E\u041D\u0427\u0415\u041D\u0410" }),
2400
+ /* @__PURE__ */ jsxs6("p", { children: [
2401
+ "\u0424\u0438\u043D\u0430\u043B\u044C\u043D\u044B\u0439 \u0441\u0447\u0435\u0442: ",
2402
+ score
2403
+ ] }),
2404
+ /* @__PURE__ */ jsxs6("p", { children: [
2405
+ "\u0421\u043E\u0431\u0440\u0430\u043D\u043E \u043C\u043E\u043D\u0435\u0442: ",
2406
+ coinsCollected,
2407
+ "/",
2408
+ totalCoins
2409
+ ] }),
2410
+ /* @__PURE__ */ jsx6("button", { className: "restart-btn", onClick: resetGame, children: "\u0418\u0433\u0440\u0430\u0442\u044C \u0441\u043D\u043E\u0432\u0430" })
2411
+ ] }) })
2412
+ ] }) }),
2413
+ /* @__PURE__ */ jsxs6("div", { className: "game-instructions", children: [
2414
+ /* @__PURE__ */ jsxs6("div", { className: "instructions-row", children: [
2415
+ /* @__PURE__ */ jsxs6("p", { children: [
2416
+ /* @__PURE__ */ jsx6("strong", { children: "\u0423\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0438\u0435:" }),
2417
+ " \u2190 \u2192 (A, D) - \u0434\u0432\u0438\u0436\u0435\u043D\u0438\u0435, \u041F\u0420\u041E\u0411\u0415\u041B - \u043F\u0440\u044B\u0436\u043E\u043A"
2418
+ ] }),
2419
+ /* @__PURE__ */ jsxs6("p", { children: [
2420
+ /* @__PURE__ */ jsx6("strong", { children: "\u0426\u0435\u043B\u044C:" }),
2421
+ " \u0421\u043E\u0431\u0440\u0430\u0442\u044C \u0432\u0441\u0435 \u043C\u043E\u043D\u0435\u0442\u044B, \u0438\u0437\u0431\u0435\u0433\u0430\u044F \u043B\u0430\u0432\u044B"
2422
+ ] })
2423
+ ] }),
2424
+ /* @__PURE__ */ jsxs6("div", { className: "legend", children: [
2425
+ /* @__PURE__ */ jsxs6("div", { className: "legend-item", children: [
2426
+ /* @__PURE__ */ jsx6("div", { className: "legend-color player-color" }),
2427
+ /* @__PURE__ */ jsx6("span", { children: "\u0418\u0433\u0440\u043E\u043A (\u0441\u0438\u043D\u0438\u0439)" })
2428
+ ] }),
2429
+ /* @__PURE__ */ jsxs6("div", { className: "legend-item", children: [
2430
+ /* @__PURE__ */ jsx6("div", { className: "legend-color coin-color" }),
2431
+ /* @__PURE__ */ jsx6("span", { children: "\u041C\u043E\u043D\u0435\u0442\u0430 (+100 \u043E\u0447\u043A\u043E\u0432)" })
2432
+ ] }),
2433
+ /* @__PURE__ */ jsxs6("div", { className: "legend-item", children: [
2434
+ /* @__PURE__ */ jsx6("div", { className: "legend-color lava-color" }),
2435
+ /* @__PURE__ */ jsx6("span", { children: "\u041B\u0430\u0432\u0430 (\u043E\u043F\u0430\u0441\u043D\u043E)" })
2436
+ ] }),
2437
+ /* @__PURE__ */ jsxs6("div", { className: "legend-item", children: [
2438
+ /* @__PURE__ */ jsx6("div", { className: "legend-color wall-color" }),
2439
+ /* @__PURE__ */ jsx6("span", { children: "\u0421\u0442\u0435\u043D\u0430" })
2440
+ ] })
2441
+ ] })
2442
+ ] })
2443
+ ] });
2444
+ };
2445
+ var PlatformerGame_default = PlatformerGame;
2446
+
2447
+ // src/components/GameHubNode/GameHubNode.js
2448
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
2449
+ var MENU_SIZE = { width: 560, height: 520 };
2450
+ var GAME_NODE_SIZES = {
2451
+ snake: { width: 700, height: 1e3 },
2452
+ "2048": { width: 560, height: 780 },
2453
+ memory: { width: 680, height: 890 },
2454
+ clicker: { width: 980, height: 760 },
2455
+ pong: { width: 760, height: 780 },
2456
+ platformer: { width: 980, height: 750 }
2457
+ };
2458
+ var STATS_API_BASE = process.env.REACT_APP_STATS_API_URL || "http://localhost:4000";
2459
+ function getAuthFromStorage() {
2460
+ const token = localStorage.getItem("jwt") || localStorage.getItem("token") || localStorage.getItem("access_token") || localStorage.getItem("accessToken") || "";
2461
+ let user = null;
2462
+ const userRaw = localStorage.getItem("user") || localStorage.getItem("userInfo") || localStorage.getItem("profile");
2463
+ if (userRaw) {
2464
+ try {
2465
+ user = JSON.parse(userRaw);
2466
+ } catch {
2467
+ user = null;
2468
+ }
2469
+ }
2470
+ const userId = user && (user.id || user.userId || user._id) || localStorage.getItem("userId") || localStorage.getItem("uid") || "";
2471
+ return { userId, token };
2472
+ }
2473
+ async function fetchUserStats(userId, token) {
2474
+ const res = await fetch(`${STATS_API_BASE}/api/stats/${encodeURIComponent(userId)}`, {
2475
+ headers: { ...token ? { Authorization: `Bearer ${token}` } : {} }
2476
+ });
2477
+ if (!res.ok) throw new Error(`GET stats failed: ${res.status}`);
2478
+ return res.json();
2479
+ }
2480
+ async function postGameStat({ userId, token, gameId, score }) {
2481
+ const res = await fetch(`${STATS_API_BASE}/api/stats`, {
2482
+ method: "POST",
2483
+ headers: {
2484
+ "Content-Type": "application/json",
2485
+ ...token ? { Authorization: `Bearer ${token}` } : {}
2486
+ },
2487
+ body: JSON.stringify({ userId, gameId, score })
2488
+ });
2489
+ if (!res.ok) {
2490
+ const text = await res.text().catch(() => "");
2491
+ throw new Error(`POST stats failed: ${res.status} ${text}`);
2492
+ }
2493
+ return res.json();
2494
+ }
2495
+ async function fetchLeaderboard(gameId, limit = 10) {
2496
+ const res = await fetch(
2497
+ `${STATS_API_BASE}/api/leaderboard/${encodeURIComponent(gameId)}?limit=${encodeURIComponent(limit)}`
2498
+ );
2499
+ if (!res.ok) throw new Error(`GET leaderboard failed: ${res.status}`);
2500
+ return res.json();
2501
+ }
2502
+ async function upsertNickname({ userId, nickname }) {
2503
+ const res = await fetch(`${STATS_API_BASE}/api/users`, {
2504
+ method: "POST",
2505
+ headers: { "Content-Type": "application/json" },
2506
+ body: JSON.stringify({ userId, nickname })
2507
+ });
2508
+ if (!res.ok) {
2509
+ const text = await res.text().catch(() => "");
2510
+ throw new Error(`POST /api/users failed: ${res.status} ${text}`);
2511
+ }
2512
+ return res.json();
2513
+ }
2514
+ async function fetchNickname(userId) {
2515
+ const res = await fetch(`${STATS_API_BASE}/api/users/${encodeURIComponent(userId)}`);
2516
+ if (!res.ok) throw new Error(`GET /api/users/:userId failed: ${res.status}`);
2517
+ return res.json();
2518
+ }
2519
+ var GameHubNode = memo(({ id, data, selected, overlayMode = false, overlayGameId }) => {
2520
+ const games = useMemo(
2521
+ () => [
2522
+ { id: "snake", title: "\u{1F40D} \u0417\u043C\u0435\u0439\u043A\u0430", desc: "\u041A\u043B\u0430\u0441\u0441\u0438\u043A\u0430 \u043D\u0430 \u0441\u0435\u0442\u043A\u0435", component: /* @__PURE__ */ jsx7(SnakeGame_default, {}) },
2523
+ { id: "2048", title: "\u{1F522} 2048", desc: "\u0421\u043E\u0431\u0435\u0440\u0438 2048", component: /* @__PURE__ */ jsx7(Game2048_default, {}) },
2524
+ { id: "memory", title: "\u{1F9E0} \u041F\u0430\u043C\u044F\u0442\u044C", desc: "\u041D\u0430\u0439\u0434\u0438 \u043F\u0430\u0440\u044B", component: /* @__PURE__ */ jsx7(MemoryGame_default, {}) },
2525
+ { id: "clicker", title: "\u{1F5B1}\uFE0F \u041A\u043B\u0438\u043A\u0435\u0440", desc: "\u041A\u043B\u0438\u043A\u0430\u0439 \u0438 \u043F\u0440\u043E\u043A\u0430\u0447\u0438\u0432\u0430\u0439", component: /* @__PURE__ */ jsx7(ClickerGame_default, {}) },
2526
+ { id: "pong", title: "\u{1F3D3} \u041F\u0438\u043D\u0433-\u041F\u043E\u043D\u0433", desc: "\u0420\u0430\u043A\u0435\u0442\u043A\u0430 \u0438 \u043C\u044F\u0447", component: /* @__PURE__ */ jsx7(Pong_default, {}) },
2527
+ { id: "platformer", title: "\u{1F47E} \u041F\u043B\u0430\u0442\u0444\u043E\u0440\u043C\u0435\u0440", desc: "\u041F\u0440\u044B\u0433\u0430\u0439 \u0438 \u0431\u0435\u0433\u0438", component: /* @__PURE__ */ jsx7(PlatformerGame_default, {}) }
2528
+ ],
2529
+ []
2530
+ );
2531
+ const [view, setView] = useState7("menu");
2532
+ const [currentGame, setCurrentGame] = useState7(games[0].id);
2533
+ const [currentScore, setCurrentScore] = useState7(0);
2534
+ const [menuTab, setMenuTab] = useState7("games");
2535
+ const [userStats, setUserStats] = useState7([]);
2536
+ const [statsLoading, setStatsLoading] = useState7(false);
2537
+ const [statsError, setStatsError] = useState7("");
2538
+ const [lbGame, setLbGame] = useState7(games[0].id);
2539
+ const [leaderboard, setLeaderboard] = useState7([]);
2540
+ const [lbLoading, setLbLoading] = useState7(false);
2541
+ const [lbError, setLbError] = useState7("");
2542
+ const [nicknameInput, setNicknameInput] = useState7("");
2543
+ const [nickSaving, setNickSaving] = useState7(false);
2544
+ const [nickMsg, setNickMsg] = useState7("");
2545
+ useEffect7(() => {
2546
+ if (overlayMode && overlayGameId) {
2547
+ setView("game");
2548
+ setCurrentGame(overlayGameId);
2549
+ setCurrentScore(0);
2550
+ }
2551
+ }, [overlayMode, overlayGameId]);
2552
+ const active = useMemo(() => games.find((g) => g.id === currentGame), [games, currentGame]);
2553
+ useEffect7(() => {
2554
+ const handleScoreUpdate = (e) => {
2555
+ const d = e?.detail;
2556
+ if (!d) return;
2557
+ if (d.game === currentGame) setCurrentScore(d.score ?? 0);
2558
+ };
2559
+ window.addEventListener("gameScoreUpdate", handleScoreUpdate);
2560
+ return () => window.removeEventListener("gameScoreUpdate", handleScoreUpdate);
2561
+ }, [currentGame]);
2562
+ useEffect7(() => {
2563
+ const size = overlayMode ? GAME_NODE_SIZES[currentGame] : view === "menu" ? MENU_SIZE : GAME_NODE_SIZES[currentGame] || MENU_SIZE;
2564
+ window.dispatchEvent(
2565
+ new CustomEvent("resizeGameHubNode", {
2566
+ detail: { nodeId: id, width: size.width, height: size.height }
2567
+ })
2568
+ );
2569
+ }, [id, view, currentGame, overlayMode]);
2570
+ const backToMenu = () => {
2571
+ setView("menu");
2572
+ setCurrentScore(0);
2573
+ };
2574
+ const startGame = (gameId) => {
2575
+ setCurrentGame(gameId);
2576
+ setCurrentScore(0);
2577
+ setView("game");
2578
+ };
2579
+ useEffect7(() => {
2580
+ if (overlayMode) return;
2581
+ if (view !== "game") return;
2582
+ const { userId, token } = getAuthFromStorage();
2583
+ if (!userId) {
2584
+ setStatsError("\u041D\u0435\u0442 userId \u0432 storage");
2585
+ setUserStats([]);
2586
+ return;
2587
+ }
2588
+ let cancelled = false;
2589
+ setStatsLoading(true);
2590
+ setStatsError("");
2591
+ fetchUserStats(userId, token).then((rows) => {
2592
+ if (cancelled) return;
2593
+ setUserStats(Array.isArray(rows) ? rows : []);
2594
+ }).catch((err) => {
2595
+ if (cancelled) return;
2596
+ setStatsError("\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044C \u0441\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u043A\u0443");
2597
+ setUserStats([]);
2598
+ console.warn(err);
2599
+ }).finally(() => {
2600
+ if (cancelled) return;
2601
+ setStatsLoading(false);
2602
+ });
2603
+ return () => {
2604
+ cancelled = true;
2605
+ };
2606
+ }, [view, currentGame, overlayMode]);
2607
+ const currentGameStat = useMemo(() => {
2608
+ const row = userStats.find((r) => r?.game_id === currentGame);
2609
+ if (!row) return null;
2610
+ return {
2611
+ best: row.best_score ?? 0,
2612
+ last: row.last_score ?? 0,
2613
+ plays: row.plays ?? 0
2614
+ };
2615
+ }, [userStats, currentGame]);
2616
+ const saveScore = async () => {
2617
+ const { userId, token } = getAuthFromStorage();
2618
+ if (!userId) {
2619
+ setStatsError("\u041D\u0435\u0442 userId \u0432 storage");
2620
+ return;
2621
+ }
2622
+ try {
2623
+ setStatsError("");
2624
+ setStatsLoading(true);
2625
+ await postGameStat({
2626
+ userId,
2627
+ token,
2628
+ gameId: currentGame,
2629
+ score: Number(currentScore) || 0
2630
+ });
2631
+ const rows = await fetchUserStats(userId, token);
2632
+ setUserStats(Array.isArray(rows) ? rows : []);
2633
+ } catch (err) {
2634
+ setStatsError("\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0441\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442");
2635
+ console.warn(err);
2636
+ } finally {
2637
+ setStatsLoading(false);
2638
+ }
2639
+ };
2640
+ const loadLeaderboard = async (gameId) => {
2641
+ try {
2642
+ setLbError("");
2643
+ setLbLoading(true);
2644
+ const rows = await fetchLeaderboard(gameId, 10);
2645
+ setLeaderboard(Array.isArray(rows) ? rows : []);
2646
+ } catch (err) {
2647
+ setLbError("\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044C \u0442\u043E\u043F-10");
2648
+ setLeaderboard([]);
2649
+ console.warn(err);
2650
+ } finally {
2651
+ setLbLoading(false);
2652
+ }
2653
+ };
2654
+ useEffect7(() => {
2655
+ if (overlayMode) return;
2656
+ if (view !== "menu") return;
2657
+ if (menuTab !== "stats") return;
2658
+ loadLeaderboard(lbGame);
2659
+ const { userId } = getAuthFromStorage();
2660
+ if (!userId) return;
2661
+ fetchNickname(userId).then((u) => {
2662
+ if (u?.nickname) setNicknameInput(u.nickname);
2663
+ }).catch((e) => {
2664
+ console.warn(e);
2665
+ });
2666
+ }, [menuTab, lbGame, view, overlayMode]);
2667
+ const saveNickname = async () => {
2668
+ const { userId } = getAuthFromStorage();
2669
+ if (!userId) {
2670
+ setNickMsg("\u26A0\uFE0F \u041D\u0435\u0442 userId \u0432 storage");
2671
+ return;
2672
+ }
2673
+ const nick = (nicknameInput || "").trim();
2674
+ if (!nick) {
2675
+ setNickMsg("\u26A0\uFE0F \u0412\u0432\u0435\u0434\u0438 \u043D\u0438\u043A\u043D\u0435\u0439\u043C");
2676
+ return;
2677
+ }
2678
+ try {
2679
+ setNickMsg("");
2680
+ setNickSaving(true);
2681
+ await upsertNickname({ userId, nickname: nick });
2682
+ setNicknameInput(nick);
2683
+ setNickMsg("\u2705 \u041D\u0438\u043A \u0441\u043E\u0445\u0440\u0430\u043D\u0451\u043D");
2684
+ await loadLeaderboard(lbGame);
2685
+ } catch (e) {
2686
+ console.warn(e);
2687
+ setNickMsg("\u26A0\uFE0F \u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0441\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C \u043D\u0438\u043A");
2688
+ } finally {
2689
+ setNickSaving(false);
2690
+ }
2691
+ };
2692
+ if (overlayMode) {
2693
+ return /* @__PURE__ */ jsx7("div", { className: "game-overlay-container nodrag", children: /* @__PURE__ */ jsx7("div", { className: "game-overlay-game nodrag", children: active?.component }) });
2694
+ }
2695
+ const subtitleText = view === "menu" ? menuTab === "stats" ? "\u041B\u0438\u0434\u0435\u0440\u0431\u043E\u0440\u0434\u044B: \u0442\u043E\u043F-10 \u043F\u043E \u0438\u0433\u0440\u0430\u043C" : "\u0412\u044B\u0431\u0435\u0440\u0438 \u0438\u0433\u0440\u0443" : `\u0422\u0435\u043A\u0443\u0449\u0430\u044F: ${active?.title || ""} \u2022 \u0421\u0447\u0451\u0442: ${currentScore}${currentGameStat ? ` \u2022 Best: ${currentGameStat.best} \u2022 Last: ${currentGameStat.last} \u2022 Plays: ${currentGameStat.plays}` : ""}`;
2696
+ return /* @__PURE__ */ jsxs7("div", { className: `gamehub-node ${selected ? "selected" : ""}`, children: [
2697
+ /* @__PURE__ */ jsx7(Handle, { type: "target", position: Position.Top }),
2698
+ /* @__PURE__ */ jsxs7("div", { className: "gamehub-drag-handle", children: [
2699
+ /* @__PURE__ */ jsx7("div", { className: "gamehub-title", children: data?.label ?? "\u{1F3AE} \u0418\u0433\u0440\u043E\u0432\u043E\u0439 \u0445\u0430\u0431" }),
2700
+ /* @__PURE__ */ jsx7("div", { className: "gamehub-subtitle", children: subtitleText })
2701
+ ] }),
2702
+ /* @__PURE__ */ jsx7("div", { className: "gamehub-content nodrag", children: view === "menu" ? /* @__PURE__ */ jsxs7("div", { className: "gamehub-menu nodrag", children: [
2703
+ /* @__PURE__ */ jsxs7("div", { className: "gamehub-menu-head nodrag", children: [
2704
+ /* @__PURE__ */ jsx7("div", { className: "gamehub-menu-title nodrag", children: menuTab === "games" ? "\u0412\u044B\u0431\u043E\u0440 \u0438\u0433\u0440\u044B" : "\u0421\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u043A\u0430 \u0438\u0433\u0440\u043E\u043A\u043E\u0432" }),
2705
+ /* @__PURE__ */ jsxs7("div", { className: "gamehub-menu-tabs nodrag", children: [
2706
+ /* @__PURE__ */ jsx7(
2707
+ "button",
2708
+ {
2709
+ type: "button",
2710
+ className: `gamehub-tab nodrag ${menuTab === "games" ? "active" : ""}`,
2711
+ onClick: () => setMenuTab("games"),
2712
+ children: "\u{1F3AE} \u0418\u0433\u0440\u044B"
2713
+ }
2714
+ ),
2715
+ /* @__PURE__ */ jsx7(
2716
+ "button",
2717
+ {
2718
+ type: "button",
2719
+ className: `gamehub-tab nodrag ${menuTab === "stats" ? "active" : ""}`,
2720
+ onClick: () => {
2721
+ setMenuTab("stats");
2722
+ setNickMsg("");
2723
+ },
2724
+ children: "\u{1F3C6} \u0421\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u043A\u0430"
2725
+ }
2726
+ )
2727
+ ] })
2728
+ ] }),
2729
+ menuTab === "games" ? /* @__PURE__ */ jsx7("div", { className: "gamehub-menu-grid nodrag", children: games.map((g) => /* @__PURE__ */ jsxs7(
2730
+ "button",
2731
+ {
2732
+ type: "button",
2733
+ className: "gamehub-card nodrag",
2734
+ onClick: () => startGame(g.id),
2735
+ children: [
2736
+ /* @__PURE__ */ jsx7("div", { className: "gamehub-card-title", children: g.title }),
2737
+ /* @__PURE__ */ jsx7("div", { className: "gamehub-card-desc", children: g.desc }),
2738
+ /* @__PURE__ */ jsx7("div", { className: "gamehub-card-cta", children: "\u0418\u0433\u0440\u0430\u0442\u044C \u2192" })
2739
+ ]
2740
+ },
2741
+ g.id
2742
+ )) }) : /* @__PURE__ */ jsxs7("div", { className: "gamehub-stats nodrag", children: [
2743
+ /* @__PURE__ */ jsxs7("div", { className: "gamehub-nick-box nodrag", children: [
2744
+ /* @__PURE__ */ jsx7("div", { className: "gamehub-nick-title nodrag", children: "\u0422\u0432\u043E\u0439 \u043D\u0438\u043A\u043D\u0435\u0439\u043C" }),
2745
+ /* @__PURE__ */ jsxs7("div", { className: "gamehub-nick-row nodrag", children: [
2746
+ /* @__PURE__ */ jsx7(
2747
+ "input",
2748
+ {
2749
+ className: "gamehub-nick-input nodrag",
2750
+ placeholder: "\u041D\u0430\u043F\u0440\u0438\u043C\u0435\u0440: \u0410\u043B\u0435\u043A\u0441",
2751
+ value: nicknameInput,
2752
+ onChange: (e) => setNicknameInput(e.target.value)
2753
+ }
2754
+ ),
2755
+ /* @__PURE__ */ jsx7(
2756
+ "button",
2757
+ {
2758
+ type: "button",
2759
+ className: "gamehub-top-btn nodrag",
2760
+ onClick: saveNickname,
2761
+ disabled: nickSaving,
2762
+ title: "\u0421\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C \u043D\u0438\u043A\u043D\u0435\u0439\u043C",
2763
+ children: "\u{1F4BE} \u0421\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C \u043D\u0438\u043A"
2764
+ }
2765
+ )
2766
+ ] }),
2767
+ nickMsg ? /* @__PURE__ */ jsx7("div", { className: "gamehub-nick-msg nodrag", children: nickMsg }) : null
2768
+ ] }),
2769
+ /* @__PURE__ */ jsxs7("div", { className: "gamehub-stats-controls nodrag", children: [
2770
+ /* @__PURE__ */ jsxs7("label", { className: "gamehub-stats-label nodrag", children: [
2771
+ "\u0418\u0433\u0440\u0430:",
2772
+ /* @__PURE__ */ jsx7(
2773
+ "select",
2774
+ {
2775
+ className: "gamehub-stats-select nodrag",
2776
+ value: lbGame,
2777
+ onChange: (e) => setLbGame(e.target.value),
2778
+ children: games.map((g) => /* @__PURE__ */ jsx7("option", { value: g.id, children: g.title }, g.id))
2779
+ }
2780
+ )
2781
+ ] }),
2782
+ /* @__PURE__ */ jsx7(
2783
+ "button",
2784
+ {
2785
+ type: "button",
2786
+ className: "gamehub-top-btn nodrag",
2787
+ onClick: () => loadLeaderboard(lbGame),
2788
+ disabled: lbLoading,
2789
+ title: "\u041E\u0431\u043D\u043E\u0432\u0438\u0442\u044C \u0442\u043E\u043F-10",
2790
+ children: "\u{1F504} \u041E\u0431\u043D\u043E\u0432\u0438\u0442\u044C"
2791
+ }
2792
+ )
2793
+ ] }),
2794
+ lbError ? /* @__PURE__ */ jsxs7("div", { className: "gamehub-stats-error nodrag", children: [
2795
+ "\u26A0\uFE0F ",
2796
+ lbError
2797
+ ] }) : lbLoading ? /* @__PURE__ */ jsx7("div", { className: "gamehub-stats-loading nodrag", children: "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430\u2026" }) : leaderboard.length === 0 ? /* @__PURE__ */ jsx7("div", { className: "gamehub-stats-empty nodrag", children: "(\u043F\u043E\u043A\u0430 \u043D\u0435\u0442 \u0434\u0430\u043D\u043D\u044B\u0445)" }) : /* @__PURE__ */ jsx7("ol", { className: "gamehub-leaderboard nodrag", children: leaderboard.map((row, idx) => /* @__PURE__ */ jsxs7("li", { className: "gamehub-leaderboard-item nodrag", children: [
2798
+ /* @__PURE__ */ jsxs7("span", { className: "gamehub-lb-rank nodrag", children: [
2799
+ "#",
2800
+ idx + 1
2801
+ ] }),
2802
+ /* @__PURE__ */ jsx7("span", { className: "gamehub-lb-user nodrag", children: row.nickname || row.user_id }),
2803
+ /* @__PURE__ */ jsx7("span", { className: "gamehub-lb-score nodrag", children: row.best_score })
2804
+ ] }, `${row.user_id}-${idx}`)) })
2805
+ ] })
2806
+ ] }) : /* @__PURE__ */ jsxs7("div", { className: "gamehub-game-screen nodrag", children: [
2807
+ /* @__PURE__ */ jsxs7("div", { className: "gamehub-game-topbar nodrag", children: [
2808
+ /* @__PURE__ */ jsx7("button", { type: "button", className: "gamehub-top-btn nodrag", onClick: backToMenu, children: "\u2190 \u041C\u0435\u043D\u044E" }),
2809
+ /* @__PURE__ */ jsx7("div", { className: "gamehub-top-title nodrag", children: active?.title }),
2810
+ /* @__PURE__ */ jsxs7("div", { className: "gamehub-top-right nodrag", children: [
2811
+ statsError ? /* @__PURE__ */ jsxs7("span", { className: "gamehub-stats-text nodrag", children: [
2812
+ "\u26A0\uFE0F ",
2813
+ statsError
2814
+ ] }) : currentGameStat ? /* @__PURE__ */ jsxs7("span", { className: "gamehub-stats-text nodrag", children: [
2815
+ "Best ",
2816
+ currentGameStat.best,
2817
+ " \u2022 Last ",
2818
+ currentGameStat.last,
2819
+ " \u2022 Plays ",
2820
+ currentGameStat.plays
2821
+ ] }) : statsLoading ? /* @__PURE__ */ jsx7("span", { className: "gamehub-stats-text nodrag", children: "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430\u2026" }) : /* @__PURE__ */ jsx7("span", { className: "gamehub-stats-text nodrag", children: "(\u043D\u0435\u0442 \u0434\u0430\u043D\u043D\u044B\u0445)" }),
2822
+ /* @__PURE__ */ jsx7(
2823
+ "button",
2824
+ {
2825
+ type: "button",
2826
+ className: "gamehub-top-btn nodrag",
2827
+ onClick: saveScore,
2828
+ title: "\u0421\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C \u0442\u0435\u043A\u0443\u0449\u0438\u0439 \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442",
2829
+ disabled: statsLoading,
2830
+ children: "\u{1F4BE} \u0421\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C"
2831
+ }
2832
+ )
2833
+ ] })
2834
+ ] }),
2835
+ /* @__PURE__ */ jsx7("div", { className: "gamehub-game-wrap nodrag", children: active?.component })
2836
+ ] }) }),
2837
+ /* @__PURE__ */ jsx7(Handle, { type: "source", position: Position.Bottom })
2838
+ ] });
2839
+ });
2840
+ var GameHubNode_default = GameHubNode;
2841
+ export {
2842
+ GameHubNode_default as GameHubNode
2843
+ };