Aleksmorshen commited on
Commit
3416830
·
verified ·
1 Parent(s): 798a123

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +569 -265
index.html CHANGED
@@ -3,319 +3,623 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
6
- <title>BabylonCraft Demo</title>
7
  <style>
8
- /* Базовый сброс стилей и стили для canvas */
9
  html, body {
10
- margin: 0;
11
- padding: 0;
12
- overflow: hidden; /* Убираем прокрутку */
13
  width: 100%;
14
  height: 100%;
15
- background-color: #000; /* Черный фон на случай проблем */
 
 
 
16
  }
17
 
18
  #renderCanvas {
19
  width: 100%;
20
  height: 100%;
21
- display: block; /* Убираем лишнее пространство под canvas */
22
- touch-action: none; /* Отключаем стандартные действия браузера при касании (важно для управления камерой) */
23
  outline: none; /* Убираем обводку при фокусе */
24
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  </style>
26
- <!-- Подключаем Babylon.js через CDN -->
27
  <script src="https://cdn.babylonjs.com/babylon.js"></script>
28
- <!-- Можно добавить библиотеки материалов, загрузчиков и т.д. при необходимости -->
29
- <!-- <script src="https://cdn.babylonjs.com/materialsLibrary/babylonjs.materials.min.js"></script> -->
30
- <!-- <script src="https://cdn.babylonjs.com/loaders/babylonjs.loaders.min.js"></script> -->
31
  </head>
32
  <body>
33
  <canvas id="renderCanvas"></canvas>
34
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  <script>
36
- // Получаем элемент canvas
37
  const canvas = document.getElementById('renderCanvas');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
 
