Aleksmorshen commited on
Commit
324e9b3
·
verified ·
1 Parent(s): 3416830

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +463 -506
index.html CHANGED
@@ -2,624 +2,581 @@
2
  <html lang="ru">
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>
 
2
  <html lang="ru">
3
  <head>
4
  <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
6
+ <title>MiniCraft 3D Demo</title>
7
+ <script src="https://cdn.babylonjs.com/babylon.js"></script>
8
+ <!-- Для работы Pointer событий на всех устройствах (важно для тачскрина) -->
9
+ <script src="https://code.jquery.com/pep/0.4.3/pep.js"></script>
10
  <style>
11
  html, body {
 
12
  width: 100%;
13
  height: 100%;
14
  margin: 0;
15
  padding: 0;
16
+ overflow: hidden;
17
+ background-color: #87CEEB; /* Цвет неба по умолчанию */
18
+ font-family: sans-serif;
19
  }
 
20
  #renderCanvas {
21
  width: 100%;
22
  height: 100%;
23
+ touch-action: none; /* Отключаем стандартные жесты браузера на канвасе */
 
24
  }
25
+ #crosshair {
 
 
26
  position: absolute;
27
+ top: 50%;
28
+ left: 50%;
29
+ width: 10px;
30
+ height: 10px;
31
+ border: 1px solid white;
32
+ background-color: rgba(0, 0, 0, 0.5);
33
+ transform: translate(-50%, -50%);
34
+ pointer-events: none; /* Не мешает кликам по канвасу */
 
 
35
  z-index: 10;
36
  }
37
+ #controls {
 
38
  position: absolute;
39
+ bottom: 10px;
40
+ left: 10px;
41
+ right: 10px;
42
+ display: flex;
43
+ justify-content: space-between;
44
+ align-items: flex-end;
45
+ pointer-events: none; /* Сам контейнер не ловит события */
46
+ z-index: 5;
47
+ }
48
+ .joystick-area {
49
  width: 120px;
50
  height: 120px;
51
+ background-color: rgba(200, 200, 200, 0.4);
52
  border-radius: 50%;
53
+ position: relative;
54
+ pointer-events: auto; /* Область джойстика ловит события */
55
  }
56
+ #joystick {
57
+ width: 50px;
58
+ height: 50px;
59
+ background-color: rgba(100, 100, 100, 0.7);
 
60
  border-radius: 50%;
61
+ position: absolute;
62
  top: 50%;
63
+ left: 50%;
64
  transform: translate(-50%, -50%);
65
+ cursor: grab;
66
+ }
 
 
67
  #actionButtons {
 
 
 
68
  display: flex;
69
+ gap: 15px;
70
+ pointer-events: auto; /* Кнопки ловят события */
 
 
71
  }
72
+ .action-button {
 
73
  width: 60px;
74
  height: 60px;
75
+ border: 2px solid rgba(50, 50, 50, 0.7);
76
+ background-color: rgba(200, 200, 200, 0.5);
77
+ border-radius: 10px;
 
78
  font-size: 12px;
79
  font-weight: bold;
80
  display: flex;
81
  justify-content: center;
82
  align-items: center;
83
  text-align: center;
84
+ cursor: pointer;
85
+ user-select: none; /* Запретить выделение текста на кнопке */
 
 
 
86
  }
87
+ .action-button:active {
88
+ background-color: rgba(150, 150, 150, 0.7);
 
89
  }
90
+ /* Скрываем мобильные контролы на десктопе (ширина > 800px) */
91
+ /* @media (min-width: 801px) {
92
+ #controls {
93
+ display: none;
94
+ }
95
+ } */ /* Пока оставим видимыми для теста */
96
 
97
+ #blockSelector {
98
+ position: absolute;
99
+ top: 10px;
100
+ left: 50%;
101
+ transform: translateX(-50%);
102
+ background-color: rgba(0,0,0,0.3);
103
+ padding: 5px;
104
+ border-radius: 5px;
105
+ display: flex;
106
+ gap: 5px;
107
+ z-index: 15;
108
+ }
109
+ .block-option {
110
+ width: 40px;
111
+ height: 40px;
112
+ border: 2px solid #555;
113
+ background-size: cover;
114
+ cursor: pointer;
115
+ }
116
+ .block-option.selected {
117
+ border-color: yellow;
118
+ box-shadow: 0 0 5px yellow;
119
+ }
120
  </style>
 
 
 
