Spaces:
Running
Running
Update index.html
Browse files- 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>
|
| 7 |
<style>
|
| 8 |
-
/* Базовый сброс стилей и стили для canvas */
|
| 9 |
html, body {
|
| 10 |
-
|
| 11 |
-
padding: 0;
|
| 12 |
-
overflow: hidden; /* Убираем прокрутку */
|
| 13 |
width: 100%;
|
| 14 |
height: 100%;
|
| 15 |
-
|
|
|
|
|
|
|
|
|
|
| 16 |
}
|
| 17 |
|
| 18 |
#renderCanvas {
|
| 19 |
width: 100%;
|
| 20 |
height: 100%;
|
| 21 |
-
|
| 22 |
-
touch-action: none; /* Отключаем стандартные действия браузера при касании (важно для управления камерой) */
|
| 23 |
outline: none; /* Убираем обводку при фокусе */
|
| 24 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
</style>
|
| 26 |
-
<!-- Подключаем Babylon.js
|
| 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 |
-
// Получаем
|
| 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 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
//
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
}
|
| 146 |
-
|
| 147 |
};
|
| 148 |
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 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 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 172 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 173 |
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
|
|
|
|
|
|
|
| 179 |
|
| 180 |
-
|
| 181 |
-
|
| 182 |
|
| 183 |
-
|
|
|
|
|
|
|
|
|
|
| 184 |
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 188 |
|
| 189 |
-
// Проверяем, нет ли уже блока в этой позиции
|
| 190 |
-
if (blockStorage[blockId]) {
|
| 191 |
-
// console.log(`Блок ${blockId} уже существует.`);
|
| 192 |
-
return null;
|
| 193 |
-
}
|
| 194 |
|
| 195 |
-
//
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 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 |
-
|
| 210 |
-
|
| 211 |
-
const blockToRemove = blockStorage[blockId];
|
| 212 |
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
// console.log(`Удален блок ${blockId}`);
|
| 217 |
-
} else {
|
| 218 |
-
// console.log(`Блок ${blockId} не найден для удаления.`);
|
| 219 |
-
}
|
| 220 |
}
|
| 221 |
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
const
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 235 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 236 |
}
|
| 237 |
-
console.log(`Сгенерирован плоский мир ${WORLD_WIDTH}x${WORLD_DEPTH}`);
|
| 238 |
-
}
|
| 239 |
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 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 |
-
|
| 310 |
|
| 311 |
-
// Главный цикл рендеринга
|
| 312 |
-
engine.runRenderLoop(() => {
|
| 313 |
-
scene.render();
|
| 314 |
-
});
|
| 315 |
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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>
|