39
- // Создаем движок Babylon.js
40
- const engine = new BABYLON.Engine(canvas, true, {
41
- preserveDrawingBuffer: true, // Необходимо для некоторых эффектов, может влиять на производительность
42
- stencil: true // Необходимо для некоторых эффектов
43
- });
44
-
45
- // --- Глобальные переменные и константы ---
46
- const BLOCK_SIZE = 1; // Размер блока
47
- const WORLD_WIDTH = 16; // Ширина мира в блоках
48
- const WORLD_DEPTH = 16; // Глубина мира в блоках
49
- const PLAYER_HEIGHT = 1.8; // Рост игрока
50
- const blockStorage = {}; // Объект для хранения созданных блоков { "x_y_z": blockMesh }
51
-
52
- // --- Материалы для блоков ---
53
- let grassMat, dirtMat, stoneMat;
54
-
55
- // Функция создания основной сцены
56
- const createScene = () => {
57
- const scene = new BABYLON.Scene(engine);
58
- scene.clearColor = new BABYLON.Color3(0.5, 0.8, 1.0); // Цвет неба (голубой)
59
- scene.collisionsEnabled = true; // Включаем обработку столкновений на сцене
60
- scene.gravity = new BABYLON.Vector3(0, -0.9, 0); // Глобальная гравитация (примерно как в Minecraft)
61
-
62
- // --- Камера от первого лица ---
63
- // Используем UniversalCamera для поддержки и мыши/клавиатуры, и сенсорного ввода
64
- const camera = new BABYLON.UniversalCamera("playerCamera", new BABYLON.Vector3(WORLD_WIDTH / 2, PLAYER_HEIGHT * 2, WORLD_DEPTH / 2), scene);
65
- camera.setTarget(new BABYLON.Vector3(0, PLAYER_HEIGHT, 0)); // Куда смотрит камера при старте
66
- camera.attachControl(canvas, true); // Привязываем управление к canvas
67
-
68
- // Настройки камеры
69
- camera.speed = 0.15; // Скорость перемещения
70
- camera.angularSensibility = 3000; // Чувствительность мыши/сенсора
71
- camera.inertia = 0.5; // Инерция при движении и вращении
72
-
73
- // Включаем гравитацию и столкновения для камеры
74
- camera.applyGravity = true;
75
- // Определяем размеры "тела" игрока для столкновений (эллипсоид)
76
- camera.ellipsoid = new BABYLON.Vector3(BLOCK_SIZE * 0.4, PLAYER_HEIGHT * 0.45, BLOCK_SIZE * 0.4);
77
- camera.ellipsoidOffset = new BABYLON.Vector3(0, PLAYER_HEIGHT * 0.45, 0); // Смещение эллипсоида, чтобы "ноги" были внизу
78
- camera.checkCollisions = true; // Камера будет проверять столкновения
79
-
80
- // Назначение клавиш управления (WASD)
81
- camera.keysUp.push(87); // W
82
- camera.keysDown.push(83); // S
83
- camera.keysLeft.push(65); // A
84
- camera.keysRight.push(68); // D
85
- // camera.keysUpward.push(32); // Пробел для прыжка (требует доп. логики или физического движка)
86
 
87
  // --- Освещение ---
88
- // Простое полусферическое освещение, имитирующее небо и землю
89
- const light = new BABYLON.HemisphericLight("hemiLight", new BABYLON.Vector3(0, 1, 0), scene);
90
  light.intensity = 0.8;
91
- light.diffuse = new BABYLON.Color3(1, 1, 1); // Цвет света сверху
92
- light.specular = new BABYLON.Color3(0.5, 0.5, 0.5); // Блики
93
- light.groundColor = new BABYLON.Color3(0.3, 0.3, 0.3); // Цвет света снизу
94
-
95
- // --- Создание материалов ---
96
- grassMat = new BABYLON.StandardMaterial("grassMat", scene);
97
- grassMat.diffuseColor = new BABYLON.Color3(0.3, 0.6, 0.2); // Зеленый
98
- grassMat.specularColor = new BABYLON.Color3(0.1, 0.1, 0.1); // Меньше бликов
99
-
100
- dirtMat = new BABYLON.StandardMaterial("dirtMat", scene);
101
- dirtMat.diffuseColor = new BABYLON.Color3(0.6, 0.4, 0.2); // Коричневый
102
- dirtMat.specularColor = new BABYLON.Color3(0.1, 0.1, 0.1);
103
-
104
- stoneMat = new BABYLON.StandardMaterial("stoneMat", scene);
105
- stoneMat.diffuseColor = new BABYLON.Color3(0.5, 0.5, 0.5); // Серый
106
- stoneMat.specularColor = new BABYLON.Color3(0.2, 0.2, 0.2);
107
-
108
- // --- Генерация простого мира ---
109
- generateFlatWorld(scene);
110
-
111
- // --- Логика взаимодействия (размещение/удаление блоков) ---
112
- scene.onPointerDown = (evt, pickInfo) => {
113
- // evt.button: 0 = левая, 1 = средняя, 2 = правая
114
- const isRightClick = evt.button === 2;
115
- const isLeftClick = evt.button === 0;
116
-
117
- if (pickInfo.hit) {
118
- const pickedMesh = pickInfo.pickedMesh;
119
-
120
- // --- Удаление блока ---
121
- // Левая кнопка мыши ИЛИ простое касание на мобильном, если попали в блок
122
- if (pickedMesh && pickedMesh.isBlock && isLeftClick) {
123
- removeBlock(pickedMesh.position.x, pickedMesh.position.y, pickedMesh.position.z);
124
- }
125
- // --- Размещение блока ---
126
- // Правая кнопка мыши ИЛИ простое касание на мобильном, если попали НЕ в блок (или в грань блока)
127
- else if (pickedMesh && (isRightClick || (!pickedMesh.isBlock && isLeftClick))) {
128
- // Рассчитываем позицию для нового блока
129
- // Берем точку попадания и сдвигаемся на половину блока по нормали к поверхности
130
- const normal = pickInfo.getNormal(true); // Получаем нормаль к поверхности в мировой системе координат
131
- const placePos = pickInfo.pickedPoint.add(normal.scale(BLOCK_SIZE / 2));
132
-
133
- // Выравниваем по сетке и центрируем
134
- const gridX = Math.floor(placePos.x / BLOCK_SIZE) * BLOCK_SIZE + BLOCK_SIZE / 2;
135
- const gridY = Math.floor(placePos.y / BLOCK_SIZE) * BLOCK_SIZE + BLOCK_SIZE / 2;
136
- const gridZ = Math.floor(placePos.z / BLOCK_SIZE) * BLOCK_SIZE + BLOCK_SIZE / 2;
137
-
138
- // Проверка, не пытаемся ли поставить блок внутри игрока
139
- if (!isPositionOccupiedByPlayer(gridX, gridY, gridZ, camera)) {
140
- // Сейчас всегда ставим камень
141
- createBlock(gridX, gridY, gridZ, stoneMat, scene);
142
- } else {
143
- console.log("Нельзя разместить блок внутри себя!");
144
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
  }
146
- }
147
  };
148
 
149
- // Блокировка курсора при клике на canvas для удобного управления мышью
150
- let isPointerLocked = false;
151
- scene.onPointerDown = (evt) => {
152
- if (!isPointerLocked && evt.button === 0) { // Блокируем только по левому клику
153
- canvas.requestPointerLock = canvas.requestPointerLock || canvas.msRequestPointerLock || canvas.mozRequestPointerLock || canvas.webkitRequestPointerLock;
154
- if (canvas.requestPointerLock) {
155
- canvas.requestPointerLock();
156
- }
157
- }
158
- // Вызываем предыдущую логику клика (размещение/удаление)
159
- handlePointerDown(evt, scene.pick(scene.pointerX, scene.pointerY));
160
  };
161
 
162
- // Освобождение курсора при нажатии Esc
163
- const pointerLockChange = () => {
164
- let controlEnabled = document.pointerLockElement || document.mozPointerLockElement || document.webkitPointerLockElement || document.msPointerLockElement || null;
165
- isPointerLocked = !!controlEnabled;
166
- // Если курсор разблокирован, останавливаем камеру (опционально)
167
- // if (!controlEnabled) {
168
- // camera.detachControl(canvas);
169
- // } else {
170
- // camera.attachControl(canvas, true);
171
- // }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
 
174
- document.addEventListener("pointerlockchange", pointerLockChange, false);
175
- document.addEventListener("mspointerlockchange", pointerLockChange, false);
176
- document.addEventListener("mozpointerlockchange", pointerLockChange, false);
177
- document.addEventListener("webkitpointerlockchange", pointerLockChange, false);
178
 
 
 
179
 
180
- return scene;
181
- };
182
 
183
- // --- Вспомогательные функции ---
 
 
 
184
 
185
- // Функция создания блока
186
- function createBlock(x, y, z, material, scene) {
187
- const blockId = `${Math.floor(x)}_${Math.floor(y)}_${Math.floor(z)}`;
 
 
 
 
 
188
 
189
- // Проверяем, нет ли уже блока в этой позиции
190
- if (blockStorage[blockId]) {
191
- // console.log(`Блок ${blockId} уже существует.`);
192
- return null;
193
- }
194
 
195
- // Создаем куб (box)
196
- const box = BABYLON.MeshBuilder.CreateBox("block_" + blockId, { size: BLOCK_SIZE }, scene);
197
- box.position = new BABYLON.Vector3(x, y, z);
198
- box.material = material;
199
- box.checkCollisions = true; // Включаем столкновения для блока
200
- box.isBlock = true; // Пользовательское свойство для идентификации
201
-
202
- // Добавляем блок в хранилище
203
- blockStorage[blockId] = box;
204
- // console.log(`Создан блок ${blockId} в ${x}, ${y}, ${z}`);
205
- return box;
206
- }
207
 
208
- // Функция удаления блока
209
- function removeBlock(x, y, z) {
210
- const blockId = `${Math.floor(x)}_${Math.floor(y)}_${Math.floor(z)}`;
211
- const blockToRemove = blockStorage[blockId];
212
 
213
- if (blockToRemove) {
214
- blockToRemove.dispose(); // Удаляем меш из сцены
215
- delete blockStorage[blockId]; // Удаляем из хранилища
216
- // console.log(`Удален блок ${blockId}`);
217
- } else {
218
- // console.log(`Блок ${blockId} не найден для удаления.`);
219
- }
220
  }
221
 
222
- // Функция генерации плоского мира
223
- function generateFlatWorld(scene) {
224
- const groundLevel = 0; // Уровень земли
225
- const dirtDepth = 3; // Глубина слоя грязи под травой
226
-
227
- for (let x = 0; x < WORLD_WIDTH; x++) {
228
- for (let z = 0; z < WORLD_DEPTH; z++) {
229
- // Верхний слой - трава
230
- createBlock(x + BLOCK_SIZE / 2, groundLevel + BLOCK_SIZE / 2, z + BLOCK_SIZE / 2, grassMat, scene);
231
- // Слои грязи под травой
232
- for (let y = 1; y <= dirtDepth; y++) {
233
- createBlock(x + BLOCK_SIZE / 2, groundLevel + BLOCK_SIZE / 2 - y * BLOCK_SIZE, z + BLOCK_SIZE / 2, dirtMat, scene);
234
- }
 
 
 
 
 
 
 
235
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
  }
237
- console.log(`Сгенерирован плоский мир ${WORLD_WIDTH}x${WORLD_DEPTH}`);
238
- }
239
 
240
- // Проверка, попадает ли позиция в эллипсоид игрока
241
- function isPositionOccupiedByPlayer(gridX, gridY, gridZ, camera) {
242
- const playerPos = camera.position;
243
- const playerEllipsoid = camera.ellipsoid;
244
- const playerOffset = camera.ellipsoidOffset;
245
-
246
- // Центр эллипсоида игрока
247
- const playerCenterY = playerPos.y + playerOffset.y;
248
-
249
- // Границы блока
250
- const blockMinX = gridX - BLOCK_SIZE / 2;
251
- const blockMaxX = gridX + BLOCK_SIZE / 2;
252
- const blockMinY = gridY - BLOCK_SIZE / 2;
253
- const blockMaxY = gridY + BLOCK_SIZE / 2;
254
- const blockMinZ = gridZ - BLOCK_SIZE / 2;
255
- const blockMaxZ = gridZ + BLOCK_SIZE / 2;
256
-
257
- // Границы эллипсоида игрока (приблизительно)
258
- const playerMinX = playerPos.x - playerEllipsoid.x;
259
- const playerMaxX = playerPos.x + playerEllipsoid.x;
260
- const playerMinY = playerCenterY - playerEllipsoid.y; // Используем центр эллипсоида
261
- const playerMaxY = playerCenterY + playerEllipsoid.y; // Используем центр эллипсоида
262
- const playerMinZ = playerPos.z - playerEllipsoid.z;
263
- const playerMaxZ = playerPos.z + playerEllipsoid.z;
264
-
265
- // Проверка на пересечение AABB (Axis-Aligned Bounding Box)
266
- const overlapX = playerMinX < blockMaxX && playerMaxX > blockMinX;
267
- const overlapY = playerMinY < blockMaxY && playerMaxY > blockMinY;
268
- const overlapZ = playerMinZ < blockMaxZ && playerMaxZ > blockMinZ;
269
-
270
- return overlapX && overlapY && overlapZ;
271
- }
272
 
273
- // Обработчик клика/касания (вынесен для работы с блокировкой курсора)
274
- function handlePointerDown(evt, pickInfo) {
275
- // evt.button: 0 = левая, 1 = средняя, 2 = правая
276
- const isRightClick = evt.button === 2;
277
- const isLeftClick = evt.button === 0;
278
-
279
- if (pickInfo && pickInfo.hit) {
280
- const pickedMesh = pickInfo.pickedMesh;
281
- const scene = pickedMesh.getScene(); // Получаем сцену из объекта
282
-
283
- // --- Удаление блока ---
284
- // Левая кнопка мыши ИЛИ простое касание на мобильном, если попали в блок
285
- if (pickedMesh && pickedMesh.isBlock && isLeftClick) {
286
- removeBlock(pickedMesh.position.x, pickedMesh.position.y, pickedMesh.position.z);
287
- }
288
- // --- Размещение блока ---
289
- // Правая кнопка мыши ИЛИ простое касание на мобильном, если попали НЕ в блок (или в грань блока)
290
- else if (pickedMesh && (isRightClick || (!pickedMesh.isBlock && isLeftClick))) {
291
- const normal = pickInfo.getNormal(true);
292
- const placePos = pickInfo.pickedPoint.add(normal.scale(BLOCK_SIZE / 2));
293
-
294
- const gridX = Math.floor(placePos.x / BLOCK_SIZE) * BLOCK_SIZE + BLOCK_SIZE / 2;
295
- const gridY = Math.floor(placePos.y / BLOCK_SIZE) * BLOCK_SIZE + BLOCK_SIZE / 2;
296
- const gridZ = Math.floor(placePos.z / BLOCK_SIZE) * BLOCK_SIZE + BLOCK_SIZE / 2;
297
-
298
- if (!isPositionOccupiedByPlayer(gridX, gridY, gridZ, scene.activeCamera)) {
299
- createBlock(gridX, gridY, gridZ, stoneMat, scene);
300
- } else {
301
- console.log("Нельзя разместить блок внутри себя!");
302
- }
303
- }
304
- }
305
- }
306
 
 
 
 
 
 
 
 
 
307
 
308
- // --- Запуск ---
309
- const scene = createScene(); // Создаем сцену
310
 
311
- // Главный цикл рендеринга
312
- engine.runRenderLoop(() => {
313
- scene.render();
314
- });
315
 
316
- // Обработчик изменения размера окна/экрана
317
- window.addEventListener('resize', () => {
318
- engine.resize();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
319
  });
320
 
321
  </script>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
6
+ <title>MiniCraft 3D (Babylon.js Demo)</title>
7
  <style>
 
8
  html, body {
9
+ overflow: hidden;
 
 
10
  width: 100%;
11
  height: 100%;
12
+ margin: 0;
13
+ padding: 0;
14
+ background-color: #000; /* Фон за канвасом */
15
+ touch-action: none; /* Отключаем стандартные действия браузера на касания */
16
  }
17
 
18
  #renderCanvas {
19
  width: 100%;
20
  height: 100%;
21
+ touch-action: none;
 
22
  outline: none; /* Убираем обводку при фокусе */
23
  }
24
+
25
+ /* --- Стили для мобильных контролов --- */
26
+ #controlsOverlay {
27
+ position: absolute;
28
+ bottom: 0;
29
+ left: 0;
30
+ width: 100%;
31
+ height: 150px; /* Высота зоны контролов */
32
+ display: flex;
33
+ justify-content: space-between;
34
+ align-items: center;
35
+ pointer-events: none; /* Позволяет кликать "сквозь" оверлей на канвас */
36
+ padding: 10px;
37
+ box-sizing: border-box;
38
+ z-index: 10;
39
+ }
40
+
41
+ #joystickZone {
42
+ position: absolute;
43
+ bottom: 20px;
44
+ left: 20px;
45
+ width: 120px;
46
+ height: 120px;
47
+ background-color: rgba(128, 128, 128, 0.3);
48
+ border-radius: 50%;
49
+ pointer-events: all; /* Включаем события для джойстика */
50
+ }
51
+ #joystickThumb {
52
+ position: absolute;
53
+ width: 60px;
54
+ height: 60px;
55
+ background-color: rgba(200, 200, 200, 0.5);
56
+ border-radius: 50%;
57
+ left: 50%;
58
+ top: 50%;
59
+ transform: translate(-50%, -50%);
60
+ pointer-events: none; /* Сам "палец" не должен ловить события */
61
+ }
62
+
63
+
64
+ #actionButtons {
65
+ position: absolute;
66
+ bottom: 20px;
67
+ right: 20px;
68
+ display: flex;
69
+ flex-direction: column;
70
+ align-items: center;
71
+ gap: 15px; /* Расстояние между кнопками */
72
+ pointer-events: all; /* Включаем события для кнопок */
73
+ }
74
+
75
+ .actionButton {
76
+ width: 60px;
77
+ height: 60px;
78
+ background-color: rgba(100, 100, 255, 0.6);
79
+ border: 2px solid rgba(200, 200, 255, 0.8);
80
+ border-radius: 15px;
81
+ color: white;
82
+ font-size: 12px;
83
+ font-weight: bold;
84
+ display: flex;
85
+ justify-content: center;
86
+ align-items: center;
87
+ text-align: center;
88
+ user-select: none; /* Запрещаем выделение текста */
89
+ pointer-events: all;
90
+ line-height: 1.1; /* Для переноса строк */
91
+ box-shadow: 0 4px 6px rgba(0,0,0,0.3);
92
+ transition: background-color 0.1s ease;
93
+ }
94
+ .actionButton:active {
95
+ background-color: rgba(80, 80, 200, 0.8);
96
+ transform: scale(0.95);
97
+ }
98
+
99
  </style>