121
  </head>
122
  <body>
123
  <canvas id="renderCanvas"></canvas>
124
+ <div id="crosshair"></div>
125
 
126
+ <!-- Элементы управления для мобильных -->
127
+ <div id="controls">
128
+ <div class="joystick-area" id="joystickArea">
129
+ <div id="joystick"></div>
130
  </div>
131
  <div id="actionButtons">
132
+ <div class="action-button" id="jumpButton">JUMP</div>
133
+ <div class="action-button" id="breakButton">BREAK</div>
134
+ <div class="action-button" id="placeButton">PLACE</div>
135
  </div>
136
  </div>
137
 
138
+ <!-- Выбор блока -->
139
+ <div id="blockSelector">
140
+ </div>
141
+
142
+
143
  <script>
 
144
  const canvas = document.getElementById('renderCanvas');
145
+ const engine = new BABYLON.Engine(canvas, true, { stencil: true, preserveDrawingBuffer: true }, true);
 
 
 
 
146
 
147
+ // --- Переменные для управления ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  let moveForward = 0;
149
+ let moveRight = 0;
150
+ let wantsToJump = false;
151
+ let currentBlockType = 'grass'; // Блок по умолчанию для установки
152
+
153
+ // --- Текстуры (используем простые URL для примера) ---
154
+ // На реальном проекте лучше использовать свои или CC0 текстуры
155
+ const textureUrls = {
156
+ grassTop: "https://assets.babylonjs.com/environments/grass.png",
157
+ grassSide: "https://assets.babylonjs.com/environments/grass_side.png", // Предположим, что есть такая
158
+ dirt: "https://assets.babylonjs.com/environments/dirt.jpg",
159
+ stone: "https://assets.babylonjs.com/environments/rock.png", // Похоже на камень
160
+ wood: "https://assets.babylonjs.com/environments/wood.png" // Похоже на дерево
161
+ };
162
+ // Простая замена для grassSide, если её нет
163
+ if (!textureUrls.grassSide) textureUrls.grassSide = textureUrls.dirt;
164
+
165
+
166
+ // --- Функция создания сцены ---
167
+ const createScene = () => {
168
+ const scene = new BABYLON.Scene(engine);
169
+ scene.clearColor = new BABYLON.Color3(0.53, 0.81, 0.92); // Цвет неба
170
+ scene.gravity = new BABYLON.Vector3(0, -0.9, 0); // Гравитация
171
+ scene.collisionsEnabled = true; // Включаем обработку столкновений
172
 
173
  // --- Камера (Игрок) ---
174
+ const camera = new BABYLON.UniversalCamera("playerCamera", new BABYLON.Vector3(5, 5, -5), scene);
175
+ camera.setTarget(new BABYLON.Vector3(0, 1.6, 0)); // Куда смотрит камера при старте
176
+ camera.attachControl(canvas, true); // Управление мышью/клавиатурой
177
+ camera.speed = 0.15; // Скорость движения
178
+ camera.angularSensibility = 4000; // Чувствительность мыши
179
+
180
+ // Настройки для столкновений и гравитации камеры
181
+ camera.checkCollisions = true;
182
+ camera.applyGravity = true;
183
+ // Эллипсоид - "хитбокс" игрока (ширина, высота, ширина)
184
+ camera.ellipsoid = new BABYLON.Vector3(0.4, 0.9, 0.4);
185
+ // Смещение эллипсоида, чтобы "ноги" были ниже камеры
186
+ camera.ellipsoidOffset = new BABYLON.Vector3(0, 0.9, 0);
187
+
188
+ // Убираем стандартное управление клавиатурой, если используем джойстик
189
+ // camera.inputs.removeByType("FreeCameraKeyboardMoveInput");
190
+ // camera.inputs.removeByType("FreeCameraMouseInput"); // Оставим мышь для обзора
191
 
192
  // --- Освещение ---
193
  const light = new BABYLON.HemisphericLight("light1", new BABYLON.Vector3(0, 1, 0), scene);
194
  light.intensity = 0.8;
195
 
196
+ // --- Материалы блоков ---
197
+ const materials = {};
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
 
199
+ // Трава (MultiMaterial)
200
+ const grassMat = new BABYLON.StandardMaterial("grassMat", scene);
201
+ grassMat.diffuseTexture = new BABYLON.Texture(textureUrls.grassTop, scene);
202
+ const grassSideMat = new BABYLON.StandardMaterial("grassSideMat", scene);
203
+ grassSideMat.diffuseTexture = new BABYLON.Texture(textureUrls.grassSide, scene);
204
+ const grassBottomMat = new BABYLON.StandardMaterial("grassBottomMat", scene);
205
+ grassBottomMat.diffuseTexture = new BABYLON.Texture(textureUrls.dirt, scene); // Низ травы = земля
206
+
207
+ const grassMultiMat = new BABYLON.MultiMaterial("grassMulti", scene);
208
+ grassMultiMat.subMaterials.push(grassSideMat); // right, left, front, back (индексы 0, 1, 2, 3)
209
+ grassMultiMat.subMaterials.push(grassSideMat);
210
+ grassMultiMat.subMaterials.push(grassSideMat);
211
+ grassMultiMat.subMaterials.push(grassSideMat);
212
+ grassMultiMat.subMaterials.push(grassMat); // top (индекс 4)
213
+ grassMultiMat.subMaterials.push(grassBottomMat); // bottom (индекс 5)
214
+ materials['grass'] = grassMultiMat;
215
+
216
+ // Земля
217
  const dirtMat = new BABYLON.StandardMaterial("dirtMat", scene);
218
+ dirtMat.diffuseTexture = new BABYLON.Texture(textureUrls.dirt, scene);
219
+ materials['dirt'] = dirtMat;
220
 
221
+ // Камень
222
  const stoneMat = new BABYLON.StandardMaterial("stoneMat", scene);
223
+ stoneMat.diffuseTexture = new BABYLON.Texture(textureUrls.stone, scene);
224
+ materials['stone'] = stoneMat;
225
+
226
+ // Дерево
227
+ const woodMat = new BABYLON.StandardMaterial("woodMat", scene);
228
+ woodMat.diffuseTexture = new BABYLON.Texture(textureUrls.wood, scene);
229
+ materials['wood'] = woodMat;
230
+
231
+
232
+ // --- Функция создания блока ---
233
+ const createBlock = (type, x, y, z) => {
234
+ const block = BABYLON.MeshBuilder.CreateBox(`block_${type}_${x}_${y}_${z}`, {size: 1}, scene);
235
+ block.position = new BABYLON.Vector3(x + 0.5, y + 0.5, z + 0.5); // Центрируем блок в координате
236
+ block.checkCollisions = true; // Включаем столкновения для блока
237
+ block.isPickable = true; // Разрешаем "подбирать" лучом
238
+
239
+ if (type === 'grass' && materials['grass'] instanceof BABYLON.MultiMaterial) {
240
+ block.material = materials['grass'];
241
+ // Указываем, какой subMaterial использовать для каждой грани
242
+ block.subMeshes = [];
243
+ // Индексы вершин куба стандартные
244
+ // Каждая грань - 2 треугольника = 6 вершин
245
+ const verticesCount = 36;
246
+ // 4 боковые грани (0-3), 1 верхняя (4), 1 нижняя (5)
247
+ block.subMeshes.push(new BABYLON.SubMesh(0, 0, verticesCount, 0, 6, block)); // right
248
+ block.subMeshes.push(new BABYLON.SubMesh(1, 0, verticesCount, 6, 6, block)); // left
249
+ block.subMeshes.push(new BABYLON.SubMesh(2, 0, verticesCount, 12, 6, block)); // front
250
+ block.subMeshes.push(new BABYLON.SubMesh(3, 0, verticesCount, 18, 6, block)); // back
251
+ block.subMeshes.push(new BABYLON.SubMesh(4, 0, verticesCount, 24, 6, block)); // top
252
+ block.subMeshes.push(new BABYLON.SubMesh(5, 0, verticesCount, 30, 6, block)); // bottom
253
+ } else if (materials[type]) {
254
+ block.material = materials[type];
255
+ } else {
256
+ console.warn(`Material for type "${type}" not found!`);
257
+ // Можно назначить материал по умолчанию
258
+ block.material = materials['dirt'];
259
+ }
260
 
261
+ // Оптимизация для статичных блоков (можно раскомментировать, если мир не меняется часто)
262
+ // block.freezeWorldMatrix();
263
+ return block;
264
+ };
265
 
 
 
 
 
 
266
 
267
+ // --- Создание простого мира ---
268
+ const worldSize = 10; // Размер мира (10x10 блоков)
269
+ const worldHeight = 3; // Высота слоя земли/травы
270
 
271
+ for (let x = 0; x < worldSize; x++) {
272
+ for (let z = 0; z < worldSize; z++) {
273
+ createBlock('stone', x, 0, z); // Слой камня внизу
274
+ createBlock('dirt', x, 1, z); // Слой земли
275
+ createBlock('grass', x, 2, z); // Слой травы сверху
276
+ }
277
+ }
278
+ // Добавим пару блоков для теста установки/удаления
279
+ createBlock('wood', 5, 3, 5);
280
+ createBlock('stone', 6, 3, 5);
281
 
 
 
 
 
 
282
 
283
+ // --- Механика установки / удаления блоков ---
284
+ let lastActionTime = 0;
285
+ const actionCooldown = 200; // Задержка между действиями в мс
 
 
 
 
 
 
 
286
 
287
+ const performAction = (isPlace) => {
288
+ const currentTime = Date.now();
289
+ if (currentTime - lastActionTime < actionCooldown) {
290
+ return; // Игнорировать действие, если еще кулдаун
 
 
 
 
 
 
 
 
291
  }
292
+ lastActionTime = currentTime;
293
+
294
+ const ray = camera.getForwardRay(5); // Луч вперед на 5 единиц
295
+ const pickInfo = scene.pickWithRay(ray, (mesh) => mesh.name.startsWith("block_"));
296
+
297
+ if (pickInfo.hit && pickInfo.pickedMesh) {
298
+ const pickedBlock = pickInfo.pickedMesh;
299
+
300
+ if (isPlace) {
301
+ // --- Установка блока ---
302
+ const normal = pickInfo.getNormal(true); // Нормаль к грани, куда попал луч
303
+ if (!normal) return;
304
+
305
+ // Вычисляем позицию для нового блока, прилегающую к грани старого
306
+ // Смещаем на половину размера блока вдоль нормали и округляем до сетки
307
+ const placePos = pickedBlock.position.add(normal.scale(0.5));
308
+ const targetX = Math.floor(placePos.x);
309
+ const targetY = Math.floor(placePos.y);
310
+ const targetZ = Math.floor(placePos.z);
311
+
312
+ // Проверка, не ставим ли блок внутрь игрока
313
+ const playerFeetY = Math.floor(camera.position.y - camera.ellipsoid.y); // Y "ног" игрока
314
+ const playerHeadY = Math.floor(camera.position.y + camera.ellipsoid.y*0.5); // Y "головы" игрока
315
+ const playerX = Math.floor(camera.position.x);
316
+ const playerZ = Math.floor(camera.position.z);
317
+
318
+ if (targetX === playerX && targetZ === playerZ && (targetY === playerFeetY || targetY === playerHeadY)) {
319
+ console.log("Cannot place block inside player");
320
+ return; // Нельзя ставить блок туда, где стоит игрок
321
+ }
322
+
323
+
324
+ // Проверяем, нет ли уже блока в этом месте
325
+ const existingBlock = scene.getMeshByName(`block_${currentBlockType}_${targetX}_${targetY}_${targetZ}`); // Проверяем по полному имени (может быть неточно)
326
+ // Более надежная проверка - перебрать все меши или использовать пространственный хэш (сложнее)
327
+ let occupied = false;
328
+ scene.meshes.forEach(mesh => {
329
+ if (mesh.name.startsWith("block_") &&
330
+ Math.abs(mesh.position.x - (targetX + 0.5)) < 0.1 &&
331
+ Math.abs(mesh.position.y - (targetY + 0.5)) < 0.1 &&
332
+ Math.abs(mesh.position.z - (targetZ + 0.5)) < 0.1) {
333
+ occupied = true;
334
+ }
335
+ });
336
+
337
+ if (!occupied) {
338
+ createBlock(currentBlockType, targetX, targetY, targetZ);
339
+ } else {
340
+ console.log("Position occupied");
341
+ }
342
 
343
+ } else {
344
+ // --- Удаление блока ---
345
+ // Не даем сломать самый нижний слой камня (условное "дно мира")
346
+ if (pickedBlock.position.y > 0.51) { // Сравниваем с центром блока (0 + 0.5)
347
+ pickedBlock.dispose();
348
+ } else {
349
+ console.log("Cannot break bedrock layer");
350
+ }
351
+ }
352
  }
353
+ };
 
 
 
 
 
 
354
 
355
+ // Обработчики для мыши (десктоп)
356
+ scene.onPointerDown = (evt) => {
357
+ // evt.button: 0 = левая, 1 = средняя, 2 = правая
358
+ if (evt.button === 0) { // Левая кнопка - ломать
359
+ performAction(false);
360
+ } else if (evt.button === 2) { // Правая кнопка - ставить
361
+ performAction(true);
 
362
  }
 
 
363
 
364
+ // Захват указателя для удобного вращения камерой
365
+ if (!engine.isPointerLock) {
366
+ // engine.enterPointerlock(); // Не всегда хорошо работает на мобильных, осторожно
 
 
 
 
 
 
 
 
 
 
 
 
 
 
367
  }
368
+ };
 
 
 
 
 
 
 
 
369
 
370
+ // Выход из захвата указателя по Esc
371
+ // document.addEventListener("pointerlockchange", () => {
372
+ // if (document.pointerLockElement !== canvas) {
373
+ // // Пользователь вышел из режима захвата
374
+ // }
375
+ // }, false);
376
 
377
+ return scene;
378
+ }; // --- Конец createScene ---
379
 
