kimhyunwoo commited on
Commit
e240c2d
·
verified ·
1 Parent(s): ba520bb

Update index.html

Browse files
Files changed (1) hide show
  1. 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 = new SimplexNoise(); // SimplexNoise 인스턴스 생성
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
- 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>
 
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>