100
+ <!-- Подключаем Babylon.js и GUI -->
101
  <script src="https://cdn.babylonjs.com/babylon.js"></script>
102
+ <script src="https://cdn.babylonjs.com/gui/babylon.gui.min.js"></script>
 
 
103
  </head>
104
  <body>
105
  <canvas id="renderCanvas"></canvas>
106
 
107
+ <!-- Оверлей для мобильных контролов -->
108
+ <div id="controlsOverlay">
109
+ <div id="joystickZone">
110
+ <div id="joystickThumb"></div>
111
+ </div>
112
+ <div id="actionButtons">
113
+ <button id="jumpButton" class="actionButton">Прыжок</button>
114
+ <button id="placeButton" class="actionButton">Пост.</button>
115
+ <button id="breakButton" class="actionButton">Слом.</button>
116
+ </div>
117
+ </div>
118
+
119
  <script>
120
+ // Получаем элементы DOM
121
  const canvas = document.getElementById('renderCanvas');
122
+ const joystickZone = document.getElementById('joystickZone');
123
+ const joystickThumb = document.getElementById('joystickThumb');
124
+ const jumpButton = document.getElementById('jumpButton');
125
+ const placeButton = document.getElementById('placeButton');
126
+ const breakButton = document.getElementById('breakButton');
127
+
128
+ // --- Настройки ---
129
+ const PLAYER_HEIGHT = 1.8;
130
+ const PLAYER_RADIUS = 0.4;
131
+ const PLAYER_SPEED = 0.15;
132
+ const JUMP_FORCE = 0.25; // Сила прыжка
133
+ const GRAVITY = -0.05; // Гравитация (должна быть отрицательной)
134
+ const BLOCK_SIZE = 1;
135
+ const REACH_DISTANCE = 5; // Дальность взаимодействия с блоками
136
+ const SENSITIVITY = 0.003; // Чувствительность мыши/тачпада
137
+ const JOYSTICK_SENSITIVITY = 0.05; // Чувствительность джойстика
138
+
139
+ // --- Глобальные переменные ---
140
+ let engine;
141
+ let scene;
142
+ let camera;
143
+ let ground;
144
+ let selectedBlockMaterial; // Материал для предпросмотра блока
145
+ let previewBlock; // Меш для предпросмотра
146
+
147
+ // Переменные для управления
148
+ let inputMap = {}; // Для отслеживания нажатых клавиш
149
+ let verticalCameraVelocity = 0; // Вертикальная скорость для прыжка/падения
150
+ let isGrounded = false; // Находится ли игрок на земле
151
+
152
+ // Переменные для мобильного управления
153
+ let joystickActive = false;
154
+ let joystickStartX = 0;
155
+ let joystickStartY = 0;
156
+ let joystickCurrentX = 0;
157
+ let joystickCurrentY = 0;
158
+ let moveForward = 0;
159
+ let moveStrafe = 0; // 0 = нет, 1 = вправо, -1 = влево
160
+ let touchLookActive = false;
161
+ let touchLookStartX = 0;
162
+ let touchLookStartY = 0;
163
+ let lastTouchX = 0;
164
+ let lastTouchY = 0;
165
+
166
+ let placeAction = false;
167
+ let breakAction = false;
168
+ let jumpAction = false;
169
+
170
+ // --- Инициализация Babylon.js ---
171
+ async function initGame() {
172
+ engine = new BABYLON.Engine(canvas, true, { preserveDrawingBuffer: true, stencil: true });
173
+ scene = new BABYLON.Scene(engine);
174
+ scene.collisionsEnabled = true; // Включаем коллизии в сцене
175
+ scene.gravity = new BABYLON.Vector3(0, GRAVITY, 0); // Устанавливаем гравитацию
176
+
177
+ // --- Камера (Игрок) ---
178
+ camera = new BABYLON.FreeCamera("playerCamera", new BABYLON.Vector3(0, PLAYER_HEIGHT + 5, -10), scene);
179
+ camera.attachControl(canvas, true); // Привязываем управление к канвасу (для мыши/тачпада)
180
+ camera.applyGravity = true; // Включаем гравитацию для камеры
181
+ camera.checkCollisions = true; // Включаем коллизии для камеры
182
+ // Определяем форму игрока для коллизий (эллипсоид)
183
+ camera.ellipsoid = new BABYLON.Vector3(PLAYER_RADIUS, PLAYER_HEIGHT / 2, PLAYER_RADIUS);
184
+ camera.ellipsoidOffset = new BABYLON.Vector3(0, PLAYER_HEIGHT / 2, 0); // Смещение центра эллипсоида
185
+ camera.minZ = 0.1; // Ближняя плоскость отсечения
186
+ camera.speed = PLAYER_SPEED; // Используем для клавиатуры
187
+ camera.angularSensibility = 1 / SENSITIVITY / 10; // Инвертированная чувствительность для мыши
188
+
189
+ // Убираем стандартное управление клавишами WASD и стрелками, будем обрабатывать сами
190
+ camera.keysUp = [];
191
+ camera.keysDown = [];
192
+ camera.keysLeft = [];
193
+ camera.keysRight = [];
194
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
195
 
196
  // --- Освещение ---
197
+ const light = new BABYLON.HemisphericLight("light1", new BABYLON.Vector3(0, 1, 0), scene);
 
198
  light.intensity = 0.8;
199
+
200
+ // --- Небо ---
201
+ const skybox = BABYLON.MeshBuilder.CreateBox("skyBox", { size: 1000.0 }, scene);
202
+ const skyboxMaterial = new BABYLON.StandardMaterial("skyBoxMat", scene);
203
+ skyboxMaterial.backFaceCulling = false;
204
+ // Простые цвета для неба (вместо текстур для простоты)
205
+ skyboxMaterial.diffuseColor = new BABYLON.Color3(0.5, 0.8, 1.0); // Голубой цвет
206
+ skyboxMaterial.specularColor = new BABYLON.Color3(0, 0, 0); // Убираем блики
207
+ skybox.material = skyboxMaterial;
208
+ skybox.infiniteDistance = true; // Небо всегда на фоне
209
+
210
+ // --- Земля ---
211
+ ground = BABYLON.MeshBuilder.CreateGround("ground", { width: 100, height: 100 }, scene);
212
+ const groundMaterial = new BABYLON.StandardMaterial("groundMat", scene);
213
+ groundMaterial.diffuseColor = new BABYLON.Color3(0.4, 0.6, 0.2); // Зеленый цвет
214
+ groundMaterial.specularColor = new BABYLON.Color3(0.1, 0.1, 0.1);
215
+ ground.material = groundMaterial;
216
+ ground.checkCollisions = true; // Включаем коллизии для земли
217
+
218
+ // --- Материалы для блоков ---
219
+ const grassMat = new BABYLON.StandardMaterial("grassMat", scene);
220
+ grassMat.diffuseColor = new BABYLON.Color3(0.4, 0.6, 0.2); // Трава
221
+
222
+ const dirtMat = new BABYLON.StandardMaterial("dirtMat", scene);
223
+ dirtMat.diffuseColor = new BABYLON.Color3(0.6, 0.4, 0.2); // Земля
224
+
225
+ const stoneMat = new BABYLON.StandardMaterial("stoneMat", scene);
226
+ stoneMat.diffuseColor = new BABYLON.Color3(0.5, 0.5, 0.5); // Камень
227
+
228
+ selectedBlockMaterial = stoneMat; // По умолчанию ставим камень
229
+
230
+ // --- Материал для предпросмотра ---
231
+ const previewMat = new BABYLON.StandardMaterial("previewMat", scene);
232
+ previewMat.diffuseColor = new BABYLON.Color3(1, 1, 1); // Белый
233
+ previewMat.alpha = 0.4; // Полупрозрачный
234
+
235
+ // --- Предпросмотр блока ---
236
+ previewBlock = BABYLON.MeshBuilder.CreateBox("previewBlock", { size: BLOCK_SIZE * 1.01 }, scene); // Чуть больше блока
237
+ previewBlock.material = previewMat;
238
+ previewBlock.isPickable = false; // Нельзя выбрать лучом
239
+ previewBlock.setEnabled(false); // Изначально скрыт
240
+
241
+
242
+ // --- Создание начал��ных блоков (простой пример) ---
243
+ createBlock(0, 0.5, 5, grassMat);
244
+ createBlock(1, 0.5, 5, dirtMat);
245
+ createBlock(-1, 0.5, 5, stoneMat);
246
+ createBlock(0, 1.5, 5, stoneMat); // Блок сверху
247
+
248
+ // --- Обработка ввода ---
249
+ setupInputHandling();
250
+ setupMobileControls();
251
+
252
+ // --- Основной цикл игры ---
253
+ engine.runRenderLoop(() => {
254
+ updatePlayerMovement();
255
+ updateBlockInteraction();
256
+ scene.render();
257
+ });
258
+
259
+ // --- Обработка изменения размера окна ---
260
+ window.addEventListener('resize', () => {
261
+ engine.resize();
262
+ });
263
+ }
264
+
265
+ // --- Функция создания блока ---
266
+ function createBlock(x, y, z, material) {
267
+ const block = BABYLON.MeshBuilder.CreateBox(`block_${x}_${y}_${z}`, { size: BLOCK_SIZE }, scene);
268
+ block.position = new BABYLON.Vector3(x, y, z);
269
+ block.material = material;
270
+ block.checkCollisions = true;
271
+ block.isPickable = true; // Делаем блоки выбираемыми лучом
272
+ block.metadata = { type: "block" }; // Для идентификации
273
+ return block;
274
+ }
275
+
276
+ // --- Настройка обработки ввода (Клавиатура) ---
277
+ function setupInputHandling() {
278
+ scene.onPointerDown = (evt, pickResult) => {
279
+ // 0 = ЛКМ (ломать), 2 = ПКМ (ставить)
280
+ if (evt.button === 0) breakAction = true;
281
+ if (evt.button === 2) placeAction = true;
282
+
283
+ // Если клик не по GUI элементам (для десктопа важно)
284
+ if (!BABYLON.GUI.AdvancedDynamicTexture.IsBabylonGUIElement(evt.target)) {
285
+ if (!engine.isPointerLock) {
286
+ engine.enterPointerlock();
287
  }
288
+ }
289
  };
290
 
291
+ scene.onPointerUp = (evt) => {
292
+ if (evt.button === 0) breakAction = false;
293
+ if (evt.button === 2) placeAction = false;
 
 
 
 
 
 
 
 
294
  };
295
 
296
+
297
+ // Отслеживание нажатых клавиш
298
+ window.addEventListener("keydown", (event) => {
299
+ inputMap[event.key.toLowerCase()] = true;
300
+ if (event.key === " ") { // Прыжок на пробел
301
+ jumpAction = true;
302
+ event.preventDefault(); // Предотвращаем прокрутку страницы
303
+ }
304
+ });
305
+ window.addEventListener("keyup", (event) => {
306
+ inputMap[event.key.toLowerCase()] = false;
307
+ if (event.key === " ") {
308
+ jumpAction = false;
309
+ }
310
+ });
311
+
312
+ // Выход из Pointer Lock по Esc
313
+ document.addEventListener("pointerlockchange", () => {
314
+ if (document.pointerLockElement !== canvas) {
315
+ engine.exitPointerlock();
316
+ // Очистить inputMap, чтобы персонаж не продолжал бежать
317
+ inputMap = {};
318
+ moveForward = 0;
319
+ moveStrafe = 0;
320
+ }
321
+ }, false);
322
+ }
323
+
324
+ // --- Настройка мобильных контролов ---
325
+ function setupMobileControls() {
326
+
327
+ // --- Джойстик ---
328
+ const joystickRadius = joystickZone.offsetWidth / 2;
329
+ const thumbRadius = joystickThumb.offsetWidth / 2;
330
+ const maxThumbOffset = joystickRadius - thumbRadius;
331
+
332
+ joystickZone.addEventListener('pointerdown', (e) => {
333
+ if (!joystickActive) {
334
+ joystickActive = true;
335
+ joystickStartX = e.clientX;
336
+ joystickStartY = e.clientY;
337
+ joystickZone.style.backgroundColor = 'rgba(128, 128, 128, 0.5)'; // Активный цвет
338
+ // Предотвращаем скролл страницы при движении пальца
339
+ e.preventDefault();
340
+ joystickZone.setPointerCapture(e.pointerId); // Захватываем указатель
341
+ }
342
+ }, { passive: false }); // passive: false нужен для preventDefault
343
+
344
+ joystickZone.addEventListener('pointermove', (e) => {
345
+ if (joystickActive) {
346
+ joystickCurrentX = e.clientX - joystickStartX;
347
+ joystickCurrentY = e.clientY - joystickStartY;
348
+
349
+ let distance = Math.sqrt(joystickCurrentX * joystickCurrentX + joystickCurrentY * joystickCurrentY);
350
+ let angle = Math.atan2(joystickCurrentY, joystickCurrentX);
351
+
352
+ // Ограничиваем смещение "пальца" джойстика
353
+ let thumbX = Math.cos(angle) * Math.min(distance, maxThumbOffset);
354
+ let thumbY = Math.sin(angle) * Math.min(distance, maxThumbOffset);
355
+
356
+ joystickThumb.style.left = `${joystickRadius + thumbX}px`;
357
+ joystickThumb.style.top = `${joystickRadius + thumbY}px`;
358
+
359
+
360
+ // Преобразуем смещение в движение камеры
361
+ // Нормализуем вектор смещения
362
+ let normalizedX = joystickCurrentX / joystickRadius;
363
+ let normalizedY = joystickCurrentY / joystickRadius; // Y инвертирован (вверх = отрицательный Y)
364
+
365
+ // Ограничиваем значения [-1, 1]
366
+ normalizedX = Math.max(-1, Math.min(1, normalizedX));
367
+ normalizedY = Math.max(-1, Math.min(1, normalizedY));
368
+
369
+ // Устанавливаем движение (Y - вперед/назад, X - влево/вправо)
370
+ moveForward = -normalizedY; // Инвертируем Y
371
+ moveStrafe = normalizedX;
372
+
373
+ // Добавляем "мертвую зону"
374
+ if (Math.abs(moveForward) < 0.1) moveForward = 0;
375
+ if (Math.abs(moveStrafe) < 0.1) moveStrafe = 0;
376
+ }
377
+ });
378
+
379
+ const onJoystickEnd = (e) => {
380
+ if (joystickActive && e.pointerId === e.target.pointerId) { // Убедимся, что это тот же палец
381
+ joystickActive = false;
382
+ moveForward = 0;
383
+ moveStrafe = 0;
384
+ joystickThumb.style.left = '50%';
385
+ joystickThumb.style.top = '50%';
386
+ joystickZone.style.backgroundColor = 'rgba(128, 128, 128, 0.3)'; // Неактивный цвет
387
+ joystickZone.releasePointerCapture(e.pointerId); // Освобождаем указатель
388
+ }
389
  };
390
+ joystickZone.addEventListener('pointerup', onJoystickEnd);
391
+ joystickZone.addEventListener('pointercancel', onJoystickEnd); // На случай отмены касания
392
+
393
+
394
+ // --- Поворот камеры касанием (правая часть экрана) ---
395
+ canvas.addEventListener('pointerdown', (e) => {
396
+ // Игнорировать касания на джойстике и кнопках
397
+ if (e.target === joystickZone || e.target.closest('#actionButtons')) return;
398
+ if (!touchLookActive && !joystickActive && document.pointerLockElement !== canvas) { // Активируем поворот только если не активен джойстик и не заблокирован курсор
399
+ touchLookActive = true;
400
+ touchLookStartX = e.clientX;
401
+ touchLookStartY = e.clientY;
402
+ lastTouchX = e.clientX;
403
+ lastTouchY = e.clientY;
404
+ canvas.setPointerCapture(e.pointerId); // Захватываем указатель на канвасе
405
+ e.preventDefault();
406
+ }
407
+ }, { passive: false });
408
 
409
+ canvas.addEventListener('pointermove', (e) => {
410
+ if (touchLookActive && e.pointerId === e.target.pointerId) {
411
+ let deltaX = e.clientX - lastTouchX;
412
+ let deltaY = e.clientY - lastTouchY;
413
 
414
+ camera.cameraRotation.y += deltaX * SENSITIVITY * 0.5; // Уменьшаем чувствительность для тачпада
415
+ camera.cameraRotation.x += deltaY * SENSITIVITY * 0.5;
416
 
417
+ // Ограничиваем вертикальный обзор
418
+ camera.cameraRotation.x = Math.max(-Math.PI / 2 + 0.01, Math.min(Math.PI / 2 - 0.01, camera.cameraRotation.x));
419
 
420
+ lastTouchX = e.clientX;
421
+ lastTouchY = e.clientY;
422
+ }
423
+ });
424
 
425
+ const onTouchLookEnd = (e) => {
426
+ if (touchLookActive && e.pointerId === e.target.pointerId) {
427
+ touchLookActive = false;
428
+ canvas.releasePointerCapture(e.pointerId);
429
+ }
430
+ };
431
+ canvas.addEventListener('pointerup', onTouchLookEnd);
432
+ canvas.addEventListener('pointercancel', onTouchLookEnd);
433
 
 
 
 
 
 
434
 
435
+ // --- Кнопки действий ---
436
+ jumpButton.addEventListener('pointerdown', (e) => { jumpAction = true; e.preventDefault(); e.stopPropagation(); });
437
+ jumpButton.addEventListener('pointerup', (e) => { jumpAction = false; });
438
+ jumpButton.addEventListener('pointerleave', (e) => { jumpAction = false; }); // Если палец ушел с кнопки
 
 
 
 
 
 
 
 
439
 
440
+ placeButton.addEventListener('pointerdown', (e) => { placeAction = true; e.preventDefault(); e.stopPropagation(); });
441
+ placeButton.addEventListener('pointerup', (e) => { placeAction = false; });
442
+ placeButton.addEventListener('pointerleave', (e) => { placeAction = false; });
 
443
 
444
+ breakButton.addEventListener('pointerdown', (e) => { breakAction = true; e.preventDefault(); e.stopPropagation(); });
445
+ breakButton.addEventListener('pointerup', (e) => { breakAction = false; });
446
+ breakButton.addEventListener('pointerleave', (e) => { breakAction = false; });
 
 
 
 
447
  }
