Spaces:
Running
Running
Update index.html
Browse files- index.html +409 -409
index.html
CHANGED
@@ -96,11 +96,8 @@
|
|
96 |
</div>
|
97 |
|
98 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
|
|
99 |
<script>
|
100 |
-
|
101 |
-
// --- Simplex Noise 라이브러리 코드 (simplex-noise.min.js 내용) ---
|
102 |
-
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).simplexNoise={})}(this,(function(e){"use strict";function t(){return Math.random()-.5}function n(e,t,n,r){return(e-t)/(n-r)}function r(e){let t=e[0],n=e[1],r=e[2],i=e[3];return t*t+n*n+r*r+i*r}const i=1/Math.sqrt(2);class o{constructor(e){this.perm=[],this.permMod12=[];for(let n=0;n<256;n++){this.perm.push(n);const r=e();this.perm[n]=r}for(let e=0;e<256;e++){const n=e+128;this.perm[n]=this.perm[e]}}grad(e){const t=this.perm[e%256];return[t&1?1:-1,t&2?1:-1,t&4?1:-1]}}const s={grad3:[[1,1,0],[-1,1,0],[1,-1,0],[-1,-1,0],[1,0,1],[-1,0,1],[1,0,-1],[-1,0,-1],[0,1,1],[0,-1,1],[0,1,-1],[0,-1,-1]],grad4:[[0,1,1,1],[0,1,1,-1],[0,1,-1,1],[0,1,-1,-1],[0,-1,1,1],[0,-1,1,-1],[0,-1,-1,1],[0,-1,-1,-1],[1,0,1,1],[1,0,1,-1],[1,0,-1,1],[1,0,-1,-1],[-1,0,1,1],[-1,0,1,-1],[-1,0,-1,1],[-1,0,-1,-1],[1,1,0,1],[1,1,0,-1],[1,-1,0,1],[1,-1,0,-1],[-1,1,0,1],[-1,1,0,-1],[-1,-1,0,1],[-1,-1,0,-1],[1,1,1,0],[1,1,-1,0],[1,-1,1,0],[1,-1,-1,0],[-1,1,1,0],[-1,1,-1,0],[-1,-1,1,0],[-1,-1,-1,0]],p:[151,160,137,91,90,15,131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23,190,6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33,88,237,149,56,87,174,20,125,136,171,168,68,175,74,165,71,134,139,48,27,166,77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244,102,143,54,65,25,63,161,1,216,80,73,209,76,132,187,208,89,18,169,200,196,135,130,116,188,159,86,164,100,109,198,173,186,3,64,52,217,226,250,124,123,5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42,223,183,170,213,119,248,152,2,44,154,163,70,221,153,101,155,167,43,172,9,129,22,39,253,19,98,108,110,79,113,224,232,178,185,112,104,218,246,97,228,251,34,242,193,238,210,144,12,191,179,162,241,81,51,145,235,249,14,239,107,49,192,214,31,181,199,106,157,184,84,204,176,115,121,50,45,127,4,150,254,138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180],perm:[],permMod12:[]};function a(e){let t,n,r,i;void 0===e?e=Math.random:("function"==typeof e&&(i=e),e={}),this.p=s.p,t=void 0!==e.perm?e.perm:this.p,n=void 0!==e.permMod12?e.permMod12:this.p.map((function(e){return e%12})),r=void 0!==e.grad3?e.grad3:s.grad3,this.perm=t,this.permMod12=n,this.grad3=r,this.grad4=void 0!==e.grad4?e.grad4:s.grad4,this.F2=.5*(Math.sqrt(3)-1),this.G2=(3-Math.sqrt(3))/6,this.F3=1/3,this.G3=1/6,this.F4=(Math.sqrt(5)-1)/4,this.G4=(5-Math.sqrt(5))/20,this.random=i||t}a.prototype.noise2D=function(e,t){const n=this.perm,r=this.permMod12,i=this.grad3;let o,s,a=.5,c=.5;o=this.F2*(e+t)+(e=Math.floor(e)),s=this.F2*(o+t)+(t=Math.floor(t));let l=e+this.G2*(o+s),f=t+this.G2*(o+s),d=e-l,u=t-f;const h=d>u?1:0,p=d>u?0:1;let m=d-h+this.G2,v=u-p+this.G2,g=d-1+2*this.G2,y=u-1+2*this.G2,M=o&255,x=s&255;let b=r[M+n[x]]%12,w=r[M+h+n[x+p]]%12,j=r[M+1+n[x+1]]%12;let S=0;const P=i[b],z=P[0]*d+P[1]*u;S+=70*(z<0?0:z*z*z*z);const D=i[w],F=D[0]*m+D[1]*v;S+=70*(F<0?0:F*F*F*F);const G=i[j],E=G[0]*g+G[1]*y;return S+70*(E<0?0:E*E*E*E)};a.prototype.noise3D=function(e,t,n){const r=this.perm,i=this.permMod12,o=this.grad3;let s,a,c,l,f,d=.6;s=this.F3*(e+t+n)+(e=Math.floor(e)),a=this.F3*(s+t+n)+(t=Math.floor(t)),c=this.F3*(s+a+n)+(n=Math.floor(n));let u=e+this.G3*(s+a+c),h=t+this.G3*(s+a+c),p=n+this.G3*(s+a+c),m=e-u,v=t-h,g=n-p;const y=m>=v?v>=g?0:m>=g?1:2:m<g?2:v<g?1:0,M=0===y?m>=v?1:2:1===y?v>=g?0:2:2===y?m<g?0:1:0,x=1===y?m>=v?1:2:2===y?v>=g?0:2:0===y?m<g?0:1:0,b=2===y?m>=v?1:2:0===y?v>=g?0:2:1===y?m<g?0:1:0;let w=m-M+this.G3,j=v-y+this.G3,S=g-x+this.G3,P=m-b+2*this.G3,z=v-M+2*this.G3,D=g-y+2*this.G3,F=m-1+3*this.G3,G=v-1+3*this.G3,E=n-1+3*this.G3,T=s&255,A=a&255,C=n&255;let R=i[T+r[A+r[C]]]%12,N=i[T+M+r[A+y+r[C+x]]]%12,O=i[T+b+r[A+M+r[C+y]]]%12,L=i[T+1+r[A+1+r[C+1]]]%12;let _=0;const q=o[R],I=q[0]*m+q[1]*v+q[2]*g;_+=32*(I<0?0:I*I*I*I);const k=o[N],B=k[0]*w+k[1]*j+k[2]*S;_+=32*(B<0?0:B*B*B*B);const V=o[O],H=V[0]*P+V[1]*z+V[2]*D;_+=32*(H<0?0:H*H*H*H);const X=o[L],J=X[0]*F+X[1]*G+X[2]*E;return _+32*(J<0?0:J*J*J*J)};a.prototype.noise4D=function(e,t,n,r){const i=this.perm,o=this.grad4;let s,a,c,l,f;s=this.F4*(e+t+n+r)+(e=Math.floor(e)),a=this.F4*(s+t+n+r)+(t=Math.floor(t)),c=this.F4*(s+a+n+r)+(n=Math.floor(n)),l=this.F4*(s+a+c+r)+(r=Math.floor(r));let d=e+this.G4*(s+a+c+l),u=t+this.G4*(s+a+c+l),h=n+this.G4*(s+a+c+l),p=r+this.G4*(s+a+c+l),m=e-d,v=t-u,g=n-h,y=r-p;let M,x,b,w,j,S,P,z,D,F,G,E,T,A,C,R;M=m>v?1:0,x=m>g?1:0,b=m>y?1:0,w=v>g?1:0,j=v>y?1:0,S=g>y?1:0;const N=x>=S?x>=w?M>=x?M>=j?0:1:j>=x?3:1:M>=j?0:1:w>=x?M>=j?0:2:j>=x?3:2:M>=j?0:2,O=b>=j?b>=S?M>=b?0:1:S>=b?4:1:M>=b?0:1:j>=b?M>=S?0:3:S>=b?4:3,L=x<=S?x<=w?M>=x?0:4:j>=x?2:4:M>=x?0:4:w>=x?M>=j?0:3:j>=x?2:3,I=b<=j?b<=S?M>=b?0:4:S>=b?3:4:M>=b?0:4:j>=b?M>=S?0:2:S>=b?3:2,k=1-M-N-L,B=1-x-y-b,V=1-w-O-I,H=1-j-x-b,X=1-S-w-j;let J=m-M+this.G4,q=v-x+this.G4,U=g-y+this.G4,Z=y-b+this.G4,K=m-N+2*this.G4,Q=v-O+2*this.G4,W=g-L+2*this.G4,Y=y-I+2*this.G4,ee=m-k+3*this.G4,te=v-B+3*this.G4,ne=g-V+3*this.G4,re=y-H+3*this.G4,ie=m-1+4*this.G4,oe=v-1+4*this.G4,se=g-1+4*this.G4,ae=y-1+4*this.G4,ce=s&255,le=a&255,fe=c&255,de=l&255;let ue=i[ce+i[le+i[fe+i[de]]]]%32,he=i[ce+M+i[le+x+i[fe+y+i[de+b]]]]%32,pe=i[ce+N+i[le+O+i[fe+L+i[de+I]]]]%32,me=i[ce+k+i[le+B+i[fe+V+i[de+H]]]]%32,ve=i[ce+1+i[le+1+i[fe+1+i[de+1]]]]%32;let ge=0;const ye=o[ue],Me=ye[0]*m+ye[1]*v+ye[2]*g+ye[3]*y;ge+=27*(Me<0?0:Me*Me*Me*Me);const xe=o[he],be=xe[0]*J+xe[1]*q+xe[2]*U+xe[3]*Z;ge+=27*(be<0?0:be*be*be*be);const we=o[pe],je=we[0]*K+we[1]*Q+we[2]*W+we[3]*Y;ge+=27*(je<0?0:je*je*je*je);const Se=o[me],Pe=Se[0]*ee+Se[1]*te+Se[2]*ne+Se[3]*re;ge+=27*(Pe<0?0:Pe*Pe*Pe*Pe);const ze=o[ve],De=ze[0]*ie+ze[1]*oe+ze[2]*se+ze[3]*ae;return ge+27*(De<0?0:De*De*De*De)},e.SimplexNoise=a,e.createNoise2D=function(e){const t=new a(e);return(e,n)=>t.noise2D(e,n)},e.createNoise3D=function(e){const t=new a(e);return(e,n,r)=>t.noise3D(e,n,r)},e.createNoise4D=function(e){const t=new a(e);return(e,n,r,i)=>t.noise4D(e,n,r,i)},Object.defineProperty(e,"__esModule",{value:!0})}));
|
103 |
-
|
104 |
const SPAWN_POSITION = new THREE.Vector3(0, 1.7, 30);
|
105 |
const CAVE_ENTRANCE = new THREE.Vector3(0, 1.7, -15);
|
106 |
const CHUNK_SIZE = 50;
|
@@ -122,7 +119,7 @@
|
|
122 |
let flashlight;
|
123 |
let isFlashlightOn = false;
|
124 |
let loadedChunks = {};
|
125 |
-
let simplex
|
126 |
let objectPools = {
|
127 |
trees: [],
|
128 |
vegetation: []
|
@@ -177,6 +174,9 @@
|
|
177 |
|
178 |
document.getElementById('loading-indicator').style.display = 'block'; // Show on start
|
179 |
|
|
|
|
|
|
|
180 |
generateCaveSystem();
|
181 |
updateChunks(); // Initial chunk loading (now after cave generation)
|
182 |
|
@@ -303,417 +303,417 @@
|
|
303 |
}
|
304 |
|
305 |
function createVegetation(x, z, parent) {
|
306 |
-
|
307 |
-
|
308 |
-
|
309 |
-
|
310 |
-
}
|
311 |
-
}
|
312 |
-
|
313 |
-
function generateCaveSystem() {
|
314 |
-
let startPoint = CAVE_ENTRANCE.clone();
|
315 |
-
caveSystem.push(startPoint);
|
316 |
-
|
317 |
-
for (let i = 0; i < 20; i++) {
|
318 |
-
const lastPoint = caveSystem[caveSystem.length - 1];
|
319 |
-
const newPoint = lastPoint.clone();
|
320 |
-
|
321 |
-
newPoint.x += (Math.random() - 0.5) * 5;
|
322 |
-
newPoint.y += (Math.random() - 0.5) * 2;
|
323 |
-
newPoint.z -= 5 + Math.random() * 5;
|
324 |
-
|
325 |
-
caveSystem.push(newPoint);
|
326 |
-
}
|
327 |
-
|
328 |
-
const caveCurve = new THREE.CatmullRomCurve3(caveSystem);
|
329 |
-
const tunnelGeometry = new THREE.TubeGeometry(caveCurve, 128, 3, 8, false);
|
330 |
-
const caveMaterial = new THREE.MeshStandardMaterial({ color: 0x333333, roughness: 1, side: THREE.BackSide });
|
331 |
-
const tunnel = new THREE.Mesh(tunnelGeometry, caveMaterial);
|
332 |
-
scene.add(tunnel);
|
333 |
-
|
334 |
-
const markingGeometry = new THREE.CircleGeometry(0.3, 16);
|
335 |
-
const markingMaterial = new THREE.MeshStandardMaterial({
|
336 |
-
color: 0xff0000,
|
337 |
-
emissive: 0xff0000,
|
338 |
-
emissiveIntensity: 0.5
|
339 |
-
});
|
340 |
-
|
341 |
-
for (let i = 0; i < 30; i++) {
|
342 |
-
const point = caveCurve.getPointAt(i / 29);
|
343 |
-
const marking = new THREE.Mesh(markingGeometry, markingMaterial);
|
344 |
-
|
345 |
-
const normal = caveCurve.getTangentAt(i / 29); // Get the tangent (direction) at this point
|
346 |
-
const offset = new THREE.Vector3().randomDirection().multiplyScalar(2.5); // Random offset
|
347 |
-
marking.position.copy(point).add(offset);
|
348 |
-
|
349 |
-
|
350 |
-
marking.rotation.y = Math.random() * Math.PI;
|
351 |
-
scene.add(marking);
|
352 |
-
}
|
353 |
-
console.log("Cave system generated:", caveSystem); // Debug
|
354 |
-
}
|
355 |
-
|
356 |
-
function generateChunk(chunkX, chunkZ) {
|
357 |
-
const chunk = new THREE.Group();
|
358 |
-
chunk.chunkPosition = { x: chunkX, z: chunkZ };
|
359 |
-
|
360 |
-
const startX = chunkX * CHUNK_SIZE;
|
361 |
-
const startZ = chunkZ * CHUNK_SIZE;
|
362 |
-
|
363 |
-
// Ground
|
364 |
-
const ground = new THREE.Mesh(
|
365 |
-
new THREE.PlaneGeometry(CHUNK_SIZE, CHUNK_SIZE),
|
366 |
-
new THREE.MeshStandardMaterial({ color: 0x33aa33, roughness: 0.8 })
|
367 |
-
);
|
368 |
-
ground.rotation.x = -Math.PI / 2;
|
369 |
-
ground.position.set(startX + CHUNK_SIZE / 2, 0, startZ + CHUNK_SIZE / 2);
|
370 |
-
ground.receiveShadow = true;
|
371 |
-
chunk.add(ground);
|
372 |
-
|
373 |
-
// Trees and vegetation
|
374 |
-
for (let x = startX; x < startX + CHUNK_SIZE; x++) {
|
375 |
-
for (let z = startZ; z < startZ + CHUNK_SIZE; z++) {
|
376 |
-
const treeDensity = simplex.noise2D(x * 0.02, z * 0.02);
|
377 |
-
const vegetationDensity = simplex.noise2D(x * 0.1, z * 0.1);
|
378 |
-
|
379 |
-
if (treeDensity > TREE_DENSITY) {
|
380 |
-
createTree(x, z, chunk);
|
381 |
}
|
382 |
-
|
383 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
384 |
}
|
|
|
385 |
}
|
386 |
-
|
387 |
-
|
388 |
-
|
389 |
-
|
390 |
-
|
391 |
-
|
392 |
-
|
393 |
-
|
394 |
-
|
395 |
-
|
396 |
-
|
397 |
-
|
398 |
-
|
399 |
-
|
400 |
-
|
401 |
-
|
402 |
-
|
403 |
-
|
404 |
-
|
405 |
-
|
406 |
-
|
407 |
-
|
408 |
-
|
409 |
-
|
410 |
-
|
411 |
-
|
412 |
-
|
413 |
-
|
414 |
-
|
415 |
-
if (!chunksToLoad[chunkId]) {
|
416 |
-
const chunk = loadedChunks[chunkId];
|
417 |
-
// Return objects to pools
|
418 |
-
chunk.children.forEach(child => {
|
419 |
-
if (child instanceof THREE.Mesh) {
|
420 |
-
if (child.geometry instanceof THREE.CylinderGeometry) { // It's likely a tree trunk
|
421 |
-
let tree = objectPools.trees.find(t => t.trunk === child);
|
422 |
-
if (tree) returnObjectToPool('trees', tree);
|
423 |
-
} else if (child.geometry instanceof THREE.SphereGeometry) { // Could be leaves or vegetation
|
424 |
-
if (child.material.color.equals(new THREE.Color(0x227722))) { // Likely leaves
|
425 |
-
let tree = objectPools.trees.find(t => t.leaves === child);
|
426 |
-
if (tree) returnObjectToPool('trees', tree);
|
427 |
-
} else { // Likely vegetation
|
428 |
-
let vegetation = objectPools.vegetation.find(v => v === child);
|
429 |
-
if (vegetation) returnObjectToPool('vegetation', vegetation);
|
430 |
-
}
|
431 |
}
|
432 |
}
|
433 |
-
}
|
434 |
-
|
435 |
-
|
436 |
-
|
437 |
-
|
438 |
-
|
439 |
-
|
440 |
-
|
441 |
-
|
442 |
-
|
443 |
-
|
444 |
-
|
445 |
-
|
446 |
-
|
447 |
-
|
448 |
-
|
449 |
-
|
450 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
451 |
})
|
452 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
453 |
}
|
454 |
-
|
455 |
-
|
456 |
-
|
457 |
-
|
458 |
-
|
459 |
-
|
460 |
-
|
461 |
-
|
462 |
-
|
463 |
-
|
464 |
-
|
465 |
-
|
466 |
-
|
467 |
-
|
468 |
-
|
469 |
-
|
470 |
-
|
471 |
-
|
472 |
-
|
473 |
-
|
474 |
-
|
475 |
-
|
476 |
-
}
|
477 |
-
|
478 |
-
|
479 |
-
|
480 |
-
|
481 |
-
|
482 |
-
|
483 |
-
|
484 |
-
|
485 |
-
|
486 |
-
|
487 |
-
|
488 |
-
|
489 |
-
|
490 |
-
|
491 |
-
|
492 |
-
|
493 |
-
|
494 |
-
|
495 |
-
|
496 |
-
|
497 |
-
|
498 |
-
|
499 |
-
|
500 |
-
|
501 |
-
|
502 |
-
|
503 |
-
|
504 |
-
|
505 |
-
|
506 |
-
|
507 |
-
|
508 |
-
|
509 |
-
|
510 |
-
|
511 |
-
|
512 |
-
|
513 |
-
|
514 |
-
|
515 |
-
|
516 |
-
|
517 |
-
|
518 |
-
|
519 |
-
|
520 |
-
|
521 |
-
|
522 |
-
|
523 |
-
|
524 |
-
|
525 |
-
|
526 |
-
|
527 |
-
|
528 |
-
|
529 |
-
|
530 |
-
|
531 |
-
|
532 |
-
|
533 |
-
|
534 |
-
|
535 |
-
|
536 |
-
|
537 |
-
|
538 |
-
|
539 |
-
|
540 |
-
|
541 |
-
}
|
542 |
-
|
543 |
-
|
544 |
-
|
545 |
-
|
546 |
-
|
547 |
-
|
548 |
-
|
549 |
-
|
550 |
-
|
551 |
-
|
552 |
-
|
553 |
-
|
554 |
-
|
555 |
-
|
556 |
-
|
557 |
-
|
558 |
-
|
559 |
-
flashlight.penumbra = 0.1;
|
560 |
-
flashlight.decay = 2;
|
561 |
-
flashlight.distance = 30;
|
562 |
-
flashlight.visible = false;
|
563 |
-
camera.add(flashlight);
|
564 |
-
scene.add(camera); // Important: Add the camera to the scene!
|
565 |
-
}
|
566 |
-
|
567 |
-
function setupParticles() {
|
568 |
-
const particleCount = 1000;
|
569 |
-
const positions = new Float32Array(particleCount * 3);
|
570 |
-
const colors = new Float32Array(particleCount * 3);
|
571 |
-
|
572 |
-
for (let i = 0; i < particleCount * 3; i += 3) {
|
573 |
-
positions[i] = Math.random() * 200 - 100;
|
574 |
-
positions[i + 1] = Math.random() * 20;
|
575 |
-
positions[i + 2] = Math.random() * 200 - 100;
|
576 |
-
|
577 |
-
colors[i] = 1;
|
578 |
-
colors[i + 1] = 1;
|
579 |
-
colors[i + 2] = 0.8;
|
580 |
-
}
|
581 |
-
|
582 |
-
const geometry = new THREE.BufferGeometry();
|
583 |
-
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
584 |
-
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
|
585 |
-
const material = new THREE.PointsMaterial({ size: 0.1, vertexColors: true, transparent: true, opacity: 0.6 });
|
586 |
-
const particles = new THREE.Points(geometry, material);
|
587 |
-
scene.add(particles);
|
588 |
-
}
|
589 |
-
|
590 |
-
function setupAudio() {
|
591 |
-
// TODO: Implement audio
|
592 |
-
}
|
593 |
-
|
594 |
-
function animate() {
|
595 |
-
requestAnimationFrame(animate);
|
596 |
-
const delta = clock.getDelta();
|
597 |
-
update(delta);
|
598 |
-
renderer.render(scene, camera);
|
599 |
-
}
|
600 |
-
|
601 |
-
// --- Keyboard controls (existing) ---
|
602 |
-
function onKeyDown(event) {
|
603 |
-
switch (event.code) {
|
604 |
-
case 'KeyW': controls.forward = true; break;
|
605 |
-
case 'KeyS': controls.backward = true; break;
|
606 |
-
case 'KeyA': controls.left = true; break;
|
607 |
-
case 'KeyD': controls.right = true; break;
|
608 |
-
case 'ShiftLeft': controls.sprint = true; break;
|
609 |
-
case 'KeyF': toggleFlashlight(); break;
|
610 |
-
}
|
611 |
-
}
|
612 |
-
|
613 |
-
function onKeyUp(event) {
|
614 |
-
switch (event.code) {
|
615 |
-
case 'KeyW': controls.forward = false; break;
|
616 |
-
case 'KeyS': controls.backward = false; break;
|
617 |
-
case 'KeyA': controls.left = false; break;
|
618 |
-
case 'KeyD': controls.right = false; break;
|
619 |
-
case 'ShiftLeft': controls.sprint = false; break;
|
620 |
-
}
|
621 |
-
}
|
622 |
-
|
623 |
-
function onMouseMove(event) {
|
624 |
-
if (document.pointerLockElement) {
|
625 |
-
camera.rotation.y -= event.movementX * 0.002;
|
626 |
-
camera.rotation.x = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, camera.rotation.x - event.movementY * 0.002));
|
627 |
-
}
|
628 |
-
}
|
629 |
-
|
630 |
-
function toggleFlashlight() {
|
631 |
-
isFlashlightOn = !isFlashlightOn;
|
632 |
-
flashlight.visible = isFlashlightOn;
|
633 |
-
document.getElementById('flashlight-status').textContent = `Flashlight [F] ${isFlashlightOn ? 'ON' : 'OFF'}`;
|
634 |
-
}
|
635 |
-
|
636 |
-
// --- Touch controls ---
|
637 |
-
function onTouchStart(event) {
|
638 |
-
event.preventDefault(); // Prevent other behaviors (like scrolling)
|
639 |
-
|
640 |
-
// For simplicity, we'll only handle the first touch point
|
641 |
-
const touch = event.touches[0];
|
642 |
-
|
643 |
-
if (touch.target.closest("#joystick-zone")) {
|
644 |
-
joystick.isActive = true;
|
645 |
-
touchStartX = touch.clientX;
|
646 |
-
touchStartY = touch.clientY;
|
647 |
-
|
648 |
-
//Put knob at center
|
649 |
-
joystickKnob.style.left = `0px`;
|
650 |
-
joystickKnob.style.top = `0px`;
|
651 |
-
|
652 |
-
} else { // General screen touch (for rotation, like mouse look)
|
653 |
-
touchStartX = touch.clientX;
|
654 |
-
touchStartY = touch.clientY;
|
655 |
-
}
|
656 |
-
}
|
657 |
-
|
658 |
-
let joystickKnob = document.getElementById("joystick-knob");
|
659 |
-
let joystickZone = document.getElementById("joystick-zone");
|
660 |
-
|
661 |
-
function onTouchMove(event) {
|
662 |
-
event.preventDefault();
|
663 |
-
const touch = event.touches[0];
|
664 |
-
|
665 |
-
if (joystick.isActive) {
|
666 |
-
// Calculate the distance the touch has moved from the starting point
|
667 |
-
const deltaX = touch.clientX - touchStartX;
|
668 |
-
const deltaY = touch.clientY - touchStartY;
|
669 |
-
|
670 |
-
// Limit delta, so it is circular
|
671 |
-
let length = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
672 |
-
const maxLength = 75 - 30; // radius of outer - radius of inner
|
673 |
-
if(length > maxLength) {
|
674 |
-
const angle = Math.atan2(deltaY, deltaX);
|
675 |
-
joystick.deltaX = Math.cos(angle) * maxLength / (maxLength*2);
|
676 |
-
joystick.deltaY = Math.sin(angle) * maxLength/ (maxLength*2);
|
677 |
-
// Move the joystick knob visually
|
678 |
-
joystickKnob.style.left = `${Math.cos(angle) * maxLength}px`;
|
679 |
-
joystickKnob.style.top = `${Math.sin(angle) * maxLength}px`;
|
680 |
-
|
681 |
-
} else {
|
682 |
-
joystick.deltaX = deltaX / (maxLength*2) ; // Normalize for movement
|
683 |
-
joystick.deltaY = deltaY / (maxLength*2);
|
684 |
-
joystickKnob.style.left = `${deltaX}px`;
|
685 |
-
joystickKnob.style.top = `${deltaY}px`;
|
686 |
-
}
|
687 |
-
|
688 |
-
|
689 |
-
|
690 |
-
} else { // Rotation (like mouse look)
|
691 |
-
const movementX = touch.clientX - touchStartX;
|
692 |
-
const movementY = touch.clientY - touchStartY;
|
693 |
-
camera.rotation.y -= movementX * 0.005; // Adjust sensitivity as needed
|
694 |
-
camera.rotation.x = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, camera.rotation.x - movementY * 0.005));
|
695 |
-
|
696 |
-
// Reset for next movement calculation. Keep camera rotation smooth.
|
697 |
-
touchStartX = touch.clientX;
|
698 |
-
touchStartY = touch.clientY;
|
699 |
-
}
|
700 |
-
}
|
701 |
-
|
702 |
-
function onTouchEnd(event) {
|
703 |
-
if(joystick.isActive){
|
704 |
-
joystick.isActive = false;
|
705 |
-
joystick.deltaX = 0;
|
706 |
-
joystick.deltaY = 0;
|
707 |
-
|
708 |
-
// Reset joystick visuals
|
709 |
-
joystickKnob.style.left = `0px`;
|
710 |
-
joystickKnob.style.top = `0px`;
|
711 |
-
}
|
712 |
-
|
713 |
-
}
|
714 |
-
|
715 |
-
init();
|
716 |
-
animate();
|
717 |
</script>
|
718 |
</body>
|
719 |
</html>
|
|
|
96 |
</div>
|
97 |
|
98 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
99 |
+
<script async defer src="https://cdnjs.cloudflare.com/ajax/libs/simplex-noise/4.0.1/simplex-noise.min.js"></script> <!-- async defer 추가 -->
|
100 |
<script>
|
|
|
|
|
|
|
|
|
101 |
const SPAWN_POSITION = new THREE.Vector3(0, 1.7, 30);
|
102 |
const CAVE_ENTRANCE = new THREE.Vector3(0, 1.7, -15);
|
103 |
const CHUNK_SIZE = 50;
|
|
|
119 |
let flashlight;
|
120 |
let isFlashlightOn = false;
|
121 |
let loadedChunks = {};
|
122 |
+
let simplex; // SimplexNoise 인스턴스는 여기서 선언
|
123 |
let objectPools = {
|
124 |
trees: [],
|
125 |
vegetation: []
|
|
|
174 |
|
175 |
document.getElementById('loading-indicator').style.display = 'block'; // Show on start
|
176 |
|
177 |
+
// Simplex Noise 초기화 (이제 defer 때문에 여기서 가능)
|
178 |
+
simplex = new SimplexNoise();
|
179 |
+
|
180 |
generateCaveSystem();
|
181 |
updateChunks(); // Initial chunk loading (now after cave generation)
|
182 |
|
|
|
303 |
}
|
304 |
|
305 |
function createVegetation(x, z, parent) {
|
306 |
+
const flower = getObjectFromPool('vegetation');
|
307 |
+
if (flower) {
|
308 |
+
flower.position.set(x, 0.2, z);
|
309 |
+
parent.add(flower);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
310 |
}
|
311 |
+
}
|
312 |
+
|
313 |
+
function generateCaveSystem() {
|
314 |
+
let startPoint = CAVE_ENTRANCE.clone();
|
315 |
+
caveSystem.push(startPoint);
|
316 |
+
|
317 |
+
for (let i = 0; i < 20; i++) {
|
318 |
+
const lastPoint = caveSystem[caveSystem.length - 1];
|
319 |
+
const newPoint = lastPoint.clone();
|
320 |
+
|
321 |
+
newPoint.x += (Math.random() - 0.5) * 5;
|
322 |
+
newPoint.y += (Math.random() - 0.5) * 2;
|
323 |
+
newPoint.z -= 5 + Math.random() * 5;
|
324 |
+
|
325 |
+
caveSystem.push(newPoint);
|
326 |
+
}
|
327 |
+
|
328 |
+
const caveCurve = new THREE.CatmullRomCurve3(caveSystem);
|
329 |
+
const tunnelGeometry = new THREE.TubeGeometry(caveCurve, 128, 3, 8, false);
|
330 |
+
const caveMaterial = new THREE.MeshStandardMaterial({ color: 0x333333, roughness: 1, side: THREE.BackSide });
|
331 |
+
const tunnel = new THREE.Mesh(tunnelGeometry, caveMaterial);
|
332 |
+
scene.add(tunnel);
|
333 |
+
|
334 |
+
const markingGeometry = new THREE.CircleGeometry(0.3, 16);
|
335 |
+
const markingMaterial = new THREE.MeshStandardMaterial({
|
336 |
+
color: 0xff0000,
|
337 |
+
emissive: 0xff0000,
|
338 |
+
emissiveIntensity: 0.5
|
339 |
+
});
|
340 |
+
|
341 |
+
for (let i = 0; i < 30; i++) {
|
342 |
+
const point = caveCurve.getPointAt(i / 29);
|
343 |
+
const marking = new THREE.Mesh(markingGeometry, markingMaterial);
|
344 |
+
|
345 |
+
const normal = caveCurve.getTangentAt(i / 29); // Get the tangent (direction) at this point
|
346 |
+
const offset = new THREE.Vector3().randomDirection().multiplyScalar(2.5); // Random offset
|
347 |
+
marking.position.copy(point).add(offset);
|
348 |
+
|
349 |
+
|
350 |
+
marking.rotation.y = Math.random() * Math.PI;
|
351 |
+
scene.add(marking);
|
352 |
}
|
353 |
+
console.log("Cave system generated:", caveSystem); // Debug
|
354 |
}
|
355 |
+
|
356 |
+
function generateChunk(chunkX, chunkZ) {
|
357 |
+
const chunk = new THREE.Group();
|
358 |
+
chunk.chunkPosition = { x: chunkX, z: chunkZ };
|
359 |
+
|
360 |
+
const startX = chunkX * CHUNK_SIZE;
|
361 |
+
const startZ = chunkZ * CHUNK_SIZE;
|
362 |
+
|
363 |
+
// Ground
|
364 |
+
const ground = new THREE.Mesh(
|
365 |
+
new THREE.PlaneGeometry(CHUNK_SIZE, CHUNK_SIZE),
|
366 |
+
new THREE.MeshStandardMaterial({ color: 0x33aa33, roughness: 0.8 })
|
367 |
+
);
|
368 |
+
ground.rotation.x = -Math.PI / 2;
|
369 |
+
ground.position.set(startX + CHUNK_SIZE / 2, 0, startZ + CHUNK_SIZE / 2);
|
370 |
+
ground.receiveShadow = true;
|
371 |
+
chunk.add(ground);
|
372 |
+
|
373 |
+
// Trees and vegetation
|
374 |
+
for (let x = startX; x < startX + CHUNK_SIZE; x++) {
|
375 |
+
for (let z = startZ; z < startZ + CHUNK_SIZE; z++) {
|
376 |
+
const treeDensity = simplex.noise2D(x * 0.02, z * 0.02);
|
377 |
+
const vegetationDensity = simplex.noise2D(x * 0.1, z * 0.1);
|
378 |
+
|
379 |
+
if (treeDensity > TREE_DENSITY) {
|
380 |
+
createTree(x, z, chunk);
|
381 |
+
}
|
382 |
+
if (vegetationDensity > VEGETATION_DENSITY) {
|
383 |
+
createVegetation(x, z, chunk);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
384 |
}
|
385 |
}
|
386 |
+
}
|
387 |
+
|
388 |
+
scene.add(chunk);
|
389 |
+
// console.log("Chunk generated:", chunk); // Debugging: Check if the chunk is created
|
390 |
+
return chunk;
|
391 |
+
}
|
392 |
+
|
393 |
+
function updateChunks() {
|
394 |
+
const playerChunkX = Math.floor(camera.position.x / CHUNK_SIZE);
|
395 |
+
const playerChunkZ = Math.floor(camera.position.z / CHUNK_SIZE);
|
396 |
+
|
397 |
+
if (playerChunkX === player.currentChunk.x && playerChunkZ === player.currentChunk.z && !isTransitioning) {
|
398 |
+
return;
|
399 |
+
}
|
400 |
+
|
401 |
+
isTransitioning = true;
|
402 |
+
|
403 |
+
player.currentChunk.x = playerChunkX;
|
404 |
+
player.currentChunk.z = playerChunkZ;
|
405 |
+
|
406 |
+
const chunksToLoad = {};
|
407 |
+
for (let x = playerChunkX - VIEW_DISTANCE; x <= playerChunkX + VIEW_DISTANCE; x++) {
|
408 |
+
for (let z = playerChunkZ - VIEW_DISTANCE; z <= playerChunkZ + VIEW_DISTANCE; z++) {
|
409 |
+
chunksToLoad[`${x},${z}`] = true; // Mark for loading
|
410 |
+
}
|
411 |
+
}
|
412 |
+
|
413 |
+
// Unload chunks *before* loading new ones
|
414 |
+
for (const chunkId in loadedChunks) {
|
415 |
+
if (!chunksToLoad[chunkId]) {
|
416 |
+
const chunk = loadedChunks[chunkId];
|
417 |
+
// Return objects to pools
|
418 |
+
chunk.children.forEach(child => {
|
419 |
+
if (child instanceof THREE.Mesh) {
|
420 |
+
if (child.geometry instanceof THREE.CylinderGeometry) { // It's likely a tree trunk
|
421 |
+
let tree = objectPools.trees.find(t => t.trunk === child);
|
422 |
+
if (tree) returnObjectToPool('trees', tree);
|
423 |
+
} else if (child.geometry instanceof THREE.SphereGeometry) { // Could be leaves or vegetation
|
424 |
+
if (child.material.color.equals(new THREE.Color(0x227722))) { // Likely leaves
|
425 |
+
let tree = objectPools.trees.find(t => t.leaves === child);
|
426 |
+
if (tree) returnObjectToPool('trees', tree);
|
427 |
+
} else { // Likely vegetation
|
428 |
+
let vegetation = objectPools.vegetation.find(v => v === child);
|
429 |
+
if (vegetation) returnObjectToPool('vegetation', vegetation);
|
430 |
+
}
|
431 |
+
}
|
432 |
+
}
|
433 |
+
});
|
434 |
+
scene.remove(chunk);
|
435 |
+
delete loadedChunks[chunkId];
|
436 |
+
}
|
437 |
+
}
|
438 |
+
|
439 |
+
// Asynchronously load/generate chunks
|
440 |
+
const chunkPromises = [];
|
441 |
+
for (const chunkId in chunksToLoad) {
|
442 |
+
if (!loadedChunks[chunkId]) {
|
443 |
+
const [x, z] = chunkId.split(',').map(Number);
|
444 |
+
chunkPromises.push(
|
445 |
+
new Promise(resolve => {
|
446 |
+
// Simulate async loading (replace with actual loading/generation)
|
447 |
+
setTimeout(() => {
|
448 |
+
loadedChunks[chunkId] = generateChunk(x, z);
|
449 |
+
resolve();
|
450 |
+
}, 0);
|
451 |
+
})
|
452 |
+
);
|
453 |
+
}
|
454 |
+
}
|
455 |
+
|
456 |
+
// Wait for ALL chunks to be loaded/generated
|
457 |
+
Promise.all(chunkPromises)
|
458 |
+
.then(() => {
|
459 |
+
isTransitioning = false;
|
460 |
+
document.getElementById('loading-indicator').style.display = 'none';
|
461 |
})
|
462 |
+
.catch(error => {
|
463 |
+
console.error("Error loading chunks:", error);
|
464 |
+
isTransitioning = false; // Ensure isTransitioning is reset
|
465 |
+
document.getElementById('loading-indicator').style.display = 'none';
|
466 |
+
});
|
467 |
+
}
|
468 |
+
|
469 |
+
function update(delta) {
|
470 |
+
updatePlayer(delta);
|
471 |
+
updateEnvironment();
|
472 |
+
// updateChunks(); // Moved to be called *after* initial setup, and only when not transitioning
|
473 |
+
if (!isTransitioning) {
|
474 |
+
updateChunks();
|
475 |
+
}
|
476 |
+
}
|
477 |
+
|
478 |
+
function updatePlayer(delta) {
|
479 |
+
if (!document.pointerLockElement && !joystick.isActive) return; // Don't move if no pointer lock OR touch
|
480 |
+
|
481 |
+
const direction = new THREE.Vector3();
|
482 |
+
|
483 |
+
// Keyboard controls (existing)
|
484 |
+
if (controls.forward) direction.z -= 1;
|
485 |
+
if (controls.backward) direction.z += 1;
|
486 |
+
if (controls.left) direction.x -= 1;
|
487 |
+
if (controls.right) direction.x += 1;
|
488 |
+
|
489 |
+
|
490 |
+
// Touch controls (joystick)
|
491 |
+
if (joystick.isActive) {
|
492 |
+
direction.x += joystick.deltaX;
|
493 |
+
direction.z += joystick.deltaY;
|
494 |
+
}
|
495 |
+
|
496 |
+
|
497 |
+
direction.normalize();
|
498 |
+
|
499 |
+
if ((controls.sprint && player.stamina > 0 && direction.length() > 0) || (joystick.isActive && player.stamina >0) ) {
|
500 |
+
player.speed.current = player.speed.sprint;
|
501 |
+
player.stamina = Math.max(0, player.stamina - delta * 30);
|
502 |
+
} else {
|
503 |
+
player.speed.current = player.speed.walk;
|
504 |
+
player.stamina = Math.min(100, player.stamina + delta * 10);
|
505 |
+
}
|
506 |
+
|
507 |
+
document.getElementById('stamina-bar').style.width = player.stamina + '%';
|
508 |
+
|
509 |
+
if (direction.length() > 0) {
|
510 |
+
const moveX = direction.x * player.speed.current * delta;
|
511 |
+
const moveZ = direction.z * player.speed.current * delta;
|
512 |
+
|
513 |
+
camera.position.x += moveX * Math.cos(camera.rotation.y) + moveZ * Math.sin(camera.rotation.y);
|
514 |
+
camera.position.z += moveZ * Math.cos(camera.rotation.y) - moveX * Math.sin(camera.rotation.y);
|
515 |
+
|
516 |
+
player.headBob.value += delta * player.headBob.speed;
|
517 |
+
camera.position.y = player.position.y + Math.sin(player.headBob.value) * player.headBob.intensity;
|
518 |
+
}
|
519 |
+
|
520 |
+
if (isFlashlightOn) {
|
521 |
+
flashlight.position.copy(camera.position);
|
522 |
+
flashlight.rotation.copy(camera.rotation);
|
523 |
+
}
|
524 |
+
}
|
525 |
+
function updateEnvironment() {
|
526 |
+
const inCaveNow = camera.position.z < CAVE_ENTRANCE.z; // Simplified cave check
|
527 |
+
if (inCaveNow !== player.inCave) {
|
528 |
+
player.inCave = inCaveNow;
|
529 |
+
transitionEnvironment();
|
530 |
+
}
|
531 |
+
}
|
532 |
+
|
533 |
+
function transitionEnvironment() {
|
534 |
+
if (player.inCave) {
|
535 |
+
scene.fog = new THREE.FogExp2(0x000000, 0.15);
|
536 |
+
scene.background = new THREE.Color(0x000000);
|
537 |
+
} else {
|
538 |
+
scene.fog = null;
|
539 |
+
scene.background = new THREE.Color(0x88ccff);
|
540 |
+
}
|
541 |
+
}
|
542 |
+
|
543 |
+
function setupLighting() {
|
544 |
+
const ambientLight = new THREE.AmbientLight(0xffffff); // White ambient light
|
545 |
+
scene.add(ambientLight);
|
546 |
+
|
547 |
+
const sunLight = new THREE.DirectionalLight(0xffffbb, 2); // Stronger sunlight
|
548 |
+
sunLight.position.set(50, 100, 50);
|
549 |
+
sunLight.castShadow = true;
|
550 |
+
|
551 |
+
// Increase shadow map size
|
552 |
+
sunLight.shadow.mapSize.width = 1024;
|
553 |
+
sunLight.shadow.mapSize.height = 1024;
|
554 |
+
scene.add(sunLight);
|
555 |
+
|
556 |
+
|
557 |
+
flashlight = new THREE.SpotLight(0xffffff, 1);
|
558 |
+
flashlight.angle = Math.PI / 6;
|
559 |
+
flashlight.penumbra = 0.1;
|
560 |
+
flashlight.decay = 2;
|
561 |
+
flashlight.distance = 30;
|
562 |
+
flashlight.visible = false;
|
563 |
+
camera.add(flashlight);
|
564 |
+
scene.add(camera); // Important: Add the camera to the scene!
|
565 |
+
}
|
566 |
+
|
567 |
+
function setupParticles() {
|
568 |
+
const particleCount = 1000;
|
569 |
+
const positions = new Float32Array(particleCount * 3);
|
570 |
+
const colors = new Float32Array(particleCount * 3);
|
571 |
+
|
572 |
+
for (let i = 0; i < particleCount * 3; i += 3) {
|
573 |
+
positions[i] = Math.random() * 200 - 100;
|
574 |
+
positions[i + 1] = Math.random() * 20;
|
575 |
+
positions[i + 2] = Math.random() * 200 - 100;
|
576 |
+
|
577 |
+
colors[i] = 1;
|
578 |
+
colors[i + 1] = 1;
|
579 |
+
colors[i + 2] = 0.8;
|
580 |
+
}
|
581 |
+
|
582 |
+
const geometry = new THREE.BufferGeometry();
|
583 |
+
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
584 |
+
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
|
585 |
+
const material = new THREE.PointsMaterial({ size: 0.1, vertexColors: true, transparent: true, opacity: 0.6 });
|
586 |
+
const particles = new THREE.Points(geometry, material);
|
587 |
+
scene.add(particles);
|
588 |
+
}
|
589 |
+
|
590 |
+
function setupAudio() {
|
591 |
+
// TODO: Implement audio
|
592 |
+
}
|
593 |
+
|
594 |
+
function animate() {
|
595 |
+
requestAnimationFrame(animate);
|
596 |
+
const delta = clock.getDelta();
|
597 |
+
update(delta);
|
598 |
+
renderer.render(scene, camera);
|
599 |
+
}
|
600 |
+
|
601 |
+
// --- Keyboard controls (existing) ---
|
602 |
+
function onKeyDown(event) {
|
603 |
+
switch (event.code) {
|
604 |
+
case 'KeyW': controls.forward = true; break;
|
605 |
+
case 'KeyS': controls.backward = true; break;
|
606 |
+
case 'KeyA': controls.left = true; break;
|
607 |
+
case 'KeyD': controls.right = true; break;
|
608 |
+
case 'ShiftLeft': controls.sprint = true; break;
|
609 |
+
case 'KeyF': toggleFlashlight(); break;
|
610 |
+
}
|
611 |
}
|
612 |
+
|
613 |
+
function onKeyUp(event) {
|
614 |
+
switch (event.code) {
|
615 |
+
case 'KeyW': controls.forward = false; break;
|
616 |
+
case 'KeyS': controls.backward = false; break;
|
617 |
+
case 'KeyA': controls.left = false; break;
|
618 |
+
case 'KeyD': controls.right = false; break;
|
619 |
+
case 'ShiftLeft': controls.sprint = false; break;
|
620 |
+
}
|
621 |
+
}
|
622 |
+
|
623 |
+
function onMouseMove(event) {
|
624 |
+
if (document.pointerLockElement) {
|
625 |
+
camera.rotation.y -= event.movementX * 0.002;
|
626 |
+
camera.rotation.x = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, camera.rotation.x - event.movementY * 0.002));
|
627 |
+
}
|
628 |
+
}
|
629 |
+
|
630 |
+
function toggleFlashlight() {
|
631 |
+
isFlashlightOn = !isFlashlightOn;
|
632 |
+
flashlight.visible = isFlashlightOn;
|
633 |
+
document.getElementById('flashlight-status').textContent = `Flashlight [F] ${isFlashlightOn ? 'ON' : 'OFF'}`;
|
634 |
+
}
|
635 |
+
|
636 |
+
// --- Touch controls ---
|
637 |
+
function onTouchStart(event) {
|
638 |
+
event.preventDefault(); // Prevent other behaviors (like scrolling)
|
639 |
+
|
640 |
+
// For simplicity, we'll only handle the first touch point
|
641 |
+
const touch = event.touches[0];
|
642 |
+
|
643 |
+
if (touch.target.closest("#joystick-zone")) {
|
644 |
+
joystick.isActive = true;
|
645 |
+
touchStartX = touch.clientX;
|
646 |
+
touchStartY = touch.clientY;
|
647 |
+
|
648 |
+
//Put knob at center
|
649 |
+
joystickKnob.style.left = `0px`;
|
650 |
+
joystickKnob.style.top = `0px`;
|
651 |
+
|
652 |
+
} else { // General screen touch (for rotation, like mouse look)
|
653 |
+
touchStartX = touch.clientX;
|
654 |
+
touchStartY = touch.clientY;
|
655 |
+
}
|
656 |
+
}
|
657 |
+
|
658 |
+
let joystickKnob = document.getElementById("joystick-knob");
|
659 |
+
let joystickZone = document.getElementById("joystick-zone");
|
660 |
+
|
661 |
+
function onTouchMove(event) {
|
662 |
+
event.preventDefault();
|
663 |
+
const touch = event.touches[0];
|
664 |
+
|
665 |
+
if (joystick.isActive) {
|
666 |
+
// Calculate the distance the touch has moved from the starting point
|
667 |
+
const deltaX = touch.clientX - touchStartX;
|
668 |
+
const deltaY = touch.clientY - touchStartY;
|
669 |
+
|
670 |
+
// Limit delta, so it is circular
|
671 |
+
let length = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
672 |
+
const maxLength = 75 - 30; // radius of outer - radius of inner
|
673 |
+
if(length > maxLength) {
|
674 |
+
const angle = Math.atan2(deltaY, deltaX);
|
675 |
+
joystick.deltaX = Math.cos(angle) * maxLength / (maxLength*2);
|
676 |
+
joystick.deltaY = Math.sin(angle) * maxLength/ (maxLength*2);
|
677 |
+
// Move the joystick knob visually
|
678 |
+
joystickKnob.style.left = `${Math.cos(angle) * maxLength}px`;
|
679 |
+
joystickKnob.style.top = `${Math.sin(angle) * maxLength}px`;
|
680 |
+
|
681 |
+
} else {
|
682 |
+
joystick.deltaX = deltaX / (maxLength*2) ; // Normalize for movement
|
683 |
+
joystick.deltaY = deltaY / (maxLength*2);
|
684 |
+
joystickKnob.style.left = `${deltaX}px`;
|
685 |
+
joystickKnob.style.top = `${deltaY}px`;
|
686 |
+
}
|
687 |
+
|
688 |
+
|
689 |
+
|
690 |
+
} else { // Rotation (like mouse look)
|
691 |
+
const movementX = touch.clientX - touchStartX;
|
692 |
+
const movementY = touch.clientY - touchStartY;
|
693 |
+
camera.rotation.y -= movementX * 0.005; // Adjust sensitivity as needed
|
694 |
+
camera.rotation.x = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, camera.rotation.x - movementY * 0.005));
|
695 |
+
|
696 |
+
// Reset for next movement calculation. Keep camera rotation smooth.
|
697 |
+
touchStartX = touch.clientX;
|
698 |
+
touchStartY = touch.clientY;
|
699 |
+
}
|
700 |
+
}
|
701 |
+
|
702 |
+
function onTouchEnd(event) {
|
703 |
+
if(joystick.isActive){
|
704 |
+
joystick.isActive = false;
|
705 |
+
joystick.deltaX = 0;
|
706 |
+
joystick.deltaY = 0;
|
707 |
+
|
708 |
+
// Reset joystick visuals
|
709 |
+
joystickKnob.style.left = `0px`;
|
710 |
+
joystickKnob.style.top = `0px`;
|
711 |
+
}
|
712 |
+
|
713 |
+
}
|
714 |
+
|
715 |
+
init();
|
716 |
+
animate();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
717 |
</script>
|
718 |
</body>
|
719 |
</html>
|