380
 
381
+ // --- Создание и запуск сцены ---
382
+ const scene = createScene();
 
 
383
 
384
+ // --- Настройка мобильных контролов ---
385
+ const joystickArea = document.getElementById('joystickArea');
386
+ const joystick = document.getElementById('joystick');
387
+ const jumpButton = document.getElementById('jumpButton');
388
+ const breakButton = document.getElementById('breakButton');
389
+ const placeButton = document.getElementById('placeButton');
390
+ const blockSelectorDiv = document.getElementById('blockSelector');
391
 
392
+ let joystickActive = false;
393
+ let joystickStartX = 0;
394
+ let joystickStartY = 0;
395
+ const joystickMaxDist = joystickArea.offsetWidth / 2 - joystick.offsetWidth / 2; // Макс смещение ручки
396
+
397
+ // --- Функции для мобильного управления ---
398
+ const handleJoystickStart = (x, y) => {
399
+ joystickActive = true;
400
+ const rect = joystickArea.getBoundingClientRect();
401
+ joystickStartX = rect.left + rect.width / 2;
402
+ joystickStartY = rect.top + rect.height / 2;
403
+ joystick.style.transition = 'none'; // Убрать плавность при перетаскивании
404
+ };
405
+
406
+ const handleJoystickMove = (x, y) => {
407
+ if (!joystickActive) return;
408
+
409
+ let deltaX = x - joystickStartX;
410
+ let deltaY = y - joystickStartY;
411
+ let distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
412
+
413
+ // Ограничиваем максимальное смещение
414
+ if (distance > joystickMaxDist) {
415
+ deltaX = (deltaX / distance) * joystickMaxDist;
416
+ deltaY = (deltaY / distance) * joystickMaxDist;
417
+ distance = joystickMaxDist;
418
+ }
419
 
420
+ // Обновляем позицию ручки джойстика
421
+ joystick.style.left = `calc(50% + ${deltaX}px)`;
422
+ joystick.style.top = `calc(50% + ${deltaY}px)`;
423
+
424
+ // Преобразуем смещение в векторы движения (нормализованные)
425
+ moveRight = deltaX / joystickMaxDist;
426
+ moveForward = -deltaY / joystickMaxDist; // Y инвертирован (вверх = вперед)
427
+ };
428
+
429
+ const handleJoystickEnd = () => {
430
+ if (!joystickActive) return;
431
+ joystickActive = false;
432
+ moveForward = 0;
433
+ moveRight = 0;
434
+ // Возвращаем ручку в центр плавно
435
+ joystick.style.transition = 'left 0.1s ease-out, top 0.1s ease-out';
436
+ joystick.style.left = '50%';
437
+ joystick.style.top = '50%';
438
+ };
439
+
440
+ // --- Навешивание событий для тачскрина и мыши (через Pointer Events) ---
441
+ joystickArea.addEventListener('pointerdown', (e) => {
442
+ e.preventDefault(); // Предотвратить скролл страницы
443
+ e.target.setPointerCapture(e.pointerId); // Захватить указатель для этого элемента
444
+ handleJoystickStart(e.clientX, e.clientY);
445
+ });
446
 