448
 
449
+
450
+ // --- Обновление движения игрока ---
451
+ function updatePlayerMovement() {
452
+ const forward = new BABYLON.Vector3(
453
+ Math.sin(camera.rotation.y),
454
+ 0,
455
+ Math.cos(camera.rotation.y)
456
+ );
457
+ const right = new BABYLON.Vector3(
458
+ Math.sin(camera.rotation.y + Math.PI / 2),
459
+ 0,
460
+ Math.cos(camera.rotation.y + Math.PI / 2)
461
+ );
462
+ const moveDirection = BABYLON.Vector3.Zero();
463
+
464
+
465
+ // Клавиатурный ввод (приоритет если залочен курсор)
466
+ if (document.pointerLockElement === canvas) {
467
+ if (inputMap["w"] || inputMap["arrowup"]) {
468
+ moveDirection.addInPlace(forward);
469
  }
470
+ if (inputMap["s"] || inputMap["arrowdown"]) {
471
+ moveDirection.addInPlace(forward.scale(-1));
472
+ }
473
+ if (inputMap["a"] || inputMap["arrowleft"]) {
474
+ moveDirection.addInPlace(right.scale(-1));
475
+ }
476
+ if (inputMap["d"] || inputMap["arrowright"]) {
477
+ moveDirection.addInPlace(right);
478
+ }
479
+ } else { // Мобильный ввод
480
+ if (moveForward > 0.1) { // Порог чувствительности
481
+ moveDirection.addInPlace(forward.scale(moveForward));
482
+ } else if (moveForward < -0.1) {
483
+ moveDirection.addInPlace(forward.scale(moveForward)); // Он уже отрицательный
484
+ }
485
+ if (moveStrafe > 0.1) {
486
+ moveDirection.addInPlace(right.scale(moveStrafe));
487
+ } else if (moveStrafe < -0.1) {
488
+ moveDirection.addInPlace(right.scale(moveStrafe)); // Он уже отрицательный
489
+ }
490
+ }
491
+
492
+
493
+ // Нормализация диагонального движения
494
+ if (moveDirection.lengthSquared() > 0) {
495
+ moveDirection.normalize();
496
  }
 
 
497
 
498
+ // Применяем движение
499
+ const finalMove = moveDirection.scale(PLAYER_SPEED);
500
+ camera.cameraDirection.addInPlace(new BABYLON.Vector3(finalMove.x, 0, finalMove.z)); // Используем cameraDirection для учета гравитации/коллизий
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
501
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
502
 
503
+ // --- Проверка нахождения на земле ---
504
+ // Кидаем короткий луч вниз из-под ног камеры
505
+ const rayStart = camera.position.clone();
506
+ rayStart.y -= (PLAYER_HEIGHT / 2) - 0.1; // Чуть ниже центра эллипсоида
507
+ const rayEnd = rayStart.clone();
508
+ rayEnd.y -= 0.3; // Короткий луч вниз
509
+ const groundRay = new BABYLON.Ray(rayStart, rayEnd.subtract(rayStart).normalize(), 0.3);
510
+ // let rayHelper = new BABYLON.RayHelper(groundRay); rayHelper.show(scene); // Для отладки
511
 
512
+ const groundHit = scene.pickWithRay(groundRay, (mesh) => mesh.isPickable && mesh.checkCollisions);
513
+ isGrounded = groundHit.hit;
514
 
 
 
 
 
515
 
516
+ // --- Прыжок ---
517
+ if (jumpAction && isGrounded) {
518
+ // Простой способ: задаем вертикальную скорость камере
519
+ // Babylon.js FreeCamera с applyGravity не имеет прямого velocity,
520
+ // поэтому имитируем прыжок, добавляя смещение к cameraDirection
521
+ camera.cameraDirection.y = JUMP_FORCE; // Применить силу прыжка
522
+ isGrounded = false; // Сразу считаем, что не на земле
523
+ jumpAction = false; // Сбрасываем флаг, чтобы не прыгать постоянно
524
+ }
525
+
526
+ // Сбрасываем флаги действий, если они были одноразовыми (как клик мыши)
527
+ // Для кнопок они сбросятся при pointerup/leave
528
+ if (!placeButton.matches(':active')) placeAction = false; // Проверка для десктопа
529
+ if (!breakButton.matches(':active')) breakAction = false; // Проверка для десктопа
530
+
531
+ }
532
+
533
+ // --- Обновление взаимодействия с блоками ---
534
+ function updateBlockInteraction() {
535
+ const ray = scene.createPickingRay(
536
+ engine.getRenderWidth() / 2,
537
+ engine.getRenderHeight() / 2,
538
+ null,
539
+ camera
540
+ );
541
+
542
+ const hit = scene.pickWithRay(ray, (mesh) => mesh.metadata?.type === "block", true); // Ищем только блоки
543
+
544
+ previewBlock.setEnabled(false); // Скрываем предпросмотр по умолчанию
545
+
546
+ if (hit.hit && hit.pickedMesh && hit.distance <= REACH_DISTANCE) {
547
+ const pickedBlock = hit.pickedMesh;
548
+ const normal = hit.getNormal(true); // Нормаль к грани
549
+
550
+ // --- Позиция для предпросмотра/нового блока ---
551
+ // Сначала округляем точку попадания, чтобы она была ближе к центру грани
552
+ const roundedHitPoint = new BABYLON.Vector3(
553
+ Math.round(hit.pickedPoint.x / BLOCK_SIZE) * BLOCK_SIZE,
554
+ Math.round(hit.pickedPoint.y / BLOCK_SIZE) * BLOCK_SIZE,
555
+ Math.round(hit.pickedPoint.z / BLOCK_SIZE) * BLOCK_SIZE
556
+ );
557
+
558
+ // Затем добавляем нормаль, умноженную на половину размера блока,
559
+ // чтобы вычислить центр соседней ячейки
560
+ const potentialNewBlockPos = pickedBlock.position.add(normal.scale(BLOCK_SIZE));
561
+
562
+ // Выравниваем позицию по сетке
563
+ const newBlockPos = new BABYLON.Vector3(
564
+ Math.round(potentialNewBlockPos.x / BLOCK_SIZE) * BLOCK_SIZE,
565
+ Math.round(potentialNewBlockPos.y / BLOCK_SIZE) * BLOCK_SIZE,
566
+ Math.round(potentialNewBlockPos.z / BLOCK_SIZE) * BLOCK_SIZE
567
+ );
568
+ // Корректируем Y, чтобы он был кратен 0.5*BLOCK_SIZE (если блоки могут стоять на пол-блока)
569
+ // В нашем случае просто округляем до ближайшего центра блока
570
+ newBlockPos.y = Math.round(potentialNewBlockPos.y / (BLOCK_SIZE / 2)) * (BLOCK_SIZE / 2); // Выравнивание по центрам блоков
571
+
572
+ // --- Предпросмотр установки блока ---
573
+ previewBlock.position = newBlockPos;
574
+ previewBlock.setEnabled(true);
575
+
576
+ // --- Ломание блока ---
577
+ if (breakAction) {
578
+ if(pickedBlock !== ground) { // Не ломаем землю
579
+ pickedBlock.dispose();
580
+ }
581
+ breakAction = false; // Обрабатываем однократно
582
+ }
583
+
584
+ // --- Установка блока ---
585
+ if (placeAction) {
586
+ // Проверяем, не пытаемся ли поставить блок внутри игрока
587
+ const playerBox = new BABYLON.BoundingBox(
588
+ camera.position.subtract(camera.ellipsoid),
589
+ camera.position.add(camera.ellipsoid)
590
+ );
591
+ const newBlockBox = new BABYLON.BoundingBox(
592
+ newBlockPos.subtract(new BABYLON.Vector3(BLOCK_SIZE/2, BLOCK_SIZE/2, BLOCK_SIZE/2)),
593
+ newBlockPos.add(new BABYLON.Vector3(BLOCK_SIZE/2, BLOCK_SIZE/2, BLOCK_SIZE/2))
594
+ );
595
+
596
+ // Проверяем, есть ли уже блок на этом месте
597
+ let collisionFound = false;
598
+ scene.meshes.forEach(mesh => {
599
+ if (mesh.metadata?.type === "block" && mesh.position.equals(newBlockPos)) {
600
+ collisionFound = true;
601
+ }
602
+ });
603
+
604
+
605
+ if (!playerBox.intersects(newBlockBox) && !collisionFound) {
606
+ createBlock(newBlockPos.x, newBlockPos.y, newBlockPos.z, selectedBlockMaterial);
607
+ }
608
+ placeAction = false; // Обрабатываем однократно
609
+ }
610
+
611
+ } else {
612
+ // Если луч ни во что не попал в пределах досягаемости
613
+ previewBlock.setEnabled(false);
614
+ // Сбрасываем действия, если кнопка отпущена (важно для мобильных)
615
+ // breakAction = false;
616
+ // placeAction = false;
617
+ }
618
+ }
619
+
620
+ // --- Запуск игры после загрузки DOM ---
621
+ document.addEventListener("DOMContentLoaded", () => {
622
+ initGame().catch(error => console.error("Initialization failed:", error));
623
  });
624
 
625
  </script>