Update app.py
Browse files
app.py
CHANGED
|
@@ -123,8 +123,6 @@ EDITOR_TEMPLATE = '''
|
|
| 123 |
border: 8px solid #f3f3f3; border-top: 8px solid #3498db; border-radius: 50%;
|
| 124 |
width: 60px; height: 60px; animation: spin 1s linear infinite; display: none; z-index: 100;
|
| 125 |
}
|
| 126 |
-
#blocker { position: absolute; width: 100%; height: 100%; background-color: rgba(0,0,0,0.5); display: none; }
|
| 127 |
-
#instructions { width: 100%; height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center; text-align: center; font-size: 14px; cursor: pointer; color: white; }
|
| 128 |
#burger-menu {
|
| 129 |
position: absolute; top: 15px; left: 15px; z-index: 20; display: none; width: 30px; height: 22px; cursor: pointer; pointer-events: auto;
|
| 130 |
}
|
|
@@ -171,15 +169,14 @@ EDITOR_TEMPLATE = '''
|
|
| 171 |
<div id="tool-selector"></div>
|
| 172 |
<p style="font-size: 0.8em; color: #888; margin-top: 10px;">
|
| 173 |
ЛКМ: Разместить<br>
|
| 174 |
-
ПКМ:
|
| 175 |
Shift + ЛКМ: Удалить<br>
|
| 176 |
-
Колесо мыши:
|
| 177 |
</p>
|
| 178 |
<button id="clear-level" class="danger-button">Очистить уровень</button>
|
| 179 |
</div>
|
| 180 |
</div>
|
| 181 |
</div>
|
| 182 |
-
<div id="blocker"><div id="instructions"><p style="font-size:36px">Нажмите, чтобы играть</p><p>Движение: WASD / Джойстик</p><p>Нажмите ESC для выхода</p></div></div>
|
| 183 |
<div id="loading-spinner"></div>
|
| 184 |
<div id="joystick-container"><div id="joystick-handle"></div></div>
|
| 185 |
|
|
@@ -188,10 +185,8 @@ EDITOR_TEMPLATE = '''
|
|
| 188 |
</script>
|
| 189 |
<script type="module">
|
| 190 |
import * as THREE from 'three';
|
| 191 |
-
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
| 192 |
|
| 193 |
-
let scene, camera, renderer,
|
| 194 |
-
let raycaster, mouse, placementPlane, gridHelper, previewMesh;
|
| 195 |
|
| 196 |
let isPlayMode = false;
|
| 197 |
let player, playerVelocity = new THREE.Vector3();
|
|
@@ -206,32 +201,56 @@ EDITOR_TEMPLATE = '''
|
|
| 206 |
const levelData = { floors: {}, walls: {}, objects: {} };
|
| 207 |
|
| 208 |
const joystick = { active: false, center: new THREE.Vector2(), current: new THREE.Vector2(), vector: new THREE.Vector2() };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 209 |
|
| 210 |
const ASSETS = {
|
| 211 |
floors: {
|
| 212 |
-
grass: {
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
|
|
|
|
|
|
| 216 |
},
|
| 217 |
walls: {
|
| 218 |
-
brick: {
|
| 219 |
-
concrete: {
|
| 220 |
},
|
| 221 |
objects: {
|
| 222 |
-
crate: {
|
| 223 |
-
barrel: {
|
| 224 |
}
|
| 225 |
};
|
| 226 |
|
| 227 |
const instancedMeshes = {};
|
|
|
|
|
|
|
| 228 |
|
| 229 |
function init() {
|
| 230 |
scene = new THREE.Scene();
|
| 231 |
-
scene.background = new THREE.Color(
|
|
|
|
| 232 |
|
| 233 |
-
|
|
|
|
|
|
|
| 234 |
camera.position.set(25, 30, 25);
|
|
|
|
| 235 |
|
| 236 |
renderer = new THREE.WebGLRenderer({ antialias: true });
|
| 237 |
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
@@ -239,17 +258,11 @@ EDITOR_TEMPLATE = '''
|
|
| 239 |
renderer.shadowMap.enabled = true;
|
| 240 |
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
| 241 |
document.body.appendChild(renderer.domElement);
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
orbitControls.enableDamping = true;
|
| 245 |
-
orbitControls.maxPolarAngle = Math.PI / 2.2;
|
| 246 |
-
orbitControls.minDistance = 10;
|
| 247 |
-
orbitControls.maxDistance = 100;
|
| 248 |
-
|
| 249 |
-
const hemiLight = new THREE.HemisphereLight(0xB1E1FF, 0xB97A20, 0.8);
|
| 250 |
scene.add(hemiLight);
|
| 251 |
|
| 252 |
-
const dirLight = new THREE.DirectionalLight(0xffffff,
|
| 253 |
dirLight.position.set(-30, 50, -30);
|
| 254 |
dirLight.castShadow = true;
|
| 255 |
dirLight.shadow.mapSize.width = 2048;
|
|
@@ -279,12 +292,15 @@ EDITOR_TEMPLATE = '''
|
|
| 279 |
window.addEventListener('resize', onWindowResize);
|
| 280 |
renderer.domElement.addEventListener('pointermove', onPointerMove);
|
| 281 |
renderer.domElement.addEventListener('pointerdown', onPointerDown);
|
|
|
|
| 282 |
window.addEventListener('keydown', e => { keyStates[e.code] = true; handleKeyDown(e); });
|
| 283 |
window.addEventListener('keyup', e => { keyStates[e.code] = false; });
|
| 284 |
renderer.domElement.addEventListener('wheel', e => {
|
| 285 |
if(!isPlayMode) {
|
| 286 |
e.preventDefault();
|
| 287 |
-
|
|
|
|
|
|
|
| 288 |
}
|
| 289 |
}, { passive: false });
|
| 290 |
renderer.domElement.addEventListener('contextmenu', e => e.preventDefault());
|
|
@@ -298,13 +314,16 @@ EDITOR_TEMPLATE = '''
|
|
| 298 |
instancedMeshes[category] = {};
|
| 299 |
for (const type in ASSETS[category]) {
|
| 300 |
const asset = ASSETS[category][type];
|
|
|
|
|
|
|
|
|
|
| 301 |
let geometry;
|
| 302 |
if(asset.geometry === 'cylinder') {
|
| 303 |
geometry = new THREE.CylinderGeometry(asset.size[0], asset.size[0], asset.size[1], 16);
|
| 304 |
} else {
|
| 305 |
geometry = new THREE.BoxGeometry(...asset.size);
|
| 306 |
}
|
| 307 |
-
const material = new THREE.MeshStandardMaterial({
|
| 308 |
const mesh = new THREE.InstancedMesh(geometry, material, MAX_COUNT);
|
| 309 |
mesh.castShadow = true;
|
| 310 |
mesh.receiveShadow = true;
|
|
@@ -332,7 +351,7 @@ EDITOR_TEMPLATE = '''
|
|
| 332 |
for (const type in ASSETS[category]) {
|
| 333 |
const item = document.createElement('div');
|
| 334 |
item.className = 'tool-item';
|
| 335 |
-
item.textContent = `${category.slice(0,1).toUpperCase()}: ${type}`;
|
| 336 |
item.dataset.category = category;
|
| 337 |
item.dataset.type = type;
|
| 338 |
item.addEventListener('click', () => selectTool(category, type));
|
|
@@ -358,8 +377,6 @@ EDITOR_TEMPLATE = '''
|
|
| 358 |
const touch = event.touches[0];
|
| 359 |
joystick.active = true;
|
| 360 |
joystick.center.set(touch.clientX, touch.clientY);
|
| 361 |
-
joystickContainer.style.left = `${touch.clientX - maxRadius}px`;
|
| 362 |
-
joystickContainer.style.top = `${touch.clientY - maxRadius}px`;
|
| 363 |
}
|
| 364 |
function onTouchMove(event) {
|
| 365 |
if (!joystick.active) return;
|
|
@@ -393,34 +410,60 @@ EDITOR_TEMPLATE = '''
|
|
| 393 |
} else {
|
| 394 |
geometry = new THREE.BoxGeometry(...asset.size);
|
| 395 |
}
|
| 396 |
-
const material = new THREE.MeshBasicMaterial({
|
|
|
|
|
|
|
| 397 |
previewMesh = new THREE.Mesh(geometry, material);
|
| 398 |
scene.add(previewMesh);
|
| 399 |
}
|
| 400 |
|
| 401 |
function onWindowResize() {
|
| 402 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 403 |
camera.updateProjectionMatrix();
|
| 404 |
renderer.setSize(window.innerWidth, window.innerHeight);
|
| 405 |
}
|
| 406 |
|
| 407 |
function onPointerMove(event) {
|
| 408 |
-
if (isPlayMode) return;
|
| 409 |
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
|
| 410 |
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 411 |
raycaster.setFromCamera(mouse, camera);
|
| 412 |
const intersects = raycaster.intersectObject(placementPlane);
|
| 413 |
if (intersects.length > 0) {
|
| 414 |
const point = intersects[0].point;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 415 |
const gridX = Math.round(point.x / gridSize);
|
| 416 |
const gridZ = Math.round(point.z / gridSize);
|
| 417 |
-
previewMesh.position.set(gridX * gridSize, 0, gridZ * gridSize);
|
| 418 |
|
| 419 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 420 |
if (currentTool.category === 'floors') {
|
| 421 |
previewMesh.position.y = asset.size[1] / 2;
|
| 422 |
-
} else if (currentTool.category === 'walls') {
|
| 423 |
-
previewMesh.position.y = asset.size[1] / 2;
|
| 424 |
} else {
|
| 425 |
previewMesh.position.y = asset.size[1] / 2;
|
| 426 |
}
|
|
@@ -438,18 +481,20 @@ EDITOR_TEMPLATE = '''
|
|
| 438 |
}
|
| 439 |
|
| 440 |
function onPointerDown(event) {
|
| 441 |
-
if (isPlayMode
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
if (event.button === 2) {
|
| 445 |
-
|
|
|
|
| 446 |
return;
|
| 447 |
}
|
| 448 |
-
|
| 449 |
-
if (event.button !== 0) return;
|
|
|
|
| 450 |
|
| 451 |
const pos = previewMesh.position;
|
| 452 |
-
const key = `${pos.x},${pos.z}`;
|
| 453 |
|
| 454 |
if (isRemoving) {
|
| 455 |
removeItemAt(pos);
|
|
@@ -458,6 +503,12 @@ EDITOR_TEMPLATE = '''
|
|
| 458 |
}
|
| 459 |
updateLevelGeometry();
|
| 460 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 461 |
|
| 462 |
function rotatePreview(direction) {
|
| 463 |
currentRotation += (Math.PI / 2) * direction;
|
|
@@ -470,6 +521,7 @@ EDITOR_TEMPLATE = '''
|
|
| 470 |
|
| 471 |
removeItemAt(pos);
|
| 472 |
|
|
|
|
| 473 |
if (!levelData[category][type]) levelData[category][type] = [];
|
| 474 |
levelData[category][type].push([pos.x, pos.z, rotation]);
|
| 475 |
}
|
|
@@ -503,17 +555,15 @@ EDITOR_TEMPLATE = '''
|
|
| 503 |
dummy.position.set(x, 0, z);
|
| 504 |
dummy.rotation.set(0, rot, 0);
|
| 505 |
|
| 506 |
-
|
| 507 |
-
dummy.position.y = asset.size[1] / 2;
|
| 508 |
-
} else {
|
| 509 |
-
dummy.position.y = asset.size[1] / 2;
|
| 510 |
-
}
|
| 511 |
|
| 512 |
dummy.updateMatrix();
|
| 513 |
mesh.setMatrixAt(i, dummy.matrix);
|
| 514 |
-
|
| 515 |
-
if (category === 'walls') {
|
| 516 |
-
const
|
|
|
|
|
|
|
| 517 |
wallBBoxes.push(box);
|
| 518 |
}
|
| 519 |
}
|
|
@@ -533,28 +583,25 @@ EDITOR_TEMPLATE = '''
|
|
| 533 |
function togglePlayMode() {
|
| 534 |
isPlayMode = !isPlayMode;
|
| 535 |
const uiContainer = document.getElementById('ui-container');
|
| 536 |
-
const blocker = document.getElementById('blocker');
|
| 537 |
const joystickContainer = document.getElementById('joystick-container');
|
| 538 |
|
| 539 |
if (isPlayMode) {
|
| 540 |
uiContainer.style.display = 'none';
|
| 541 |
gridHelper.visible = false;
|
| 542 |
previewMesh.visible = false;
|
| 543 |
-
orbitControls.enabled = false;
|
| 544 |
player.visible = true;
|
| 545 |
player.position.set(0, 0, 0);
|
| 546 |
-
|
| 547 |
-
joystickContainer.style.display = 'block';
|
| 548 |
} else {
|
| 549 |
uiContainer.style.display = 'flex';
|
| 550 |
gridHelper.visible = true;
|
| 551 |
previewMesh.visible = true;
|
| 552 |
-
orbitControls.enabled = true;
|
| 553 |
player.visible = false;
|
| 554 |
-
blocker.style.display = 'none';
|
| 555 |
joystickContainer.style.display = 'none';
|
| 556 |
camera.position.set(25, 30, 25);
|
| 557 |
-
|
|
|
|
|
|
|
| 558 |
}
|
| 559 |
}
|
| 560 |
|
|
@@ -605,6 +652,8 @@ EDITOR_TEMPLATE = '''
|
|
| 605 |
}
|
| 606 |
|
| 607 |
const moveDirection = new THREE.Vector3();
|
|
|
|
|
|
|
| 608 |
function updatePlayer(deltaTime) {
|
| 609 |
const speed = playerSpeed * deltaTime;
|
| 610 |
moveDirection.set(0,0,0);
|
|
@@ -646,10 +695,9 @@ EDITOR_TEMPLATE = '''
|
|
| 646 |
}
|
| 647 |
if(!collisionZ) player.position.z = intendedZPos.z;
|
| 648 |
|
| 649 |
-
camera.position.
|
| 650 |
-
camera.position.z = player.position.z + 15;
|
| 651 |
-
camera.position.y = 20;
|
| 652 |
camera.lookAt(player.position);
|
|
|
|
| 653 |
}
|
| 654 |
|
| 655 |
function showSpinner(show) {
|
|
@@ -662,9 +710,8 @@ EDITOR_TEMPLATE = '''
|
|
| 662 |
|
| 663 |
if (isPlayMode) {
|
| 664 |
updatePlayer(deltaTime);
|
| 665 |
-
} else {
|
| 666 |
-
orbitControls.update();
|
| 667 |
}
|
|
|
|
| 668 |
renderer.render(scene, camera);
|
| 669 |
}
|
| 670 |
|
|
|
|
| 123 |
border: 8px solid #f3f3f3; border-top: 8px solid #3498db; border-radius: 50%;
|
| 124 |
width: 60px; height: 60px; animation: spin 1s linear infinite; display: none; z-index: 100;
|
| 125 |
}
|
|
|
|
|
|
|
| 126 |
#burger-menu {
|
| 127 |
position: absolute; top: 15px; left: 15px; z-index: 20; display: none; width: 30px; height: 22px; cursor: pointer; pointer-events: auto;
|
| 128 |
}
|
|
|
|
| 169 |
<div id="tool-selector"></div>
|
| 170 |
<p style="font-size: 0.8em; color: #888; margin-top: 10px;">
|
| 171 |
ЛКМ: Разместить<br>
|
| 172 |
+
ПКМ: Вращать / Двигать камеру<br>
|
| 173 |
Shift + ЛКМ: Удалить<br>
|
| 174 |
+
Колесо мыши: Приближение
|
| 175 |
</p>
|
| 176 |
<button id="clear-level" class="danger-button">Очистить уровень</button>
|
| 177 |
</div>
|
| 178 |
</div>
|
| 179 |
</div>
|
|
|
|
| 180 |
<div id="loading-spinner"></div>
|
| 181 |
<div id="joystick-container"><div id="joystick-handle"></div></div>
|
| 182 |
|
|
|
|
| 185 |
</script>
|
| 186 |
<script type="module">
|
| 187 |
import * as THREE from 'three';
|
|
|
|
| 188 |
|
| 189 |
+
let scene, camera, renderer, raycaster, mouse, placementPlane, gridHelper, previewMesh;
|
|
|
|
| 190 |
|
| 191 |
let isPlayMode = false;
|
| 192 |
let player, playerVelocity = new THREE.Vector3();
|
|
|
|
| 201 |
const levelData = { floors: {}, walls: {}, objects: {} };
|
| 202 |
|
| 203 |
const joystick = { active: false, center: new THREE.Vector2(), current: new THREE.Vector2(), vector: new THREE.Vector2() };
|
| 204 |
+
|
| 205 |
+
const isMobile = 'ontouchstart' in window;
|
| 206 |
+
|
| 207 |
+
const textureLoader = new THREE.TextureLoader();
|
| 208 |
+
const loadedTextures = {};
|
| 209 |
+
|
| 210 |
+
function loadTexture(url) {
|
| 211 |
+
if (loadedTextures[url]) return loadedTextures[url];
|
| 212 |
+
const texture = textureLoader.load(url);
|
| 213 |
+
texture.wrapS = THREE.RepeatWrapping;
|
| 214 |
+
texture.wrapT = THREE.RepeatWrapping;
|
| 215 |
+
texture.magFilter = THREE.NearestFilter;
|
| 216 |
+
texture.minFilter = THREE.NearestFilter;
|
| 217 |
+
loadedTextures[url] = texture;
|
| 218 |
+
return texture;
|
| 219 |
+
}
|
| 220 |
|
| 221 |
const ASSETS = {
|
| 222 |
floors: {
|
| 223 |
+
grass: { size: [1, 0.1, 1], mapUrl: 'https://i.ibb.co/FqsRw2T/grass.png' },
|
| 224 |
+
grass_4x4: { size: [4, 0.1, 4], mapUrl: 'https://i.ibb.co/FqsRw2T/grass.png', repeat: [4,4] },
|
| 225 |
+
concrete: { size: [1, 0.1, 1], mapUrl: 'https://i.ibb.co/yQdGbpS/concrete.png' },
|
| 226 |
+
concrete_4x4: { size: [4, 0.1, 4], mapUrl: 'https://i.ibb.co/yQdGbpS/concrete.png', repeat: [4,4] },
|
| 227 |
+
wood: { size: [1, 0.1, 1], mapUrl: 'https://i.ibb.co/GvxPb7k/wood.png' },
|
| 228 |
+
dirt: { size: [1, 0.1, 1], mapUrl: 'https://i.ibb.co/P9tLdZn/dirt.png' }
|
| 229 |
},
|
| 230 |
walls: {
|
| 231 |
+
brick: { size: [1, 2.5, 0.2], mapUrl: 'https://i.ibb.co/PMNtV6z/brick.png', repeat: [1,2] },
|
| 232 |
+
concrete: { size: [1, 2.5, 0.2], mapUrl: 'https://i.ibb.co/VMyXgV8/wall-concrete.png', repeat: [1,2] }
|
| 233 |
},
|
| 234 |
objects: {
|
| 235 |
+
crate: { size: [0.8, 0.8, 0.8], mapUrl: 'https://i.ibb.co/PZ9rV3g/crate.png' },
|
| 236 |
+
barrel: { size: [0.4, 0.8, 0], geometry: 'cylinder', mapUrl: 'https://i.ibb.co/PcnwQqp/barrel.png' }
|
| 237 |
}
|
| 238 |
};
|
| 239 |
|
| 240 |
const instancedMeshes = {};
|
| 241 |
+
let isPanning = false;
|
| 242 |
+
const panStart = new THREE.Vector2();
|
| 243 |
|
| 244 |
function init() {
|
| 245 |
scene = new THREE.Scene();
|
| 246 |
+
scene.background = new THREE.Color(0x3d4a55);
|
| 247 |
+
scene.fog = new THREE.Fog(0x3d4a55, 50, 150);
|
| 248 |
|
| 249 |
+
const aspect = window.innerWidth / window.innerHeight;
|
| 250 |
+
const frustumSize = 30;
|
| 251 |
+
camera = new THREE.OrthographicCamera(frustumSize * aspect / -2, frustumSize * aspect / 2, frustumSize / 2, frustumSize / -2, 1, 1000);
|
| 252 |
camera.position.set(25, 30, 25);
|
| 253 |
+
camera.lookAt(0,0,0);
|
| 254 |
|
| 255 |
renderer = new THREE.WebGLRenderer({ antialias: true });
|
| 256 |
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
|
|
| 258 |
renderer.shadowMap.enabled = true;
|
| 259 |
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
| 260 |
document.body.appendChild(renderer.domElement);
|
| 261 |
+
|
| 262 |
+
const hemiLight = new THREE.HemisphereLight(0xB1E1FF, 0x495057, 0.6);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 263 |
scene.add(hemiLight);
|
| 264 |
|
| 265 |
+
const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
| 266 |
dirLight.position.set(-30, 50, -30);
|
| 267 |
dirLight.castShadow = true;
|
| 268 |
dirLight.shadow.mapSize.width = 2048;
|
|
|
|
| 292 |
window.addEventListener('resize', onWindowResize);
|
| 293 |
renderer.domElement.addEventListener('pointermove', onPointerMove);
|
| 294 |
renderer.domElement.addEventListener('pointerdown', onPointerDown);
|
| 295 |
+
renderer.domElement.addEventListener('pointerup', onPointerUp);
|
| 296 |
window.addEventListener('keydown', e => { keyStates[e.code] = true; handleKeyDown(e); });
|
| 297 |
window.addEventListener('keyup', e => { keyStates[e.code] = false; });
|
| 298 |
renderer.domElement.addEventListener('wheel', e => {
|
| 299 |
if(!isPlayMode) {
|
| 300 |
e.preventDefault();
|
| 301 |
+
const zoomAmount = e.deltaY > 0 ? 1.1 : 0.9;
|
| 302 |
+
camera.zoom *= zoomAmount;
|
| 303 |
+
camera.updateProjectionMatrix();
|
| 304 |
}
|
| 305 |
}, { passive: false });
|
| 306 |
renderer.domElement.addEventListener('contextmenu', e => e.preventDefault());
|
|
|
|
| 314 |
instancedMeshes[category] = {};
|
| 315 |
for (const type in ASSETS[category]) {
|
| 316 |
const asset = ASSETS[category][type];
|
| 317 |
+
const texture = loadTexture(asset.mapUrl);
|
| 318 |
+
if(asset.repeat) texture.repeat.set(...asset.repeat);
|
| 319 |
+
|
| 320 |
let geometry;
|
| 321 |
if(asset.geometry === 'cylinder') {
|
| 322 |
geometry = new THREE.CylinderGeometry(asset.size[0], asset.size[0], asset.size[1], 16);
|
| 323 |
} else {
|
| 324 |
geometry = new THREE.BoxGeometry(...asset.size);
|
| 325 |
}
|
| 326 |
+
const material = new THREE.MeshStandardMaterial({ map: texture });
|
| 327 |
const mesh = new THREE.InstancedMesh(geometry, material, MAX_COUNT);
|
| 328 |
mesh.castShadow = true;
|
| 329 |
mesh.receiveShadow = true;
|
|
|
|
| 351 |
for (const type in ASSETS[category]) {
|
| 352 |
const item = document.createElement('div');
|
| 353 |
item.className = 'tool-item';
|
| 354 |
+
item.textContent = `${category.slice(0,1).toUpperCase()}: ${type.replace('_',' ')}`;
|
| 355 |
item.dataset.category = category;
|
| 356 |
item.dataset.type = type;
|
| 357 |
item.addEventListener('click', () => selectTool(category, type));
|
|
|
|
| 377 |
const touch = event.touches[0];
|
| 378 |
joystick.active = true;
|
| 379 |
joystick.center.set(touch.clientX, touch.clientY);
|
|
|
|
|
|
|
| 380 |
}
|
| 381 |
function onTouchMove(event) {
|
| 382 |
if (!joystick.active) return;
|
|
|
|
| 410 |
} else {
|
| 411 |
geometry = new THREE.BoxGeometry(...asset.size);
|
| 412 |
}
|
| 413 |
+
const material = new THREE.MeshBasicMaterial({ map: loadTexture(asset.mapUrl), transparent: true, opacity: 0.6 });
|
| 414 |
+
if(asset.repeat) material.map.repeat.set(...asset.repeat);
|
| 415 |
+
|
| 416 |
previewMesh = new THREE.Mesh(geometry, material);
|
| 417 |
scene.add(previewMesh);
|
| 418 |
}
|
| 419 |
|
| 420 |
function onWindowResize() {
|
| 421 |
+
const aspect = window.innerWidth / window.innerHeight;
|
| 422 |
+
const frustumSize = 30 / camera.zoom;
|
| 423 |
+
camera.left = frustumSize * aspect / -2;
|
| 424 |
+
camera.right = frustumSize * aspect / 2;
|
| 425 |
+
camera.top = frustumSize / 2;
|
| 426 |
+
camera.bottom = frustumSize / -2;
|
| 427 |
camera.updateProjectionMatrix();
|
| 428 |
renderer.setSize(window.innerWidth, window.innerHeight);
|
| 429 |
}
|
| 430 |
|
| 431 |
function onPointerMove(event) {
|
|
|
|
| 432 |
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
|
| 433 |
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
|
| 434 |
+
|
| 435 |
+
if(isPanning) {
|
| 436 |
+
const dx = event.clientX - panStart.x;
|
| 437 |
+
const dy = event.clientY - panStart.y;
|
| 438 |
+
panStart.set(event.clientX, event.clientY);
|
| 439 |
+
const panSpeed = (camera.right - camera.left) / (camera.zoom * renderer.domElement.clientWidth);
|
| 440 |
+
camera.position.x -= dx * panSpeed;
|
| 441 |
+
camera.position.z -= dy * panSpeed * Math.cos(camera.rotation.x);
|
| 442 |
+
return;
|
| 443 |
+
}
|
| 444 |
+
|
| 445 |
+
if (isPlayMode) return;
|
| 446 |
raycaster.setFromCamera(mouse, camera);
|
| 447 |
const intersects = raycaster.intersectObject(placementPlane);
|
| 448 |
if (intersects.length > 0) {
|
| 449 |
const point = intersects[0].point;
|
| 450 |
+
const asset = ASSETS[currentTool.category][currentTool.type];
|
| 451 |
+
const w = (asset.size[0] / gridSize);
|
| 452 |
+
const h = (asset.size[2] / gridSize);
|
| 453 |
+
|
| 454 |
const gridX = Math.round(point.x / gridSize);
|
| 455 |
const gridZ = Math.round(point.z / gridSize);
|
|
|
|
| 456 |
|
| 457 |
+
let finalX = gridX * gridSize;
|
| 458 |
+
let finalZ = gridZ * gridSize;
|
| 459 |
+
|
| 460 |
+
if (w > 1) finalX += (w/2-0.5) * Math.sign(finalX) * gridSize;
|
| 461 |
+
if (h > 1) finalZ += (h/2-0.5) * Math.sign(finalZ) * gridSize;
|
| 462 |
+
|
| 463 |
+
previewMesh.position.set(finalX, 0, finalZ);
|
| 464 |
+
|
| 465 |
if (currentTool.category === 'floors') {
|
| 466 |
previewMesh.position.y = asset.size[1] / 2;
|
|
|
|
|
|
|
| 467 |
} else {
|
| 468 |
previewMesh.position.y = asset.size[1] / 2;
|
| 469 |
}
|
|
|
|
| 481 |
}
|
| 482 |
|
| 483 |
function onPointerDown(event) {
|
| 484 |
+
if (isPlayMode && event.target.tagName === 'CANVAS') {
|
| 485 |
+
if(isMobile) return;
|
| 486 |
+
}
|
| 487 |
+
if (!isPlayMode && event.button === 2) {
|
| 488 |
+
isPanning = true;
|
| 489 |
+
panStart.set(event.clientX, event.clientY);
|
| 490 |
return;
|
| 491 |
}
|
| 492 |
+
|
| 493 |
+
if (isPlayMode || !previewMesh.visible || event.button !== 0) return;
|
| 494 |
+
const isRemoving = event.shiftKey;
|
| 495 |
|
| 496 |
const pos = previewMesh.position;
|
| 497 |
+
const key = `${pos.x},${pos.y},${pos.z}`;
|
| 498 |
|
| 499 |
if (isRemoving) {
|
| 500 |
removeItemAt(pos);
|
|
|
|
| 503 |
}
|
| 504 |
updateLevelGeometry();
|
| 505 |
}
|
| 506 |
+
|
| 507 |
+
function onPointerUp(event) {
|
| 508 |
+
if (event.button === 2) {
|
| 509 |
+
isPanning = false;
|
| 510 |
+
}
|
| 511 |
+
}
|
| 512 |
|
| 513 |
function rotatePreview(direction) {
|
| 514 |
currentRotation += (Math.PI / 2) * direction;
|
|
|
|
| 521 |
|
| 522 |
removeItemAt(pos);
|
| 523 |
|
| 524 |
+
if (!levelData[category]) levelData[category] = {};
|
| 525 |
if (!levelData[category][type]) levelData[category][type] = [];
|
| 526 |
levelData[category][type].push([pos.x, pos.z, rotation]);
|
| 527 |
}
|
|
|
|
| 555 |
dummy.position.set(x, 0, z);
|
| 556 |
dummy.rotation.set(0, rot, 0);
|
| 557 |
|
| 558 |
+
dummy.position.y = (category === 'floors' ? asset.size[1] / 2 : asset.size[1] / 2);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 559 |
|
| 560 |
dummy.updateMatrix();
|
| 561 |
mesh.setMatrixAt(i, dummy.matrix);
|
| 562 |
+
|
| 563 |
+
if (category === 'walls' || category === 'objects') {
|
| 564 |
+
const boxHelper = new THREE.BoxHelper(dummy);
|
| 565 |
+
boxHelper.geometry.computeBoundingBox();
|
| 566 |
+
const box = boxHelper.geometry.boundingBox;
|
| 567 |
wallBBoxes.push(box);
|
| 568 |
}
|
| 569 |
}
|
|
|
|
| 583 |
function togglePlayMode() {
|
| 584 |
isPlayMode = !isPlayMode;
|
| 585 |
const uiContainer = document.getElementById('ui-container');
|
|
|
|
| 586 |
const joystickContainer = document.getElementById('joystick-container');
|
| 587 |
|
| 588 |
if (isPlayMode) {
|
| 589 |
uiContainer.style.display = 'none';
|
| 590 |
gridHelper.visible = false;
|
| 591 |
previewMesh.visible = false;
|
|
|
|
| 592 |
player.visible = true;
|
| 593 |
player.position.set(0, 0, 0);
|
| 594 |
+
if(isMobile) joystickContainer.style.display = 'block';
|
|
|
|
| 595 |
} else {
|
| 596 |
uiContainer.style.display = 'flex';
|
| 597 |
gridHelper.visible = true;
|
| 598 |
previewMesh.visible = true;
|
|
|
|
| 599 |
player.visible = false;
|
|
|
|
| 600 |
joystickContainer.style.display = 'none';
|
| 601 |
camera.position.set(25, 30, 25);
|
| 602 |
+
camera.lookAt(0,0,0);
|
| 603 |
+
camera.zoom = 1;
|
| 604 |
+
camera.updateProjectionMatrix();
|
| 605 |
}
|
| 606 |
}
|
| 607 |
|
|
|
|
| 652 |
}
|
| 653 |
|
| 654 |
const moveDirection = new THREE.Vector3();
|
| 655 |
+
const cameraOffset = new THREE.Vector3(20, 25, 20);
|
| 656 |
+
|
| 657 |
function updatePlayer(deltaTime) {
|
| 658 |
const speed = playerSpeed * deltaTime;
|
| 659 |
moveDirection.set(0,0,0);
|
|
|
|
| 695 |
}
|
| 696 |
if(!collisionZ) player.position.z = intendedZPos.z;
|
| 697 |
|
| 698 |
+
camera.position.copy(player.position).add(cameraOffset);
|
|
|
|
|
|
|
| 699 |
camera.lookAt(player.position);
|
| 700 |
+
camera.updateProjectionMatrix();
|
| 701 |
}
|
| 702 |
|
| 703 |
function showSpinner(show) {
|
|
|
|
| 710 |
|
| 711 |
if (isPlayMode) {
|
| 712 |
updatePlayer(deltaTime);
|
|
|
|
|
|
|
| 713 |
}
|
| 714 |
+
|
| 715 |
renderer.render(scene, camera);
|
| 716 |
}
|
| 717 |
|