447
+ joystickArea.addEventListener('pointermove', (e) => {
448
+ e.preventDefault();
449
+ handleJoystickMove(e.clientX, e.clientY);
450
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
451
 
452
+ // События на окончание касания/клика
453
+ joystickArea.addEventListener('pointerup', handleJoystickEnd);
454
+ joystickArea.addEventListener('pointercancel', handleJoystickEnd); // Если палец ушел с экрана
455
+ joystickArea.addEventListener('pointerleave', handleJoystickEnd); // Если указатель мыши ушел с элемента
456
+
457
+
458
+ // Кнопки действий
459
+ jumpButton.addEventListener('pointerdown', () => wantsToJump = true);
460
+ // При отпускании кнопки прыжка можно сбросить флаг, но для одиночного прыжка это не нужно
461
+ // jumpButton.addEventListener('pointerup', () => wantsToJump = false);
462
+
463
+ breakButton.addEventListener('pointerdown', () => scene.onPointerDown({ button: 0 })); // Симулируем левый клик
464
+ placeButton.addEventListener('pointerdown', () => scene.onPointerDown({ button: 2 })); // Симулируем правый клик
465
+
466
+ // --- Выбор блока ---
467
+ const availableBlocks = ['grass', 'dirt', 'stone', 'wood'];
468
+ availableBlocks.forEach(type => {
469
+ const option = document.createElement('div');
470
+ option.classList.add('block-option');
471
+ option.dataset.type = type;
472
+ // Устанавливаем фон из текстуры (упрощенно, берем первую попавшуюся)
473
+ let textureUrl = textureUrls[type] || textureUrls.grassTop || textureUrls.dirt; // Найти подходящую
474
+ option.style.backgroundImage = `url(${textureUrl})`;
475
+ if (type === currentBlockType) {
476
+ option.classList.add('selected');
477
+ }
478
 
479
+ option.addEventListener('click', () => {
480
+ currentBlockType = type;
481
+ // Обновляем выделение
482
+ document.querySelectorAll('.block-option').forEach(el => el.classList.remove('selected'));
483
+ option.classList.add('selected');
484
+ console.log("Selected block:", currentBlockType);
485
+ });
486
+ blockSelectorDiv.appendChild(option);
487
+ });
488
 
 
 
489
 
490
+ // --- Основной цикл рендеринга ---
491
+ let playerGrounded = false; // Флаг, стоит ли игрок на земле
492
+ const groundCheckDist = 0.1; // Дистанция для проверки земли под ногами
 
493
 
494
+ engine.runRenderLoop(() => {
495
+ const camera = scene.activeCamera;
496
+ if (!camera) return;
 
 
 
 
 
497
 
498
+ const deltaTime = engine.getDeltaTime() / 1000.0; // Время кадра в секундах
499
 
500
+ // --- Проверка, стоит ли игрок на земле ---
501
+ const rayStart = camera.position.clone();
502
+ rayStart.y -= camera.ellipsoid.y; // Начало луча у "ног"
503
+ const rayDir = new BABYLON.Vector3(0, -1, 0);
504
+ const groundRay = new BABYLON.Ray(rayStart, rayDir, camera.ellipsoid.y + groundCheckDist);
505
+ const groundPickInfo = scene.pickWithRay(groundRay, (mesh) => mesh.checkCollisions);
506
+ playerGrounded = groundPickInfo.hit;
507
 
508
+ // --- Прыжок ---
509
+ if (wantsToJump && playerGrounded) {
510
+ // Простой вариант: напрямую изменить Y-скорость (если бы была физика)
511
+ // Или просто подкинуть вверх (может пройти сквозь потолок без физики)
512
+ // camera.cameraDirection.y = 0.2; // Не работает с UniversalCamera напрямую
513
+ // Используем импульс, если есть физический движок, или просто меняем позицию
514
+ camera.position.y += 1.5; // Резкий подъем (величина прыжка)
515
+ wantsToJump = false; // Сбрасываем флаг после прыжка
516
+ }
517
+ // Сбрасываем флаг, если кнопка отпущена или игрок уже не на земле
518
+ // (это нужно, если кнопка удерживается, для одиночного нажатия сброс выше)
519
+ // if (!wantsToJump || !playerGrounded) { wantsToJump = false }
520
 
 
 
 
 
521
 
522
+ // --- Перемещение с помощью джойстика/клавиатуры ---
523
+ const effectiveSpeed = camera.speed * deltaTime * 60; // Нормализуем скорость к 60 FPS
524
 
525
+ // Получаем векторы направления камеры (независимо от ее наклона вверх/вниз)
 
526
  const forward = new BABYLON.Vector3(
527
  Math.sin(camera.rotation.y),
528
  0,
529
  Math.cos(camera.rotation.y)
530
+ ).normalize();
531
  const right = new BABYLON.Vector3(
532
  Math.sin(camera.rotation.y + Math.PI / 2),
533
  0,
534
  Math.cos(camera.rotation.y + Math.PI / 2)
535
+ ).normalize();
536
+
537
+ // Вычисляем вектор движения на основе ввода (джойстик или WASD)
538
+ // Клавиатурный ввод обрабатывается UniversalCamera по умолчанию,
539
+ // но мы можем его переопределить или дополнить джойстиком.
540
+ let moveDirection = BABYLON.Vector3.Zero();
541
+
542
+ // Используем ввод с джойстика, если он активен
543
+ if (Math.abs(moveForward) > 0.1 || Math.abs(moveRight) > 0.1) {
544
+ moveDirection = forward.scale(moveForward).add(right.scale(moveRight));
545
+ } else {
546
+ // Если джойстик неактивен, проверяем стандартный ввод камеры (WASD)
547
+ // Это немного хак, зависит от внутренней реализации камеры
548
+ const keyboardInput = camera.inputs.attached.keyboard; // Получаем доступ к клавиатурному вводу
549
+ if (keyboardInput) {
550
+ const x = keyboardInput.keysLeft.indexOf(keyboardInput.keysLeft[0]) !== -1 ? -1 : keyboardInput.keysRight.indexOf(keyboardInput.keysRight[0]) !== -1 ? 1 : 0;
551
+ const z = keyboardInput.keysUp.indexOf(keyboardInput.keysUp[0]) !== -1 ? 1 : keyboardInput.keysDown.indexOf(keyboardInput.keysDown[0]) !== -1 ? -1 : 0;
552
+ if (x !== 0 || z !== 0) {
553
+ moveDirection = forward.scale(z).add(right.scale(x));
554
+ }
 
 
 
 
 
 
 
 
555
  }
556
  }
557
 
558
 
559
+ // Нормализуем и применяем скорость
560
  if (moveDirection.lengthSquared() > 0) {
561
+ moveDirection.normalize().scaleInPlace(effectiveSpeed);
562
+ // Двигаем камеру с учетом столкновений
563
+ camera.moveWithCollisions(moveDirection);
564
  }
565
 
566
+ // Небольшая коррекция, чтобы игрок не "тонул" в земле при столкновениях
567
+ if (playerGrounded && camera.position.y < (groundPickInfo.pickedPoint.y + camera.ellipsoid.y)) {
568
+ //camera.position.y = groundPickInfo.pickedPoint.y + camera.ellipsoid.y;
569
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
570
 
 
 
 
 
 
571
 
572
+ scene.render();
573
+ });
 
 
 
 
 
 
574
 
575
+ // --- Обработка изменения размера окна ---
576
+ window.addEventListener('resize', () => {
577
+ engine.resize();
578
+ // Пересчитать joystickMaxDist при рес��йзе
579
+ // joystickMaxDist = joystickArea.offsetWidth / 2 - joystick.offsetWidth / 2;
580
  });
581
 
582
  </script>