FARMANT

STUDIOS

let scene, camera, renderer, simplex1, simplex2, simplex3, worldSeed; let sunLight, ambientLight, hemiLight, moonLight, playerTorch; let isNightPhase = false; let clock = new THREE.Clock(); let animationFrameId = null; let gameActive = false; let isPaused = false; const CHUNK_SIZE = 12; let VIEW_DISTANCE = 3; const SEA_LEVEL = 18; const MAX_WORLD_HEIGHT = 54; const worldBlocks = new Map(); const chunkMeshes = new Map(); const generatedChunks = new Set(); let activeChunks = []; // 50x50 Boyutunda Üçlü Korku Biyomları const B1_MIN_X = 35, B1_MAX_X = 85; const B1_MIN_Z = 35, B1_MAX_Z = 85; const B2_MIN_X = 135, B2_MAX_X = 185; const B2_MIN_Z = 35, B2_MAX_Z = 85; const B3_MIN_X = 235, B3_MAX_X = 285; const B3_MIN_Z = 35, B3_MAX_Z = 85; // 40x40 Boyutunda Tam 20 Adet "Eski Kuleler" (Old Towers) Biyomu Merkez Koordinatları (Her 100 Blokta Bir) const TOWER_BIOMES = []; for (let i = 0; i < 5; i++) { for (let j = 0; j < 4; j++) { TOWER_BIOMES.push({ x: i * 100, z: j * 100 }); } } let insideGrayBiome = false; let insideGrayBiomeId = 0; let grayBiomeTrees = []; let creepyObservers = []; let lastScreamTime = 0; // Eski Kuleler Biyomu Değişkenleri let insideTowerBiome = false; let insideTowerBiomeIdx = -1; let towerGuards = []; let biomeAggroTriggered = new Array(20).fill(false); let lastTowerScreamTime = 0; let frames = 0, lastTime = performance.now(), currentFps = 60; let lowFpsCount = 0; // Katil Balık Yapay Zekası ve Durumu const killerFishList = []; let fishDebuffTimer = 0; let fishDebuffFlashTimer = 0; let fishDebuffScreenActive = false; // Gece - Gündüz Yörünge Zamanları const DAY_DURATION = 240; // 4 Dakika const AFTERNOON_DURATION = 120; // 2 Dakika const EVENING_DURATION = 50; // 50 Saniye const NIGHT_DURATION = 240; // 4 Dakika const TOTAL_CYCLE_DURATION = DAY_DURATION + AFTERNOON_DURATION + EVENING_DURATION + NIGHT_DURATION; let dayNightTimer = 0; let isBloodMoon = false; let hasCheckedBloodMoonThisNight = false; let blackoutTimer = 0; let lastBlackoutTrigger = 0; let starsField = null; let cloudsGroup = null; let cloudDataList = []; let mergedCloudRainZones = []; let rainParticlesGroup = null; let isRainActive = false; let cloudMergeCheckTimer = 0; const sandRainHits = new Map(); let horrorGiant = null; const GUARD_NOTE_TEXT = 'Canavarlar bizim yerleşim yerine saldırdı. Sadece bize değil, bize benzeyenlere de saldırdılar. Burayı sonsuza kadar korumaya yemin ettik.'; let sunMesh = null; let moonMesh = null; let moonMat = null; const player = { pos: new THREE.Vector3(0, 40, 0), vel: new THREE.Vector3(0, 0, 0), speed: 0.08, runMultiplier: 1.8, crouchMultiplier: 0.5, jumpForce: 0.18, onGround: false, inWater: false, isUnderwater: false, isCrouching: false, height: 1.8, radius: 0.35, isSprinting: false, stepAccumulator: 0, spawnPos: new THREE.Vector3(0, 40, 0) }; const keys = { KeyW: false, KeyS: false, KeyA: false, KeyD: false, Space: false, ShiftLeft: false, ControlLeft: false, KeyB: false, KeyN: false }; const INVENTORY_SIZE = 9; const MAX_HEALTH = 9; const PLAYER_ID = 'local'; let inventory = []; let selectedSlot = 0; let playerHealth = MAX_HEALTH; let playerDamageCooldown = 0; const droppedItems = []; let isMouseBreakHeld = false; let breakTarget = null; let breakProgress = 0; let attackCooldown = 0; const BREAK_TIMES = { leaf: 0.2, grayLeaf: 0.2, dirt: 0.1, grayDirt: 0.1, sand: 0.1, slightlyWetSand: 0.1, wetSand: 0.1, grass: 0.9, grayGrass: 0.9, wood: 14, grayWood: 14, stone: 20, grayStone: 20, chest: 10 }; const BLOCK_COLORS = { grass: '#2e8b57', dirt: '#5c4033', sand: '#EDC9AF', slightlyWetSand: '#756030', wetSand: '#5a4825', stone: '#708090', wood: '#6b3a12', leaf: '#1b4d3e', chest: '#ffd700', grayGrass: '#222222', grayDirt: '#151515', grayStone: '#0c0c0c', grayWood: '#0f0f0f', grayLeaf: '#1a1a1a', guardNote: '#f5d042' }; const PICKUP_TYPES = ['grass','dirt','sand','slightlyWetSand','wetSand','stone','wood','leaf','chest','grayGrass','grayDirt','grayStone','grayWood','grayLeaf','guardNote']; function initInventoryState() { inventory = Array(INVENTORY_SIZE).fill(null); selectedSlot = 0; playerHealth = MAX_HEALTH; playerDamageCooldown = 0; breakTarget = null; breakProgress = 0; attackCooldown = 0; updateHealthUI(); updateInventoryUI(); } function updateHealthUI() { const bar = document.getElementById('health-bar'); if (!bar) return; bar.innerHTML = ''; for (let i = 0; i < MAX_HEALTH; i++) { const h = document.createElement('div'); h.className = 'heart' + (i < playerHealth ? '' : ' empty'); bar.appendChild(h); } } function updateInventoryUI() { const bar = document.getElementById('inventory-bar'); if (!bar) return; bar.innerHTML = ''; for (let i = 0; i < INVENTORY_SIZE; i++) { const slot = document.createElement('div'); slot.className = 'inv-slot' + (i === selectedSlot ? ' selected' : ''); const item = inventory[i]; if (item) { const icon = document.createElement('div'); icon.className = 'slot-icon' + (item.type === 'guardNote' ? ' note-icon' : ''); if (item.type !== 'guardNote') { icon.style.background = BLOCK_COLORS[item.type] || '#888'; } slot.appendChild(icon); const count = document.createElement('span'); count.className = 'slot-count'; count.innerText = String(item.count); slot.appendChild(count); } bar.appendChild(slot); } } function addItemToInventory(type, amount = 1) { if (!PICKUP_TYPES.includes(type)) return; if (type === 'guardNote') { for (let i = 0; i < INVENTORY_SIZE; i++) { if (inventory[i] && inventory[i].type === 'guardNote') { inventory[i].count += amount; updateInventoryUI(); return; } } } for (let i = 0; i < INVENTORY_SIZE; i++) { if (inventory[i] && inventory[i].type === type) { inventory[i].count += amount; updateInventoryUI(); return; } } if (inventory[0] === null) { inventory[0] = { type, count: amount }; } else { const emptyIdx = inventory.findIndex(s => s === null); if (emptyIdx >= 0) { inventory[emptyIdx] = { type, count: amount }; } else return; } updateInventoryUI(); } function removeOneFromSlot(slotIdx) { const item = inventory[slotIdx]; if (!item) return null; const type = item.type; item.count--; if (item.count <= 0) inventory[slotIdx] = null; updateInventoryUI(); return type; } function damagePlayer(amount) { if (playerDamageCooldown > 0 || isPaused) return; playerHealth = Math.max(0, playerHealth - amount); playerDamageCooldown = 0.8; updateHealthUI(); audio.playBeep(120, 'sawtooth', 0.15, 0.2); if (playerHealth <= 0) { player.pos.copy(player.spawnPos); player.vel.set(0, 0, 0); playerHealth = MAX_HEALTH; playerDamageCooldown = 2; updateHealthUI(); } } function getBreakTime(type) { return BREAK_TIMES[type] || 4; } function isNearWaterAt(wx, wz, h) { if (h < SEA_LEVEL) return true; for (let dx = -4; dx <= 4; dx++) { for (let dz = -4; dz <= 4; dz++) { if (dx === 0 && dz === 0) continue; const nh = getTerrainHeight(wx + dx, wz + dz); if (nh < SEA_LEVEL) return true; } } return false; } function createDroppedItemMesh(type) { if (type === 'guardNote') { const geo = new THREE.BoxGeometry(0.28, 0.32, 0.04); const mat = new THREE.MeshLambertMaterial({ color: 0xf5d042 }); return new THREE.Mesh(geo, mat); } const color = BLOCK_COLORS[type] || '#888888'; const geo = new THREE.BoxGeometry(0.28, 0.28, 0.28); const mat = new THREE.MeshLambertMaterial({ color: new THREE.Color(color) }); return new THREE.Mesh(geo, mat); } function openGuardNoteModal() { const modal = document.getElementById('guard-note-modal'); const textEl = document.getElementById('guard-note-text'); if (!modal || !textEl) return; textEl.innerText = GUARD_NOTE_TEXT; modal.classList.remove('hidden'); modal.classList.add('flex'); isPaused = true; document.exitPointerLock(); } function closeGuardNoteModal() { const modal = document.getElementById('guard-note-modal'); if (!modal) return; modal.classList.add('hidden'); modal.classList.remove('flex'); isPaused = false; } function spawnDroppedItem(type, x, y, z, count = 1, ownerId = null) { const mesh = createDroppedItemMesh(type); mesh.position.set(x + 0.5, y + 0.35, z + 0.5); scene.add(mesh); droppedItems.push({ mesh, type, count, bobPhase: Math.random() * Math.PI * 2, baseY: y + 0.35, ownerId, rotSpeed: 1.5 + Math.random() * 1.5 }); } function updateDroppedItems(dt) { for (let i = droppedItems.length - 1; i >= 0; i--) { const item = droppedItems[i]; item.bobPhase += dt * 3; item.mesh.position.y = item.baseY + Math.sin(item.bobPhase) * 0.12; item.mesh.rotation.y += item.rotSpeed * dt; const dist = item.mesh.position.distanceTo(player.pos); if (dist < 1.6) { if (item.ownerId && item.ownerId === PLAYER_ID) continue; addItemToInventory(item.type, item.count); scene.remove(item.mesh); droppedItems.splice(i, 1); } } } function dropSelectedItem(throwFar = false) { const item = inventory[selectedSlot]; if (!item) return; const type = removeOneFromSlot(selectedSlot); if (!type) return; const fwd = new THREE.Vector3(0, 0, -1).applyQuaternion(camera.quaternion); fwd.y = 0; fwd.normalize(); const dist = throwFar ? 2.5 : 1.2; const px = player.pos.x + fwd.x * dist; const pz = player.pos.z + fwd.z * dist; const py = Math.floor(player.pos.y); spawnDroppedItem(type, Math.floor(px), py, Math.floor(pz), 1, throwFar ? PLAYER_ID : null); } window.addEventListener('DOMContentLoaded', () => { const wakeupAudio = () => { audio.init(); window.removeEventListener('click', wakeupAudio); window.removeEventListener('touchstart', wakeupAudio); }; window.addEventListener('click', wakeupAudio); window.addEventListener('touchstart', wakeupAudio); setTimeout(() => { const splash = document.getElementById('splash-screen'); if (splash) { splash.style.opacity = '0'; setTimeout(() => { splash.style.display = 'none'; }, 1500); } }, 4000); }); // Notepad UI Dinleyicileri document.getElementById('btn-notes').addEventListener('click', (e) => { e.stopPropagation(); document.getElementById('notes-modal').classList.remove('hidden'); document.getElementById('notes-modal').classList.add('flex'); }); document.getElementById('btn-close-notes').addEventListener('click', (e) => { e.stopPropagation(); document.getElementById('notes-modal').classList.add('hidden'); document.getElementById('notes-modal').classList.remove('flex'); }); const guardNoteCloseBtn = document.getElementById('guard-note-close'); if (guardNoteCloseBtn) { guardNoteCloseBtn.addEventListener('click', (e) => { e.stopPropagation(); closeGuardNoteModal(); }); } class SoundSynth { constructor() { this.ctx = null; this.droneOsc = null; this.droneGain = null; this.towerOsc = null; this.towerGain = null; } init() { if (this.ctx) return; this.ctx = new (window.AudioContext || window.webkitAudioContext)(); } playBeep(freq, type = 'sine', duration = 0.1, vol = 0.1) { if (!this.ctx) return; try { const osc = this.ctx.createOscillator(); const gain = this.ctx.createGain(); osc.connect(gain); gain.connect(this.ctx.destination); osc.type = type; osc.frequency.setValueAtTime(freq, this.ctx.currentTime); gain.gain.setValueAtTime(vol, this.ctx.currentTime); gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + duration); osc.start(); osc.stop(this.ctx.currentTime + duration); } catch (e) {} } playPlaceTık() { this.playBeep(250, 'sine', 0.04, 0.25); } playBreakTıkTık() { this.playBeep(220, 'sine', 0.03, 0.22); setTimeout(() => { this.playBeep(185, 'sine', 0.03, 0.2); }, 70); } playStepSound(type) { if (!this.ctx) return; const now = this.ctx.currentTime; if (type === 'grass') { const osc = this.ctx.createOscillator(); const gain = this.ctx.createGain(); osc.type = 'triangle'; osc.frequency.setValueAtTime(160, now); osc.frequency.exponentialRampToValueAtTime(60, now + 0.08); gain.gain.setValueAtTime(0.14, now); gain.gain.exponentialRampToValueAtTime(0.001, now + 0.11); osc.connect(gain); gain.connect(this.ctx.destination); osc.start(now); osc.stop(now + 0.12); } else if (type === 'dirt') { const osc = this.ctx.createOscillator(); const gain = this.ctx.createGain(); osc.type = 'sine'; osc.frequency.setValueAtTime(110, now); osc.frequency.exponentialRampToValueAtTime(55, now + 0.08); gain.gain.setValueAtTime(0.18, now); gain.gain.exponentialRampToValueAtTime(0.001, now + 0.11); osc.connect(gain); gain.connect(this.ctx.destination); osc.start(now); osc.stop(now + 0.12); } } playWaterSound() { if (!this.ctx) return; const now = this.ctx.currentTime; const osc = this.ctx.createOscillator(); const gain = this.ctx.createGain(); osc.type = 'sine'; osc.frequency.setValueAtTime(75, now); osc.frequency.linearRampToValueAtTime(170, now + 0.35); gain.gain.setValueAtTime(0.25, now); gain.gain.exponentialRampToValueAtTime(0.001, now + 0.4); osc.connect(gain); gain.connect(this.ctx.destination); osc.start(now); osc.stop(now + 0.45); } startScaryDrone() { if (!this.ctx || this.droneOsc) return; try { this.droneOsc = this.ctx.createOscillator(); this.droneGain = this.ctx.createGain(); this.droneOsc.type = 'sawtooth'; this.droneOsc.frequency.setValueAtTime(55, this.ctx.currentTime); this.droneGain.gain.setValueAtTime(0.12, this.ctx.currentTime); this.droneOsc.connect(this.droneGain); this.droneGain.connect(this.ctx.destination); this.droneOsc.start(); } catch(e) {} } stopScaryDrone() { if (this.droneOsc) { try { this.droneOsc.stop(); this.droneOsc.disconnect(); } catch(e) {} this.droneOsc = null; this.droneGain = null; } } startTowerHum() { if (!this.ctx || this.towerOsc) return; try { this.towerOsc = this.ctx.createOscillator(); this.towerGain = this.ctx.createGain(); this.towerOsc.type = 'sine'; this.towerOsc.frequency.setValueAtTime(90, this.ctx.currentTime); this.towerGain.gain.setValueAtTime(0.18, this.ctx.currentTime); this.towerOsc.connect(this.towerGain); this.towerGain.connect(this.ctx.destination); this.towerOsc.start(); } catch(e) {} } stopTowerHum() { if (this.towerOsc) { try { this.towerOsc.stop(); this.towerOsc.disconnect(); } catch(e) {} this.towerOsc = null; this.towerGain = null; } } playScream(intensity = 1) { if (!this.ctx) return; try { const now = this.ctx.currentTime; const duration = intensity === 2 ? 3.5 : 1.2; const bufferSize = this.ctx.sampleRate * duration; const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate); const data = buffer.getChannelData(0); for (let i = 0; i < bufferSize; i++) { data[i] = Math.random() * 2 - 1; } const noiseNode = this.ctx.createBufferSource(); noiseNode.buffer = buffer; const filter = this.ctx.createBiquadFilter(); filter.type = 'bandpass'; filter.Q.setValueAtTime(10, now); filter.frequency.setValueAtTime(intensity === 2 ? 1500 : 1100, now); filter.frequency.exponentialRampToValueAtTime(intensity === 2 ? 150 : 350, now + duration); const osc1 = this.ctx.createOscillator(); osc1.type = 'sawtooth'; osc1.frequency.setValueAtTime(intensity === 2 ? 800 : 450, now); osc1.frequency.linearRampToValueAtTime(intensity === 2 ? 100 : 180, now + duration); const osc2 = this.ctx.createOscillator(); osc2.type = 'square'; osc2.frequency.setValueAtTime(intensity === 2 ? 1000 : 600, now); osc2.frequency.linearRampToValueAtTime(intensity === 2 ? 60 : 100, now + duration); const noiseGain = this.ctx.createGain(); noiseGain.gain.setValueAtTime(intensity === 2 ? 0.9 : 0.3, now); noiseGain.gain.exponentialRampToValueAtTime(0.001, now + duration); const oscGain = this.ctx.createGain(); oscGain.gain.setValueAtTime(intensity === 2 ? 0.4 : 0.12, now); oscGain.gain.exponentialRampToValueAtTime(0.001, now + duration * 0.8); noiseNode.connect(filter); filter.connect(noiseGain); noiseGain.connect(this.ctx.destination); osc1.connect(oscGain); osc2.connect(oscGain); oscGain.connect(this.ctx.destination); noiseNode.start(now); noiseNode.stop(now + duration); osc1.start(now); osc1.stop(now + duration); osc2.start(now); osc2.stop(now + duration); } catch (e) {} } } const audio = new SoundSynth(); document.getElementById('btn-create').addEventListener('click', (e) => { e.stopPropagation(); audio.init(); document.getElementById('menu').style.opacity = '0'; setTimeout(() => { document.getElementById('menu').style.display = 'none'; document.getElementById('ui-overlay').style.display = 'block'; document.getElementById('gpu-monitor').style.display = 'block'; document.getElementById('crosshair').style.display = 'block'; document.getElementById('hud-bottom').style.display = 'flex'; initGame(); requestPointerLock(); }, 700); }); function requestPointerLock() { if (gameActive && document.pointerLockElement !== document.body) { document.body.requestPointerLock(); } } document.getElementById('btn-resume').addEventListener('click', (e) => { e.stopPropagation(); isPaused = false; document.getElementById('pause-menu').classList.add('hidden'); requestPointerLock(); }); document.getElementById('btn-back-to-menu').addEventListener('click', (e) => { e.stopPropagation(); document.exitPointerLock(); resetGameToMenu(); }); document.getElementById('btn-error-back').addEventListener('click', (e) => { e.stopPropagation(); document.getElementById('error-screen').classList.add('hidden'); resetGameToMenu(); }); function handlePointerLockChange() { if (!gameActive) return; if (document.pointerLockElement === document.body) { isPaused = false; document.getElementById('pause-menu').classList.add('hidden'); } else { isPaused = true; document.getElementById('pause-menu').classList.remove('hidden'); Object.keys(keys).forEach(k => keys[k] = false); } } document.addEventListener('pointerlockchange', handlePointerLockChange); function generate32BitSeed() { let seed32; try { const array = new Uint32Array(1); window.crypto.getRandomValues(array); seed32 = array[0] >>> 0; } catch (e) { seed32 = (Math.floor(Math.random() * 0x100000000)) >>> 0; } return { raw: seed32, seed32: seed32 }; } function chunkSeed(cx, cz, salt = 0) { let h = (worldSeed ^ salt) >>> 0; h = Math.imul(h ^ cx, 374761393) >>> 0; h = Math.imul(h ^ cz, 668265263) >>> 0; return h >>> 0; } function makeChunkRng(cx, cz, salt = 0) { let state = chunkSeed(cx, cz, salt) || 1; return function() { state = (Math.imul(state, 1664525) + 1013904223) >>> 0; return state / 4294967296; }; } function initGame() { gameActive = true; isPaused = false; dayNightTimer = 0; isBloodMoon = false; hasCheckedBloodMoonThisNight = false; lowFpsCount = 0; insideTowerBiome = false; insideTowerBiomeIdx = -1; biomeAggroTriggered.fill(false); const seeds = generate32BitSeed(); worldSeed = seeds.seed32; const seedEl = document.getElementById('ui-seed'); if (seedEl) seedEl.innerText = String(worldSeed >>> 0); simplex1 = new SimplexNoise((worldSeed).toString()); simplex2 = new SimplexNoise((worldSeed + 1).toString()); simplex3 = new SimplexNoise((worldSeed + 2).toString()); scene = new THREE.Scene(); const skyColor = 0xb0c0d0; scene.background = new THREE.Color(skyColor); scene.fog = new THREE.FogExp2(0x8a9ba8, 0.025); camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 1000); camera.rotation.order = 'YXZ'; renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: "high-performance" }); renderer.setSize(window.innerWidth, window.innerHeight); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.2)); renderer.shadowMap.enabled = true; renderer.shadowMap.type = THREE.PCFSoftShadowMap; document.body.appendChild(renderer.domElement); sunLight = new THREE.DirectionalLight(0xfff4e0, 1.2); sunLight.position.set(40, 100, 20); sunLight.castShadow = true; sunLight.shadow.mapSize.set(1024, 1024); sunLight.shadow.camera.near = 1; sunLight.shadow.camera.far = 180; sunLight.shadow.camera.left = -55; sunLight.shadow.camera.right = 55; sunLight.shadow.camera.top = 55; sunLight.shadow.camera.bottom = -55; sunLight.shadow.bias = -0.0008; scene.add(sunLight); sunLight.target = new THREE.Object3D(); scene.add(sunLight.target); moonLight = new THREE.DirectionalLight(0x8899cc, 0); moonLight.position.set(-40, 80, -20); moonLight.castShadow = false; moonLight.target = new THREE.Object3D(); scene.add(moonLight); scene.add(moonLight.target); ambientLight = new THREE.AmbientLight(0xffffff, 0.75); scene.add(ambientLight); hemiLight = new THREE.HemisphereLight(0xb8d4f0, 0x3d2817, 0.45); scene.add(hemiLight); playerTorch = new THREE.PointLight(0xfff8f0, 0, 5.5, 1.2); playerTorch.position.set(0, 0.25, 0); camera.add(playerTorch); scene.add(camera); initInventoryState(); const sunGeo = new THREE.SphereGeometry(6, 16, 16); const sunMat = new THREE.MeshBasicMaterial({ color: 0xfffebb, fog: false }); sunMesh = new THREE.Mesh(sunGeo, sunMat); scene.add(sunMesh); const moonGeo = new THREE.SphereGeometry(5, 16, 16); moonMat = new THREE.MeshBasicMaterial({ color: 0xdddddd, fog: false }); moonMesh = new THREE.Mesh(moonGeo, moonMat); scene.add(moonMesh); initMaterials(); initClouds(); initStars(); // Oyuncu doğrudan ilk "Eski Kuleler" biyomunun merkezinde başlar (evin önü) const firstTower = TOWER_BIOMES[0]; player.spawnPos.set(firstTower.x, 21, firstTower.z + 8); player.pos.copy(player.spawnPos); createCreepyObservers(); spawnTowerGuardMobs(); spawnHorrorGiant(); const spawnPx = Math.floor(player.pos.x / CHUNK_SIZE); const spawnPz = Math.floor(player.pos.z / CHUNK_SIZE); for (let x = spawnPx - VIEW_DISTANCE; x <= spawnPx + VIEW_DISTANCE; x++) { for (let z = spawnPz - VIEW_DISTANCE; z <= spawnPz + VIEW_DISTANCE; z++) { createChunk(x, z); } } window.addEventListener('keydown', handleKeyDown); window.addEventListener('keyup', handleKeyUp); window.addEventListener('mousemove', handleMouseMove); window.addEventListener('mousedown', handleMouseDown); window.addEventListener('mouseup', handleMouseUp); document.body.addEventListener('click', requestPointerLock); window.addEventListener('contextmenu', e => e.preventDefault()); animate(); } function initStars() { const starsGeo = new THREE.BufferGeometry(); const starsCount = 450; const starPositions = new Float32Array(starsCount * 3); for (let i = 0; i < starsCount * 3; i += 3) { const u = Math.random(); const v = Math.random(); const theta = u * 2.0 * Math.PI; const phi = Math.acos(2.0 * v - 1.0); const radius = 250; starPositions[i] = radius * Math.sin(phi) * Math.cos(theta); starPositions[i + 1] = Math.abs(radius * Math.sin(phi) * Math.sin(theta)); starPositions[i + 2] = radius * Math.cos(phi); } starsGeo.setAttribute('position', new THREE.BufferAttribute(starPositions, 3)); const starMat = new THREE.PointsMaterial({ color: 0xffffff, size: 1.2, sizeAttenuation: false, transparent: true, opacity: 0.0 }); starsField = new THREE.Points(starsGeo, starMat); scene.add(starsField); } function initClouds() { cloudsGroup = new THREE.Group(); cloudDataList = []; mergedCloudRainZones = []; for (let i = 0; i < 18; i++) { const w = 18 + Math.random() * 10; const h = 1.2 + Math.random() * 0.6; const d = 10 + Math.random() * 8; const cloudGeo = new THREE.BoxGeometry(w, h, d); const cloudMat = new THREE.MeshBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.45, fog: true }); const cloud = new THREE.Mesh(cloudGeo, cloudMat); cloud.position.set( (Math.random() - 0.5) * 350, 65 + Math.random() * 15, (Math.random() - 0.5) * 350 ); cloudsGroup.add(cloud); cloudDataList.push({ mesh: cloud, merged: false, baseSpeed: 0.04 + Math.random() * 0.02 }); } scene.add(cloudsGroup); initRainParticles(); } function initRainParticles() { if (rainParticlesGroup) { scene.remove(rainParticlesGroup); rainParticlesGroup.traverse(c => { if (c.geometry) c.geometry.dispose(); if (c.material) c.material.dispose(); }); } rainParticlesGroup = new THREE.Group(); const dropGeo = new THREE.BoxGeometry(0.04, 0.5, 0.04); const dropMat = new THREE.MeshBasicMaterial({ color: 0x88aacc, transparent: true, opacity: 0.55 }); for (let i = 0; i < 120; i++) { const drop = new THREE.Mesh(dropGeo, dropMat); drop.visible = false; rainParticlesGroup.add(drop); } scene.add(rainParticlesGroup); } function isMorningHours() { return dayNightTimer < DAY_DURATION && (dayNightTimer / DAY_DURATION) < 0.35; } function mergeClouds(a, b) { if (a.merged || b.merged) return; a.merged = true; b.merged = true; const survivor = a.mesh; const consumed = b.mesh; const newW = survivor.geometry.parameters.width * 1.55; const newH = survivor.geometry.parameters.height * 1.35; const newD = survivor.geometry.parameters.depth * 1.85; survivor.geometry.dispose(); survivor.geometry = new THREE.BoxGeometry(newW, newH, newD); survivor.material.color.setHex(0x666666); survivor.material.opacity = 0.62; survivor.position.x = (survivor.position.x + consumed.position.x) * 0.5; survivor.position.z = (survivor.position.z + consumed.position.z) * 0.5; cloudsGroup.remove(consumed); consumed.geometry.dispose(); consumed.material.dispose(); const zone = { mesh: survivor, rainDelay: 60, isRaining: false, rainDuration: 90 + Math.random() * 60 }; mergedCloudRainZones.push(zone); cloudDataList = cloudDataList.filter(c => c.mesh !== consumed); } function tryMergeNearbyClouds() { const mergeChance = isMorningHours() ? 0.70 : 0.50; for (let i = 0; i < cloudDataList.length; i++) { for (let j = i + 1; j < cloudDataList.length; j++) { const a = cloudDataList[i]; const b = cloudDataList[j]; if (a.merged || b.merged) continue; const dist = a.mesh.position.distanceTo(b.mesh.position); if (dist < 28 && Math.random() < mergeChance) { mergeClouds(a, b); return; } } } } function applyRainToSandBlock(x, y, z) { const key = `${x},${y},${z}`; let hits = sandRainHits.get(key) || 0; hits++; sandRainHits.set(key, hits); const type = getBlock(x, y, z); if (type === 'sand' && hits >= 5) { setBlock(x, y, z, 'slightlyWetSand'); sandRainHits.set(key, 0); updateAffectedChunks(x, y, z); } else if (type === 'slightlyWetSand' && hits >= 15) { setBlock(x, y, z, 'wetSand'); sandRainHits.delete(key); updateAffectedChunks(x, y, z); } } function processRainWetness() { if (!isRainActive || isPaused) return; const dropsPerFrame = 6; for (let n = 0; n < dropsPerFrame; n++) { const rx = Math.floor(player.pos.x + (Math.random() - 0.5) * 100); const rz = Math.floor(player.pos.z + (Math.random() - 0.5) * 100); for (let y = MAX_WORLD_HEIGHT; y >= 0; y--) { const block = getBlock(rx, y, rz); if (block === 'sand' || block === 'slightlyWetSand' || block === 'wetSand') { applyRainToSandBlock(rx, y, rz); break; } if (block && block !== 'leaf' && block !== 'grayLeaf') break; } } } function updateRainVisuals(dt) { const overlay = document.getElementById('rain-overlay'); if (overlay) overlay.style.opacity = isRainActive ? '1' : '0'; if (!rainParticlesGroup) return; rainParticlesGroup.children.forEach((drop, idx) => { drop.visible = isRainActive; if (!isRainActive) return; if (!drop.userData.speed) { drop.userData.speed = 18 + Math.random() * 12; drop.userData.offsetX = (Math.random() - 0.5) * 80; drop.userData.offsetZ = (Math.random() - 0.5) * 80; } drop.position.x = player.pos.x + drop.userData.offsetX; drop.position.z = player.pos.z + drop.userData.offsetZ; drop.position.y += drop.userData.speed * dt; if (drop.position.y < player.pos.y - 5) { drop.position.y = player.pos.y + 25 + (idx % 10); } }); } function updateWeatherSystem(dt) { cloudMergeCheckTimer += dt; if (cloudMergeCheckTimer >= 2.5) { cloudMergeCheckTimer = 0; tryMergeNearbyClouds(); } let anyRain = false; for (let i = mergedCloudRainZones.length - 1; i >= 0; i--) { const zone = mergedCloudRainZones[i]; if (!zone.isRaining) { zone.rainDelay -= dt; if (zone.rainDelay <= 0) zone.isRaining = true; } else { zone.rainDuration -= dt; anyRain = true; if (zone.rainDuration <= 0) { zone.isRaining = false; zone.mesh.material.color.setHex(0x888888); zone.mesh.material.opacity = 0.5; } } if (zone.isRaining) anyRain = true; } isRainActive = anyRain; if (cloudsGroup) { cloudDataList.forEach(c => { if (c.merged) return; c.mesh.position.x += c.baseSpeed; if (c.mesh.position.x > player.pos.x + 150) { c.mesh.position.x = player.pos.x - 150; } }); mergedCloudRainZones.forEach(z => { z.mesh.position.x += 0.03; if (z.mesh.position.x > player.pos.x + 150) { z.mesh.position.x = player.pos.x - 150; } }); } processRainWetness(); updateRainVisuals(dt); } function createCreepyObservers() { const centers = [ { x: 60, z: 60 }, { x: 160, z: 60 }, { x: 260, z: 60 } ]; centers.forEach((center, index) => { const obsY = 19; const observer = new THREE.Group(); observer.position.set(center.x, obsY, center.z); const bodyGeo = new THREE.BoxGeometry(0.5, 2.8, 0.5); const bodyMat = new THREE.MeshBasicMaterial({ color: 0x050505 }); const bodyMesh = new THREE.Mesh(bodyGeo, bodyMat); bodyMesh.position.y = 1.4; observer.add(bodyMesh); const headGeo = new THREE.BoxGeometry(0.7, 0.7, 0.7); const headMesh = new THREE.Mesh(headGeo, bodyMat); headMesh.position.y = 3.15; observer.add(headMesh); const eyeGeo = new THREE.BoxGeometry(0.12, 0.12, 0.12); const eyeMat = new THREE.MeshBasicMaterial({ color: 0xff0000 }); const leftEye = new THREE.Mesh(eyeGeo, eyeMat); leftEye.position.set(-0.18, 3.2, 0.36); const rightEye = new THREE.Mesh(eyeGeo, eyeMat); rightEye.position.set(0.18, 3.2, 0.36); observer.add(leftEye); observer.add(rightEye); scene.add(observer); creepyObservers.push({ id: index + 1, obj: observer, center: center, hasScream: false }); }); } function spawnTowerGuardMobs() { TOWER_BIOMES.forEach((center, biomeIdx) => { const offsets = [ { dx: -15, dz: -15 }, { dx: 15, dz: -15 }, { dx: -15, dz: 15 }, { dx: 15, dz: 15 } ]; offsets.forEach((offset, idx) => { const guard = new THREE.Group(); const gx = center.x + offset.dx; const gz = center.z + offset.dz; const gy = 20; guard.position.set(gx, gy, gz); const ironMat = new THREE.MeshLambertMaterial({ color: 0x8e9399 }); const skinMat = new THREE.MeshLambertMaterial({ color: 0xdfb087 }); const swordMat = new THREE.MeshLambertMaterial({ color: 0xb0c0d0 }); const darkEyeMat = new THREE.MeshBasicMaterial({ color: 0x333333 }); const bodyGeo = new THREE.BoxGeometry(1.0, 1.2, 1.0); const bodyMesh = new THREE.Mesh(bodyGeo, ironMat); bodyMesh.position.y = 1.0; guard.add(bodyMesh); const headGroup = new THREE.Group(); headGroup.position.y = 1.95; const helmetGeo = new THREE.BoxGeometry(0.85, 0.75, 0.85); const helmetMesh = new THREE.Mesh(helmetGeo, ironMat); headGroup.add(helmetMesh); const faceGeo = new THREE.BoxGeometry(0.65, 0.5, 0.1); const faceMesh = new THREE.Mesh(faceGeo, skinMat); faceMesh.position.set(0, -0.05, 0.38); headGroup.add(faceMesh); const eyeGeo = new THREE.BoxGeometry(0.12, 0.12, 0.1); const eyeL = new THREE.Mesh(eyeGeo, darkEyeMat); eyeL.position.set(-0.16, -0.05, 0.44); const eyeR = new THREE.Mesh(eyeGeo, darkEyeMat); eyeR.position.set(0.16, -0.05, 0.44); headGroup.add(eyeL, eyeR); guard.add(headGroup); const legGeo = new THREE.BoxGeometry(0.35, 0.45, 0.35); const legL = new THREE.Mesh(legGeo, ironMat); legL.position.set(-0.25, 0.22, 0); const legR = new THREE.Mesh(legGeo, ironMat); legR.position.set(0.25, 0.22, 0); guard.add(legL, legR); const armGeo = new THREE.BoxGeometry(0.3, 0.95, 0.3); const armL = new THREE.Mesh(armGeo, ironMat); armL.position.set(-0.65, 1.0, 0); const armR = new THREE.Mesh(armGeo, ironMat); armR.position.set(0.65, 1.0, 0); guard.add(armL, armR); const swordGeo = new THREE.BoxGeometry(0.12, 1.1, 0.12); const swordMesh = new THREE.Mesh(swordGeo, swordMat); swordMesh.position.set(0.65, 1.2, 0.4); swordMesh.rotation.x = Math.PI / 3; guard.add(swordMesh); scene.add(guard); towerGuards.push({ obj: guard, biomeIdx: biomeIdx, health: 9, maxHealth: 9, isTriggered: false, isDamaged: false, attackCooldown: 0, homeCenter: center, spawnPos: new THREE.Vector3(gx, gy, gz), animTime: Math.random() * 50, eyes: [eyeL, eyeR], normalEyeMat: darkEyeMat, bodyMesh: bodyMesh, ironMat: ironMat, skinMat: skinMat, sword: swordMesh, legL: legL, legR: legR, armL: armL, armR: armR, velocity: new THREE.Vector3() }); }); }); } let matList; function initMaterials() { const grassTop = new THREE.MeshLambertMaterial({color: 0x2e8b57}); const grassSide = new THREE.MeshLambertMaterial({color: 0x8b5a2b}); const dirtMat = new THREE.MeshLambertMaterial({color: 0x5c4033}); const sandMat = new THREE.MeshLambertMaterial({color: 0x8a7338}); const slightlyWetSandMat = new THREE.MeshLambertMaterial({color: 0x756030}); const wetSandMat = new THREE.MeshLambertMaterial({color: 0x5a4825}); const stoneMat = new THREE.MeshLambertMaterial({color: 0x708090}); const copyGrassTop = new THREE.MeshLambertMaterial({color: 0x222222}); const copyGrassSide = new THREE.MeshLambertMaterial({color: 0x111111}); const copyDirtMat = new THREE.MeshLambertMaterial({color: 0x151515}); const copyStoneMat = new THREE.MeshLambertMaterial({color: 0x0c0c0c}); matList = { grass: [grassSide, grassSide, grassTop, grassSide, grassSide, grassSide], dirt: dirtMat, sand: sandMat, slightlyWetSand: slightlyWetSandMat, wetSand: wetSandMat, stone: stoneMat, wood: new THREE.MeshLambertMaterial({ color: 0x6b3a12, emissive: 0x2a1205, emissiveIntensity: 0.12 }), leaf: new THREE.MeshLambertMaterial({color: 0x1b4d3e, transparent: true, opacity: 0.95}), water: new THREE.MeshLambertMaterial({color: 0x1d4ed8, transparent: true, opacity: 0.65}), chest: new THREE.MeshLambertMaterial({color: 0xffd700}), grayGrass: [copyGrassSide, copyGrassSide, copyGrassTop, copyGrassSide, copyGrassSide, copyGrassSide], grayDirt: copyDirtMat, grayStone: copyStoneMat, grayWood: new THREE.MeshLambertMaterial({color: 0x0f0f0f}), grayLeaf: new THREE.MeshLambertMaterial({color: 0x1a1a1a, transparent: true, opacity: 0.9}) }; } function getActiveSpecialBiome(x, z) { if (x >= B1_MIN_X && x <= B1_MAX_X && z >= B1_MIN_Z && z <= B1_MAX_Z) return 1; if (x >= B2_MIN_X && x <= B2_MAX_X && z >= B2_MIN_Z && z <= B2_MAX_Z) return 2; if (x >= B3_MIN_X && x <= B3_MAX_X && z >= B3_MIN_Z && z <= B3_MAX_Z) return 3; return 0; } function getTowerBiomeIndex(wx, wz) { for(let i = 0; i < TOWER_BIOMES.length; i++) { const tb = TOWER_BIOMES[i]; if (Math.abs(wx - tb.x) <= 20 && Math.abs(wz - tb.z) <= 20) { return i; } } return -1; } function getTerrainHeight(x, z) { const insideT = getTowerBiomeIndex(x, z); if (insideT >= 0) { return 20; } const getSpecial = getActiveSpecialBiome(x, z) > 0; if (getSpecial) { return 19; } const noise1 = simplex1.noise2D(x * 0.012, z * 0.012) * 16; const noise2 = simplex2.noise2D(x * 0.06, z * 0.06) * 3; const distFromCenter = Math.sqrt(x*x + z*z); let baseHeight = 16; if (distFromCenter < 10) { return baseHeight; } let finalHeight = Math.floor(baseHeight + noise1 + noise2); // BÜYÜK DAĞ MEKANİĞİ const mountainScaleNoise = simplex3.noise2D(x * 0.003, z * 0.003); if (mountainScaleNoise > 0.1) { finalHeight += Math.floor((mountainScaleNoise - 0.1) * 36); } return Math.min(MAX_WORLD_HEIGHT, Math.max(16, finalHeight)); } function isBlockOccluded(wx, wy, wz) { const neighbors = [ getBlock(wx + 1, wy, wz), getBlock(wx - 1, wy, wz), getBlock(wx, wy + 1, wz), getBlock(wx, wy - 1, wz), getBlock(wx, wy, wz + 1), getBlock(wx, wy, wz - 1) ]; return neighbors.every(n => n && n !== 'water'); } function generateChunkData(cx, cz) { const key = `${cx},${cz}`; if (generatedChunks.has(key)) return; generatedChunks.add(key); const chunkRng = makeChunkRng(cx, cz); for (let x = 0; x < CHUNK_SIZE; x++) { for (let z = 0; z < CHUNK_SIZE; z++) { const wx = cx * CHUNK_SIZE + x; const wz = cz * CHUNK_SIZE + z; const towerBiomeIdx = getTowerBiomeIndex(wx, wz); const insideTower = towerBiomeIdx >= 0; if (insideTower) { const center = TOWER_BIOMES[towerBiomeIdx]; const rx = wx - center.x; const rz = wz - center.z; // Eski Kuleler Biyomunda çimenler olsun setBlock(wx, 20, wz, 'grass'); for(let y = 1; y <= 3; y++) { setBlock(wx, 20 - y, wz, 'dirt'); } for(let y = 4; y <= 20; y++) { setBlock(wx, 20 - y, wz, 'stone'); } // KULE 1 - 20 Blok yüksekliğe çıksın (Y=21'den Y=40'a kadar) const tx1 = -12, tz1 = 0; const dx1 = rx - tx1, dz1 = rz - tz1; if (Math.abs(dx1) <= 2 && Math.abs(dz1) <= 2) { // Simetrik 3x3 hollow içi boş kule sistemi const isHollow = Math.abs(dx1) <= 1 && Math.abs(dz1) <= 1; setBlock(wx, 20, wz, 'wood'); for (let y = 21; y <= 40; y++) { // Eve bakan tarafta (Doğu) açık giriş kapısı if (dx1 === 2 && dz1 === 0 && (y === 21 || y === 22 || y === 23)) { continue; } if (isHollow && y < 40) { setBlock(wx, y, wz, 'wood'); continue; } // Kule ahşap orta bölümü güncellendi if (y <= 22) { setBlock(wx, y, wz, 'stone'); } else if (y <= 36) { setBlock(wx, y, wz, 'wood'); } else { if (y === 40) { if ((dx1 + dz1) % 2 === 0) { setBlock(wx, y, wz, 'stone'); } } else { setBlock(wx, y, wz, 'stone'); } } } } // Corrected: Added missing closing brace for Kule 1 if-block // KULE 2 - 20 Blok yüksekliğe çıksın (Y=21'den Y=40'a kadar) const tx2 = 12, tz2 = 0; const dx2 = rx - tx2, dz2 = rz - tz2; if (Math.abs(dx2) <= 2 && Math.abs(dz2) <= 2) { // Simetrik 3x3 hollow içi boş kule sistemi const isHollow = Math.abs(dx2) <= 1 && Math.abs(dz2) <= 1; setBlock(wx, 20, wz, 'wood'); for (let y = 21; y <= 40; y++) { // Eve bakan tarafta (Batı) açık giriş kapısı if (dx2 === -2 && dz2 === 0 && (y === 21 || y === 22 || y === 23)) { continue; } if (isHollow && y < 40) { setBlock(wx, y, wz, 'wood'); continue; } // Kule ahşap orta bölümü güncellendi if (y <= 22) { setBlock(wx, y, wz, 'stone'); } else if (y <= 36) { setBlock(wx, y, wz, 'wood'); } else { if (y === 40) { if ((dx2 + dz2) % 2 === 0) { setBlock(wx, y, wz, 'stone'); } } else { setBlock(wx, y, wz, 'stone'); } } } } // Corrected: Added missing closing brace for Kule 2 if-block // İKİ KULE ARASINDAKİ EV - Çok daha büyük (11x11 boyutunda) ve içinde sarı sandık var const hSize = 5; if (Math.abs(rx) <= hSize && Math.abs(rz) <= hSize) { const isInterior = Math.abs(rx) < hSize && Math.abs(rz) < hSize; for (let y = 1; y <= 19; y++) { setBlock(wx, y, wz, 'wood'); } setBlock(wx, 20, wz, 'wood'); for (let y = 21; y <= 25; y++) { const isEdgeX = Math.abs(rx) === hSize; const isEdgeZ = Math.abs(rz) === hSize; const isCorner = isEdgeX && isEdgeZ; const isDoorWall = rz === hSize && Math.abs(rx) <= 1; if (y === 25) { setBlock(wx, y, wz, 'wood'); } else if (isCorner) { setBlock(wx, y, wz, 'wood'); } else if (isEdgeX || isEdgeZ) { if (isDoorWall && y <= 24) continue; setBlock(wx, y, wz, 'stone'); } else if (isInterior && y === 21) { setBlock(wx, y, wz, 'wood'); } } if (rx === 2 && rz === 2) { setBlock(wx, 21, wz, 'chest'); } } if (rx === -8 && rz === -8) { setBlock(wx, 20, wz, 'water'); } continue; } const h = getTerrainHeight(wx, wz); const biomeNum = getActiveSpecialBiome(wx, wz); const isGray = biomeNum > 0; if (!isGray && h < SEA_LEVEL) { for (let y = h + 1; y <= SEA_LEVEL; y++) { setBlock(wx, y, wz, 'water'); } } if (isGray) { setBlock(wx, h, wz, 'grayGrass'); for(let y = 1; y <= 4; y++) { setBlock(wx, h - y, wz, 'grayDirt'); } for(let y = 5; y <= 20; y++) { setBlock(wx, h - y, wz, 'grayStone'); } } else { const nearWater = isNearWaterAt(wx, wz, h); const surfaceType = nearWater ? 'sand' : 'grass'; const subType = nearWater ? 'sand' : 'dirt'; setBlock(wx, h, wz, surfaceType); for(let y = 1; y <= 4; y++) { setBlock(wx, h - y, wz, nearWater && y <= 3 ? 'sand' : subType); } for(let y = 5; y <= 20; y++) { setBlock(wx, h - y, wz, 'stone'); } } const noise3Val = simplex3.noise2D(wx * 0.05, wz * 0.05); if (Math.abs(wx) > 8 && Math.abs(wz) > 8 && h >= SEA_LEVEL && h + 6 <= MAX_WORLD_HEIGHT && noise3Val > 0.4 && chunkRng() < 0.06) { const trunkType = isGray ? 'grayWood' : 'wood'; const leafType = isGray ? 'grayLeaf' : 'leaf'; const treeGroup = new THREE.Group(); treeGroup.position.set(wx, h, wz); const trunkGeo = new THREE.BoxGeometry(1, 1, 1); const leafGeo = new THREE.BoxGeometry(1, 1, 1); for (let i = 1; i <= 5; i++) { setBlock(wx, h + i, wz, trunkType); } for (let ly = 4; ly <= 6; ly++) { for (let lx = -1; lx <= 1; lx++) { for (let lz = -1; lz <= 1; lz++) { if (lx !== 0 || lz !== 0 || ly > 5) { const leafMesh = new THREE.Mesh(leafGeo, matList[leafType]); leafMesh.position.set(lx, ly - 0.5, lz); leafMesh.userData = { type: leafType, localX: lx, localY: ly, localZ: lz }; treeGroup.add(leafMesh); setBlock(wx + lx, h + ly, wz + lz, leafType); } } } } scene.add(treeGroup); grayBiomeTrees.push({ chunkKey: key, biomeId: biomeNum, obj: treeGroup, startY: h, targetY: h, currentY: h, isSunk: false }); } } } } function buildChunkMeshGroup(cx, cz) { const group = new THREE.Group(); const geo = new THREE.BoxGeometry(1, 1, 1); const blocks = { grass: [], dirt: [], sand: [], slightlyWetSand: [], wetSand: [], stone: [], wood: [], leaf: [], water: [], chest: [], grayGrass: [], grayDirt: [], grayStone: [], grayWood: [], grayLeaf: [] }; for (let x = 0; x < CHUNK_SIZE; x++) { for (let z = 0; z < CHUNK_SIZE; z++) { const wx = cx * CHUNK_SIZE + x; const wz = cz * CHUNK_SIZE + z; for (let y = 0; y <= MAX_WORLD_HEIGHT; y++) { const type = getBlock(wx, y, wz); if (!type || blocks[type] === undefined) continue; if (type === 'leaf' || type === 'grayLeaf') continue; if (!isBlockOccluded(wx, y, wz)) { blocks[type].push(new THREE.Vector3(wx, y, wz)); } } } } Object.keys(blocks).forEach(type => { const list = blocks[type]; if (list.length === 0) return; let material = matList[type]; if (type === 'grass') material = matList.grass; if (type === 'grayGrass') material = matList.grayGrass; const inst = new THREE.InstancedMesh(geo, material, list.length); inst.receiveShadow = true; const mat = new THREE.Matrix4(); list.forEach((pos, i) => { mat.setPosition(pos); inst.setMatrixAt(i, mat); }); inst.instanceMatrix.needsUpdate = true; group.add(inst); }); return group; } function createChunk(cx, cz) { const key = `${cx},${cz}`; if (chunkMeshes.has(key)) return; generateChunkData(cx, cz); const group = buildChunkMeshGroup(cx, cz); scene.add(group); chunkMeshes.set(key, group); } function setBlock(x, y, z, type) { worldBlocks.set(`${Math.floor(x)},${Math.floor(y)},${Math.floor(z)}`, type); } function getBlock(x, y, z) { return worldBlocks.get(`${Math.floor(x)},${Math.floor(y)},${Math.floor(z)}`); } function rebuildChunk(cx, cz) { const key = `${cx},${cz}`; if (!generatedChunks.has(key)) return; if (chunkMeshes.has(key)) { scene.remove(chunkMeshes.get(key)); chunkMeshes.delete(key); } const group = buildChunkMeshGroup(cx, cz); scene.add(group); chunkMeshes.set(key, group); } function checkCollision(x, y, z) { const r = player.radius; const h = player.height; for (let ox of [-r, r]) { for (let oz of [-r, r]) { for (let oy of [0.05, h * 0.5, h - 0.05]) { const block = getBlock(x + ox, y + oy, z + oz); if (block && block !== 'water' && block !== 'leaf' && block !== 'grayLeaf') return true; } } } return false; } function updatePhysics() { if (isPaused) return; const currentBlock = getBlock(player.pos.x, player.pos.y + 0.4, player.pos.z); player.inWater = (currentBlock === 'water'); player.isSprinting = keys['ShiftLeft'] || keys['ShiftRight']; player.isCrouching = keys['ControlLeft'] || keys['ControlRight']; let speedMultiplier = 1.0; if (player.isCrouching) { speedMultiplier = player.crouchMultiplier; player.height = 1.1; } else { player.height = 1.8; if (player.isSprinting) speedMultiplier = player.runMultiplier; } const moveDir = new THREE.Vector3(); let fwd = keys['KeyW'] ? 1 : 0; let bwd = keys['KeyS'] ? 1 : 0; let lft = keys['KeyA'] ? 1 : 0; let rgt = keys['KeyD'] ? 1 : 0; if (fwd !== 0 || bwd !== 0 || lft !== 0 || rgt !== 0) { moveDir.z = bwd - fwd; moveDir.x = rgt - lft; } moveDir.applyQuaternion(camera.quaternion); moveDir.y = 0; moveDir.normalize(); let currentSpeed = player.inWater ? player.speed * 0.45 : player.speed; currentSpeed *= speedMultiplier; if (fishDebuffTimer > 0) { currentSpeed *= 0.5; } const dx = moveDir.x * currentSpeed; const dz = moveDir.z * currentSpeed; if (dx !== 0) { player.pos.x += dx; if (checkCollision(player.pos.x, player.pos.y, player.pos.z)) { const chestCollides = checkCollision(player.pos.x, player.pos.y + 1.1, player.pos.z); if (!player.isCrouching && !chestCollides) { player.pos.y += 1.0; if (checkCollision(player.pos.x, player.pos.y, player.pos.z)) { player.pos.y -= 1.0; player.pos.x -= dx; } } else { player.pos.x -= dx; } } } if (dz !== 0) { player.pos.z += dz; if (checkCollision(player.pos.x, player.pos.y, player.pos.z)) { const chestCollides = checkCollision(player.pos.x, player.pos.y + 1.1, player.pos.z); if (!player.isCrouching && !chestCollides) { player.pos.y += 1.0; if (checkCollision(player.pos.x, player.pos.y, player.pos.z)) { player.pos.y -= 1.0; player.pos.z -= dz; } } else { player.pos.z -= dz; } } } const gravity = player.inWater ? 0.003 : 0.008; // Düşüşten kurtarma mekaniği if (player.pos.y < -5) { player.pos.y = 40; player.vel.y = 0; } const onGroundNow = checkCollision(player.pos.x, player.pos.y - 0.05, player.pos.z); if (onGroundNow) { player.onGround = true; if (player.vel.y < 0) player.vel.y = 0; } else { player.onGround = false; player.vel.y -= gravity; } player.pos.y += player.vel.y; if (checkCollision(player.pos.x, player.pos.y, player.pos.z)) { if (player.vel.y < 0) { player.pos.y = Math.ceil(player.pos.y); while (checkCollision(player.pos.x, player.pos.y, player.pos.z)) { player.pos.y += 0.01; } player.onGround = true; player.vel.y = 0; } else if (player.vel.y > 0) { player.pos.y = Math.floor(player.pos.y); while (checkCollision(player.pos.x, player.pos.y, player.pos.z)) { player.pos.y -= 0.01; } player.vel.y = 0; } } if (player.onGround && (dx !== 0 || dz !== 0)) { player.stepAccumulator += currentSpeed; const stepInterval = player.isSprinting ? 0.5 : 0.75; if (player.stepAccumulator >= stepInterval) { player.stepAccumulator = 0; const blockBelow = getBlock(player.pos.x, player.pos.y - 0.15, player.pos.z); if (blockBelow === 'grass' || blockBelow === 'grayGrass') { audio.playStepSound('grass'); } else if (blockBelow === 'sand' || blockBelow === 'slightlyWetSand' || blockBelow === 'wetSand') { audio.playStepSound('dirt'); } else if (blockBelow === 'dirt' || blockBelow === 'grayDirt' || blockBelow === 'stone' || blockBelow === 'grayStone') { audio.playStepSound('dirt'); } } } else { player.stepAccumulator = 0; } if (keys['Space']) { if (player.onGround) { player.vel.y = player.jumpForce; player.onGround = false; } else if (player.inWater) { player.vel.y = 0.065; } } const targetHeight = player.isCrouching ? 0.95 : 1.6; camera.position.x = player.pos.x; camera.position.z = player.pos.z; camera.position.y += (player.pos.y + targetHeight - camera.position.y) * 0.3; } function getRaycastHit(maxDist = 6) { const raycaster = new THREE.Raycaster(); raycaster.setFromCamera(new THREE.Vector2(0, 0), camera); const targets = [...Array.from(chunkMeshes.values())]; grayBiomeTrees.forEach(t => { if (t.obj.visible && !t.isSunk) targets.push(t.obj); }); killerFishList.forEach(f => targets.push(f.obj)); towerGuards.forEach(g => targets.push(g.obj)); if (horrorGiant && horrorGiant.obj) targets.push(horrorGiant.obj); const intersects = raycaster.intersectObjects(targets, true); if (intersects.length > 0 && intersects[0].distance < maxDist) return intersects[0]; return null; } function flashMobRed(obj) { obj.traverse(c => { if (c.material && c.material.color) { const orig = c.material.color.getHex(); c.material.color.setHex(0xff0000); setTimeout(() => { if (c.material && c.material.color) c.material.color.setHex(orig); }, 150); } }); } function attackEntity(hit, clickedObj) { if (attackCooldown > 0) return false; if (horrorGiant && horrorGiant.obj) { const gObj = horrorGiant.obj; if (gObj === clickedObj || gObj.children.includes(clickedObj)) { horrorGiant.health--; horrorGiant.targetPlayer = true; attackCooldown = 0.35; audio.playBreakTıkTık(); flashMobRed(gObj); if (horrorGiant.health <= 0) { scene.remove(gObj); gObj.traverse(c => { if (c.geometry) c.geometry.dispose(); if (c.material) c.material.dispose(); }); horrorGiant = null; audio.playScream(2); } return true; } } for (let i = 0; i < killerFishList.length; i++) { const fObj = killerFishList[i].obj; if (fObj === clickedObj || fObj.children.includes(clickedObj)) { killerFishList[i].health--; attackCooldown = 0.35; audio.playBreakTıkTık(); flashMobRed(fObj); if (killerFishList[i].health <= 0) { scene.remove(fObj); killerFishList.splice(i, 1); audio.playScream(1); } return true; } } for (let i = 0; i < towerGuards.length; i++) { const gObj = towerGuards[i].obj; if (gObj === clickedObj || gObj.children.includes(clickedObj)) { const g = towerGuards[i]; g.health--; g.isTriggered = true; g.isDamaged = true; attackCooldown = 0.35; audio.playBreakTıkTık(); flashMobRed(gObj); g.eyes.forEach(eye => { eye.material = new THREE.MeshBasicMaterial({ color: 0xff0000 }); }); if (performance.now() - lastTowerScreamTime > 1200) { audio.playScream(2); lastTowerScreamTime = performance.now(); } if (g.health <= 0) { killTowerGuard(i); } return true; } } return false; } function killTowerGuard(index) { const g = towerGuards[index]; if (!g) return; const gx = g.obj.position.x; const gy = g.obj.position.y; const gz = g.obj.position.z; if (Math.random() < 0.01) { spawnDroppedItem('guardNote', Math.floor(gx), Math.floor(gy), Math.floor(gz)); } scene.remove(g.obj); g.obj.traverse(child => { if (child.geometry) child.geometry.dispose(); if (child.material && !child.material.isShared) child.material.dispose(); }); towerGuards.splice(index, 1); } function resetTowerGuardsOnBiomeExit(biomeIdx) { towerGuards.forEach(g => { if (g.biomeIdx !== biomeIdx) return; g.isTriggered = false; g.attackCooldown = 0; g.eyes.forEach(eye => { eye.material = g.normalEyeMat; }); if (g.isDamaged && g.bodyMesh) { g.bodyMesh.material.color.setHex(0x8e9399); g.legL.material.color.setHex(0x8e9399); g.legR.material.color.setHex(0x8e9399); g.armL.material.color.setHex(0x8e9399); g.armR.material.color.setHex(0x8e9399); } }); audio.stopTowerHum(); lastTowerScreamTime = performance.now(); } function reactivateTowerGuardsOnBiomeEnter(biomeIdx) { if (!biomeAggroTriggered[biomeIdx]) return; towerGuards.forEach(g => { if (g.biomeIdx !== biomeIdx) return; g.isTriggered = true; g.eyes.forEach(eye => { eye.material = new THREE.MeshBasicMaterial({ color: 0xff0000 }); }); }); } function getBlockTargetFromHit(hit) { const clickedObj = hit.object; for (let i = 0; i < grayBiomeTrees.length; i++) { const treeObj = grayBiomeTrees[i].obj; if (treeObj === clickedObj.parent || treeObj === clickedObj) { const uData = clickedObj.userData; if (uData) { return { kind: 'tree', treeObj, mesh: clickedObj, bx: Math.round(treeObj.position.x + uData.localX), by: Math.round(treeObj.position.y + uData.localY), bz: Math.round(treeObj.position.z + uData.localZ), type: uData.type }; } } } const point = hit.point; const normal = hit.face.normal; const bx = Math.round(point.x - normal.x * 0.4); const by = Math.round(point.y - normal.y * 0.4); const bz = Math.round(point.z - normal.z * 0.4); const type = getBlock(bx, by, bz); if (type && type !== 'water') { return { kind: 'block', bx, by, bz, type }; } return null; } function breakBlockTarget(target) { if (target.kind === 'tree') { target.treeObj.remove(target.mesh); } worldBlocks.delete(`${target.bx},${target.by},${target.bz}`); sandRainHits.delete(`${target.bx},${target.by},${target.bz}`); updateAffectedChunks(target.bx, target.by, target.bz); const towerIdx = getTowerBiomeIndex(target.bx, target.bz); if (towerIdx >= 0) triggerTowerAggro(towerIdx); if (PICKUP_TYPES.includes(target.type)) { spawnDroppedItem(target.type, target.bx, target.by, target.bz); } audio.playBreakTıkTık(); } function startBreaking() { const hit = getRaycastHit(); if (!hit) { breakTarget = null; breakProgress = 0; return; } if (attackEntity(hit, hit.object)) { breakTarget = null; breakProgress = 0; return; } const target = getBlockTargetFromHit(hit); if (!target) { breakTarget = null; breakProgress = 0; return; } const key = target.kind === 'tree' ? `tree,${target.bx},${target.by},${target.bz}` : `block,${target.bx},${target.by},${target.bz}`; if (!breakTarget || breakTarget.key !== key) { breakTarget = { ...target, key }; breakProgress = 0; } } function updateBreaking(dt) { const progEl = document.getElementById('break-progress'); const fillEl = document.getElementById('break-progress-fill'); if (!isMouseBreakHeld || isPaused) { if (progEl) progEl.style.display = 'none'; if (!isMouseBreakHeld) { breakTarget = null; breakProgress = 0; } return; } if (!breakTarget) { startBreaking(); if (!breakTarget) { if (progEl) progEl.style.display = 'none'; return; } } const hit = getRaycastHit(); if (!hit) { breakTarget = null; breakProgress = 0; if (progEl) progEl.style.display = 'none'; return; } const current = getBlockTargetFromHit(hit); const match = current && ( (breakTarget.kind === 'tree' && current.kind === 'tree' && breakTarget.bx === current.bx && breakTarget.by === current.by && breakTarget.bz === current.bz) || (breakTarget.kind === 'block' && current.kind === 'block' && breakTarget.bx === current.bx && breakTarget.by === current.by && breakTarget.bz === current.bz) ); if (!match) { breakTarget = null; breakProgress = 0; if (progEl) progEl.style.display = 'none'; return; } const needed = getBreakTime(breakTarget.type); breakProgress += dt; if (progEl) progEl.style.display = 'block'; if (fillEl) fillEl.style.width = Math.min(100, (breakProgress / needed) * 100) + '%'; if (breakProgress >= needed) { breakBlockTarget(breakTarget); breakTarget = null; breakProgress = 0; if (progEl) progEl.style.display = 'none'; if (isMouseBreakHeld) startBreaking(); } } function triggerWorldInteraction(type) { const hit = getRaycastHit(); if (!hit) return; if (type === 'break') { attackEntity(hit, hit.object); return; } if (type === 'place') { const slotItem = inventory[selectedSlot]; if (slotItem && slotItem.type === 'guardNote') { openGuardNoteModal(); return; } const point = hit.point; const normal = hit.face.normal; const px = Math.round(point.x + normal.x * 0.4); const py = Math.round(point.y + normal.y * 0.4); const pz = Math.round(point.z + normal.z * 0.4); if (py > MAX_WORLD_HEIGHT) { audio.playPlaceTık(); setTimeout(() => audio.playBreakTıkTık(), 120); return; } if (!slotItem) return; const placePos = new THREE.Vector3(px + 0.5, py + 0.5, pz + 0.5); const pDist = player.pos.distanceTo(placePos); if (pDist < 1.4) return; const targetBlock = getBlock(px, py, pz); if (!targetBlock || targetBlock === 'water') { const blockInPlayer = ( Math.abs(px + 0.5 - player.pos.x) < player.radius + 0.55 && Math.abs(pz + 0.5 - player.pos.z) < player.radius + 0.55 && py >= Math.floor(player.pos.y) && py <= Math.floor(player.pos.y + player.height) ); if (blockInPlayer) return; if (slotItem.type === 'guardNote') return; setBlock(px, py, pz, slotItem.type); removeOneFromSlot(selectedSlot); audio.playPlaceTık(); updateAffectedChunks(px, py, pz); } } } function triggerTowerAggro(biomeIdx) { if (biomeAggroTriggered[biomeIdx]) return; biomeAggroTriggered[biomeIdx] = true; towerGuards.forEach(g => { if (g.biomeIdx === biomeIdx) { g.isTriggered = true; g.eyes.forEach(eye => { eye.material = new THREE.MeshBasicMaterial({ color: 0xff0000 }); }); } }); } function handleKeyDown(e) { if (isPaused) return; if (e.code in keys) { keys[e.code] = true; } else if (e.code === 'Space') { keys['Space'] = true; } else if (e.code === 'ShiftLeft' || e.code === 'ShiftRight') { keys['ShiftLeft'] = true; } else if (e.code === 'ControlLeft' || e.code === 'ControlRight') { keys['ControlLeft'] = true; } if (e.code >= 'Digit1' && e.code <= 'Digit9') { selectedSlot = parseInt(e.code.replace('Digit', ''), 10) - 1; updateInventoryUI(); } if (e.code === 'KeyB' && !e.repeat) { if (keys['KeyN']) dropSelectedItem(true); else dropSelectedItem(false); } if (e.code === 'KeyN' && keys['KeyB'] && !e.repeat) { dropSelectedItem(true); } if (e.code === 'KeyI') { player.pos.copy(player.spawnPos); player.vel.set(0, 0, 0); audio.playBeep(220, 'square', 0.25, 0.15); } } function handleKeyUp(e) { if (e.code in keys) { keys[e.code] = false; } else if (e.code === 'Space') { keys['Space'] = false; } else if (e.code === 'ShiftLeft' || e.code === 'ShiftRight') { keys['ShiftLeft'] = false; } else if (e.code === 'ControlLeft' || e.code === 'ControlRight') { keys['ControlLeft'] = false; } } function handleMouseMove(e) { if (!gameActive || isPaused) return; if (document.pointerLockElement === document.body) { camera.rotation.y -= e.movementX * 0.0022; camera.rotation.x -= e.movementY * 0.0022; camera.rotation.x = Math.max(-Math.PI / 2.1, Math.min(Math.PI / 2.1, camera.rotation.x)); } } function handleMouseDown(e) { if (!gameActive || isPaused || document.pointerLockElement !== document.body) return; if (e.button === 0) { isMouseBreakHeld = true; startBreaking(); } else if (e.button === 2) { const slotItem = inventory[selectedSlot]; if (slotItem && slotItem.type === 'guardNote') { openGuardNoteModal(); return; } triggerWorldInteraction('place'); } } function handleMouseUp(e) { if (e.button === 0) { isMouseBreakHeld = false; breakTarget = null; breakProgress = 0; const progEl = document.getElementById('break-progress'); if (progEl) progEl.style.display = 'none'; } } function createHorrorGiantMesh() { const group = new THREE.Group(); const bodyMat = new THREE.MeshLambertMaterial({ color: 0x2a1a1a }); const eyeMat = new THREE.MeshBasicMaterial({ color: 0xffffff }); const bodyGeo = new THREE.BoxGeometry(2.2, 3.2, 1.6); const body = new THREE.Mesh(bodyGeo, bodyMat); body.position.y = 2.8; group.add(body); const headGeo = new THREE.BoxGeometry(1.4, 1.3, 1.2); const head = new THREE.Mesh(headGeo, bodyMat); head.position.y = 5.0; group.add(head); const eyeGeo = new THREE.BoxGeometry(0.28, 0.28, 0.12); const eyeL = new THREE.Mesh(eyeGeo, eyeMat); eyeL.position.set(-0.35, 5.1, 0.62); const eyeR = new THREE.Mesh(eyeGeo, eyeMat); eyeR.position.set(0.35, 5.1, 0.62); group.add(eyeL, eyeR); const armGeo = new THREE.BoxGeometry(0.55, 2.4, 0.55); const armPositions = [ [-1.45, 3.2, 0.35], [1.45, 3.2, 0.35], [-1.15, 3.0, -0.45], [1.15, 3.0, -0.45] ]; const arms = []; armPositions.forEach(pos => { const arm = new THREE.Mesh(armGeo, bodyMat); arm.position.set(pos[0], pos[1], pos[2]); group.add(arm); arms.push(arm); }); const legGeo = new THREE.BoxGeometry(0.7, 1.6, 0.7); const legL = new THREE.Mesh(legGeo, bodyMat); legL.position.set(-0.55, 0.8, 0); const legR = new THREE.Mesh(legGeo, bodyMat); legR.position.set(0.55, 0.8, 0); group.add(legL, legR); group.scale.set(1.15, 1.15, 1.15); return { group, body, arms, legL, legR, eyeL, eyeR }; } function spawnHorrorGiant() { if (horrorGiant) return; const meshData = createHorrorGiantMesh(); const sx = 80 + Math.random() * 240; const sz = 80 + Math.random() * 240; const sy = getTerrainHeight(sx, sz) + 2; meshData.group.position.set(sx, sy, sz); scene.add(meshData.group); horrorGiant = { obj: meshData.group, body: meshData.body, arms: meshData.arms, legL: meshData.legL, legR: meshData.legR, health: 15, maxHealth: 15, targetPlayer: false, attackCooldown: 0, wanderTimer: 0, wanderDir: new THREE.Vector3(1, 0, 0), currentTarget: null, currentTargetType: null }; } function getHorrorGiantForward(g) { const fwd = new THREE.Vector3(0, 0, 1); fwd.applyQuaternion(g.obj.quaternion); fwd.y = 0; return fwd.normalize(); } function findEnemyMobInFront(g) { const origin = g.obj.position.clone(); origin.y += 3; const forward = getHorrorGiantForward(g); let best = null; let bestType = null; let bestDist = 14; towerGuards.forEach((guard) => { const pos = guard.obj.position; const toEnemy = new THREE.Vector3().subVectors(pos, origin); toEnemy.y = 0; const dist = toEnemy.length(); if (dist > 14 || dist < 0.5) return; toEnemy.normalize(); if (forward.dot(toEnemy) > 0.55 && dist < bestDist) { best = { obj: guard.obj, pos, type: 'guard' }; bestType = 'guard'; bestDist = dist; } }); killerFishList.forEach((fish) => { const pos = fish.obj.position; const toEnemy = new THREE.Vector3().subVectors(pos, origin); toEnemy.y = 0; const dist = toEnemy.length(); if (dist > 12 || dist < 0.5) return; toEnemy.normalize(); if (forward.dot(toEnemy) > 0.5 && dist < bestDist) { best = { obj: fish.obj, pos, type: 'fish' }; bestType = 'fish'; bestDist = dist; } }); if (best) { g.currentTarget = best; g.currentTargetType = bestType; return best; } return null; } function horrorGiantAttackTarget(g) { if (g.attackCooldown > 0) return; const target = g.currentTarget; if (!target) return; const dist = g.obj.position.distanceTo(target.pos); if (dist > 2.2) return; g.attackCooldown = 1.4; flashMobRed(target.obj); audio.playBreakTıkTık(); if (target.type === 'guard') { const idx = towerGuards.findIndex(gr => gr.obj === target.obj); if (idx < 0) return; const guard = towerGuards[idx]; guard.health -= 5; guard.isTriggered = true; guard.isDamaged = true; guard.eyes.forEach(eye => { eye.material = new THREE.MeshBasicMaterial({ color: 0xff0000 }); }); if (guard.health <= 0) killTowerGuard(idx); } else if (target.type === 'fish') { const idx = killerFishList.findIndex(f => f.obj === target.obj); if (idx < 0) return; const fish = killerFishList[idx]; fish.health -= 5; if (fish.health <= 0) { scene.remove(fish.obj); killerFishList.splice(idx, 1); audio.playScream(1); } } } function updateHorrorGiantLogic(dt) { if (!horrorGiant) return; const g = horrorGiant; if (g.attackCooldown > 0) g.attackCooldown -= dt; let moveTarget = null; if (g.targetPlayer) { moveTarget = player.pos.clone(); moveTarget.y = g.obj.position.y; } else { const enemy = findEnemyMobInFront(g); if (enemy) { moveTarget = enemy.pos.clone(); moveTarget.y = g.obj.position.y; } else { g.currentTarget = null; g.wanderTimer -= dt; if (g.wanderTimer <= 0) { g.wanderTimer = 4 + Math.random() * 6; g.wanderDir.set( (Math.random() - 0.5) * 2, 0, (Math.random() - 0.5) * 2 ).normalize(); } moveTarget = g.obj.position.clone().addScaledVector(g.wanderDir, 30); moveTarget.y = g.obj.position.y; } } const dir = new THREE.Vector3().subVectors(moveTarget, g.obj.position); dir.y = 0; const len = dir.length(); if (len > 0.5) { dir.normalize(); g.obj.lookAt(g.obj.position.clone().add(dir)); const speed = g.targetPlayer ? 4.2 : 2.8; g.obj.position.addScaledVector(dir, Math.min(len, speed * dt)); } const groundY = getTerrainHeight(g.obj.position.x, g.obj.position.z) + 2; g.obj.position.y += (groundY - g.obj.position.y) * 0.15; const wave = Math.sin(clock.getElapsedTime() * 6); g.legL.rotation.x = wave * 0.45; g.legR.rotation.x = -wave * 0.45; g.arms.forEach((arm, i) => { arm.rotation.x = (i % 2 === 0 ? 1 : -1) * wave * 0.35; }); if (g.currentTarget && !g.targetPlayer) { horrorGiantAttackTarget(g); } if (g.targetPlayer && !isPaused) { const dist = g.obj.position.distanceTo(player.pos); if (dist < 2.5 && g.attackCooldown <= 0) { g.attackCooldown = 1.5; damagePlayer(5); player.vel.y = 0.12; const push = new THREE.Vector3().subVectors(player.pos, g.obj.position).normalize(); player.pos.addScaledVector(push, 1.2); audio.playScream(2); } } } function createKillerFishMesh() { const group = new THREE.Group(); const bodyMat = new THREE.MeshLambertMaterial({ color: 0x800080 }); const eyeMat = new THREE.MeshBasicMaterial({ color: 0xff0000 }); const headGeo = new THREE.BoxGeometry(0.3, 0.2, 0.4); const head = new THREE.Mesh(headGeo, bodyMat); head.position.set(0, 0, 0.2); group.add(head); const eyeGeo = new THREE.BoxGeometry(0.05, 0.05, 0.05); const leftEye = new THREE.Mesh(eyeGeo, eyeMat); leftEye.position.set(-0.16, 0.05, 0.3); const rightEye = new THREE.Mesh(eyeGeo, eyeMat); rightEye.position.set(0.16, 0.05, 0.3); group.add(leftEye, rightEye); const segGeo = new THREE.BoxGeometry(0.24, 0.16, 0.3); for (let i = 0; i < 3; i++) { const seg = new THREE.Mesh(segGeo, bodyMat); seg.position.set(0, 0, -0.15 - (i * 0.28)); group.add(seg); } return group; } function spawnKillerFish() { if (killerFishList.length >= 3) return; let px = Math.floor(player.pos.x); let pz = Math.floor(player.pos.z); let found = false; let sx, sy, sz; for (let attempt = 0; attempt < 200; attempt++) { const rx = px + Math.floor((Math.random() - 0.5) * 120); const rz = pz + Math.floor((Math.random() - 0.5) * 120); const ry = 8 + Math.floor(Math.random() * 10); if (getBlock(rx, ry, rz) === 'water') { sx = rx; sy = ry; sz = rz; found = true; break; } } if (found) { const fishObj = createKillerFishMesh(); fishObj.position.set(sx, sy, sz); scene.add(fishObj); killerFishList.push({ obj: fishObj, health: 3, swimTimer: Math.random() * 5, swimTarget: new THREE.Vector3(sx, sy, sz) }); } } function updateKillerFishLogic(dt) { if (killerFishList.length < 3 && Math.random() < 0.05) { spawnKillerFish(); } const playerPos = player.pos.clone(); playerPos.y += 0.9; for (let i = killerFishList.length - 1; i >= 0; i--) { const fish = killerFishList[i]; const dist = fish.obj.position.distanceTo(player.pos); const playerInWaterZone = player.pos.y <= SEA_LEVEL + 1; let speed = 2.0; let isChasing = false; if (playerInWaterZone && dist < 25) { isChasing = true; speed = 5.5; fish.swimTarget.copy(playerPos); } else { fish.swimTimer -= dt; if (fish.swimTimer <= 0) { fish.swimTimer = 3 + Math.random() * 5; fish.swimTarget.set( fish.obj.position.x + (Math.random() - 0.5) * 15, Math.max(8, Math.min(SEA_LEVEL - 1, fish.obj.position.y + (Math.random() - 0.5) * 4)), fish.obj.position.z + (Math.random() - 0.5) * 15 ); } } const dir = new THREE.Vector3().subVectors(fish.swimTarget, fish.obj.position); const targetDist = dir.length(); if (targetDist > 0.1) { dir.normalize(); fish.obj.lookAt(fish.obj.position.clone().add(dir)); const moveDist = speed * dt; fish.obj.position.addScaledVector(dir, Math.min(moveDist, targetDist)); } if (fish.obj.position.y > SEA_LEVEL - 0.2) { fish.obj.position.y = SEA_LEVEL - 0.2; } if (dist < 1.4 && !isPaused) { triggerFishHit(); damagePlayer(1); const pushDir = new THREE.Vector3().subVectors(fish.obj.position, player.pos).normalize(); fish.obj.position.addScaledVector(pushDir, 3.0); } } if (fishDebuffTimer > 0) { fishDebuffTimer -= dt; fishDebuffFlashTimer += dt; if (fishDebuffFlashTimer >= 0.5) { fishDebuffFlashTimer = 0; fishDebuffScreenActive = !fishDebuffScreenActive; const debuffOverlay = document.getElementById('fish-debuff-overlay'); if (debuffOverlay) { debuffOverlay.style.opacity = fishDebuffScreenActive ? "0.9" : "0.0"; } } if (fishDebuffTimer <= 0) { const debuffOverlay = document.getElementById('fish-debuff-overlay'); if (debuffOverlay) debuffOverlay.style.opacity = "0"; } } } function triggerFishHit() { if (fishDebuffTimer <= 0) { audio.playScream(1); } fishDebuffTimer = 10.0; fishDebuffFlashTimer = 0; fishDebuffScreenActive = true; const debuffOverlay = document.getElementById('fish-debuff-overlay'); if (debuffOverlay) debuffOverlay.style.opacity = "0.9"; } function updateTowerGuardsLogic(dt) { const playerTowerIdx = getTowerBiomeIndex(player.pos.x, player.pos.z); towerGuards.forEach(g => { const playerInSameBiome = playerTowerIdx === g.biomeIdx; const distToPlayer = g.obj.position.distanceTo(player.pos); let speed = (g.isTriggered && playerInSameBiome) ? 3.8 : 1.2; // 40x40 boyutuna göre sınırlar ayarlandı (rx > 20 || rz > 20) const rx = Math.abs(g.obj.position.x - g.homeCenter.x); const rz = Math.abs(g.obj.position.z - g.homeCenter.z); const outOfBounds = rx > 20 || rz > 20; let target = new THREE.Vector3(); if (g.isTriggered && playerInSameBiome) { target.copy(player.pos); target.y = g.obj.position.y; } else { g.animTime += dt; if (g.animTime > 6.0) { g.animTime = 0; g.velocity.set( (Math.random() - 0.5) * 10, 0, (Math.random() - 0.5) * 10 ).normalize(); } target.copy(g.obj.position).addScaledVector(g.velocity, 2); } const dir = new THREE.Vector3().subVectors(target, g.obj.position); dir.y = 0; const len = dir.length(); if (len > 0.1) { dir.normalize(); g.obj.lookAt(g.obj.position.clone().add(dir)); g.obj.position.addScaledVector(dir, speed * dt); } if (outOfBounds) { const toHome = new THREE.Vector3().subVectors(g.spawnPos, g.obj.position).normalize(); g.obj.position.addScaledVector(toHome, 1.5); } const wave = Math.sin(clock.getElapsedTime() * (g.isTriggered ? 12 : 5)); g.legL.rotation.x = wave * 0.5; g.legR.rotation.x = -wave * 0.5; g.armL.rotation.x = -wave * 0.4; g.armR.rotation.x = wave * 0.4; if (g.attackCooldown > 0) g.attackCooldown -= dt; if (g.isDamaged && playerInSameBiome) { if (g.bodyMesh) g.bodyMesh.material.color.setHex(0xaa3333); if (performance.now() - lastTowerScreamTime > 2500 && Math.random() < 0.02) { audio.playScream(2); lastTowerScreamTime = performance.now(); } } if (distToPlayer < 1.3 && !isPaused && g.attackCooldown <= 0 && playerInSameBiome && g.isTriggered) { player.vel.y = 0.1; const push = new THREE.Vector3().subVectors(player.pos, g.obj.position).normalize(); player.pos.addScaledVector(push, 0.8); damagePlayer(2); g.attackCooldown = 1.2; if (g.isDamaged && performance.now() - lastTowerScreamTime > 800) { audio.playScream(2); lastTowerScreamTime = performance.now(); } } }); } function triggerErrorMinus1() { gameActive = false; isPaused = true; document.exitPointerLock(); audio.stopScaryDrone(); audio.stopTowerHum(); stopScaryGlitches(); const errScreen = document.getElementById('error-screen'); if (errScreen) { errScreen.classList.remove('hidden'); } if (animationFrameId) { cancelAnimationFrame(animationFrameId); animationFrameId = null; } } function resetGameToMenu() { gameActive = false; isPaused = false; if (animationFrameId) { cancelAnimationFrame(animationFrameId); animationFrameId = null; } stopScaryGlitches(); audio.stopScaryDrone(); audio.stopTowerHum(); if (renderer) { renderer.dispose(); if (renderer.domElement && renderer.domElement.parentNode) { renderer.domElement.parentNode.removeChild(renderer.domElement); } } worldBlocks.clear(); chunkMeshes.clear(); generatedChunks.clear(); grayBiomeTrees = []; activeChunks = []; creepyObservers = []; towerGuards = []; killerFishList.forEach(f => scene.remove(f.obj)); killerFishList.length = 0; if (horrorGiant && horrorGiant.obj) { scene.remove(horrorGiant.obj); horrorGiant.obj.traverse(c => { if (c.geometry) c.geometry.dispose(); if (c.material) c.material.dispose(); }); } horrorGiant = null; cloudDataList = []; mergedCloudRainZones = []; isRainActive = false; sandRainHits.clear(); cloudMergeCheckTimer = 0; const rainOverlay = document.getElementById('rain-overlay'); if (rainOverlay) rainOverlay.style.opacity = '0'; closeGuardNoteModal(); droppedItems.forEach(d => scene.remove(d.mesh)); droppedItems.length = 0; fishDebuffTimer = 0; isMouseBreakHeld = false; breakTarget = null; const debuffOverlay = document.getElementById('fish-debuff-overlay'); if (debuffOverlay) debuffOverlay.style.opacity = "0"; document.getElementById('pause-menu').classList.add('hidden'); document.getElementById('ui-overlay').style.display = 'none'; document.getElementById('gpu-monitor').style.display = 'none'; document.getElementById('crosshair').style.display = 'none'; document.getElementById('hud-bottom').style.display = 'none'; const progEl = document.getElementById('break-progress'); if (progEl) progEl.style.display = 'none'; const menu = document.getElementById('menu'); if (menu) { menu.style.display = 'flex'; setTimeout(() => { menu.style.opacity = '1'; }, 50); } } function updateAffectedChunks(bx, by, bz) { const cx = Math.floor(bx / CHUNK_SIZE); const cz = Math.floor(bz / CHUNK_SIZE); const modX = ((bx % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE; const modZ = ((bz % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE; rebuildChunk(cx, cz); if (modX === 0) rebuildChunk(cx - 1, cz); if (modX === CHUNK_SIZE - 1) rebuildChunk(cx + 1, cz); if (modZ === 0) rebuildChunk(cx, cz - 1); if (modZ === CHUNK_SIZE - 1) rebuildChunk(cx, cz + 1); if (by === 0 || by === MAX_WORLD_HEIGHT) { rebuildChunk(cx, cz); } } function onWindowResize() { if (!gameActive || !renderer) return; camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); } window.addEventListener('resize', onWindowResize); const scaryMessages = [ "HUKAHUKA HUKA?", "I DIDN'T BUY THE GAME, I'M IN IT. -HUKA", "GAME IS DELETE GAME IS DELET GAME GAME", "HUKA IS WATCHING YOU!" ]; let messageIndex = 0; let glitchInterval = null; function startScaryGlitches() { if(glitchInterval) return; const overlay = document.getElementById('scary-overlay'); const scaryText = document.getElementById('scary-text'); if (overlay && scaryText) { overlay.classList.remove('hidden'); } glitchInterval = setInterval(() => { messageIndex = (messageIndex + 1) % scaryMessages.length; if (scaryText) { scaryText.innerText = scaryMessages[messageIndex]; } if (overlay) { overlay.style.backgroundColor = `rgba(150, 0, 0, ${Math.random() * 0.4 + 0.2})`; } audio.playBeep(60 + Math.random() * 40, 'sawtooth', 0.25, 0.2); }, 1000); } function stopScaryGlitches() { if(!glitchInterval) return; clearInterval(glitchInterval); glitchInterval = null; const overlay = document.getElementById('scary-overlay'); if (overlay) { overlay.classList.add('hidden'); } } function animate() { if (!gameActive) return; animationFrameId = requestAnimationFrame(animate); const dt = clock.getDelta(); updatePhysics(); if (playerDamageCooldown > 0) playerDamageCooldown -= dt; if (attackCooldown > 0) attackCooldown -= dt; if (!isPaused) { updateBreaking(dt); updateDroppedItems(dt); updateKillerFishLogic(dt); updateTowerGuardsLogic(dt); updateHorrorGiantLogic(dt); updateWeatherSystem(dt); } frames++; const now = performance.now(); if (now >= lastTime + 1000) { currentFps = Math.round((frames * 1000) / (now - lastTime)); const fpsEl = document.getElementById('gpu-fps'); if (fpsEl) { fpsEl.innerText = currentFps; } if (currentFps < 18) { lowFpsCount++; if (lowFpsCount >= 5) { triggerErrorMinus1(); return; } } else { lowFpsCount = 0; } frames = 0; lastTime = now; } const px = Math.floor(player.pos.x / CHUNK_SIZE); const pz = Math.floor(player.pos.z / CHUNK_SIZE); const coordsEl = document.getElementById('ui-coords'); if (coordsEl) { coordsEl.innerText = `X: ${Math.round(player.pos.x)} | Y: ${Math.round(player.pos.y)} | Z: ${Math.round(player.pos.z)}`; } const cameraY = camera.position.y; const wasUnderwater = player.isUnderwater; player.isUnderwater = (cameraY < SEA_LEVEL); if (player.isUnderwater && !wasUnderwater) { audio.playWaterSound(); } // GÜNDÜZ - ÖĞLE - AKŞAM - GECE EVRELERİ dayNightTimer += dt; if (dayNightTimer >= TOTAL_CYCLE_DURATION) { dayNightTimer = 0; hasCheckedBloodMoonThisNight = false; isBloodMoon = false; } let phaseName = "Gündüz"; let celestialAngle = 0; let targetSkyColor = 0x87b8e8; let targetFogDensity = 0.018; let targetAmbientIntensity = 0.55; let targetSunIntensity = 1.35; let targetHemiIntensity = 0.55; let targetHemiSky = 0xb8d8f8; let targetHemiGround = 0x6b8f4e; let targetMoonIntensity = 0; let starOpacity = 0.0; isNightPhase = false; if (dayNightTimer < DAY_DURATION) { phaseName = "Gündüz"; const t = dayNightTimer / DAY_DURATION; celestialAngle = t * Math.PI; targetSkyColor = 0x87b8e8; targetFogDensity = 0.014; targetAmbientIntensity = 0.5; targetSunIntensity = 1.2 + Math.sin(t * Math.PI) * 0.25; targetHemiIntensity = 0.6; targetHemiSky = 0xc8e4ff; targetHemiGround = 0x5a8a3a; VIEW_DISTANCE = 3; } else if (dayNightTimer < DAY_DURATION + AFTERNOON_DURATION) { phaseName = "Öğle"; const localTimer = dayNightTimer - DAY_DURATION; const t = localTimer / AFTERNOON_DURATION; celestialAngle = Math.PI + t * (Math.PI * 0.5); targetSkyColor = 0xe8a860; targetFogDensity = 0.016; targetAmbientIntensity = 0.42; targetSunIntensity = 0.85; targetHemiIntensity = 0.48; targetHemiSky = 0xffd090; targetHemiGround = 0x8a6030; } else if (dayNightTimer < DAY_DURATION + AFTERNOON_DURATION + EVENING_DURATION) { phaseName = "Akşam"; const localTimer = dayNightTimer - (DAY_DURATION + AFTERNOON_DURATION); const t = localTimer / EVENING_DURATION; celestialAngle = Math.PI * 1.5 + t * (Math.PI * 0.3); targetSkyColor = 0x6a2818; targetFogDensity = 0.028; targetAmbientIntensity = 0.22; targetSunIntensity = 0.15 + (1 - t) * 0.2; targetHemiIntensity = 0.28; targetHemiSky = 0x884433; targetHemiGround = 0x2a1810; } else { phaseName = "Gece"; isNightPhase = true; const localTimer = dayNightTimer - (DAY_DURATION + AFTERNOON_DURATION + EVENING_DURATION); celestialAngle = Math.PI * 1.8 + (localTimer / NIGHT_DURATION) * (Math.PI * 1.2); if (!hasCheckedBloodMoonThisNight) { hasCheckedBloodMoonThisNight = true; isBloodMoon = Math.random() < 0.01; } if (isBloodMoon) { phaseName = "🩸 KANLI AY 🩸"; targetSkyColor = 0x220101; targetFogDensity = 0.1; targetAmbientIntensity = 0.04; targetSunIntensity = 0.02; targetHemiIntensity = 0.08; targetHemiSky = 0x440000; targetHemiGround = 0x110000; targetMoonIntensity = 0.15; } else { targetSkyColor = 0x060a18; targetFogDensity = 0.055; targetAmbientIntensity = 0.08; targetSunIntensity = 0.0; targetHemiIntensity = 0.15; targetHemiSky = 0x1a2848; targetHemiGround = 0x0a0a14; targetMoonIntensity = 0.35; } starOpacity = 1.0; VIEW_DISTANCE = 3; } const timeEl = document.getElementById('ui-time'); if (timeEl) { timeEl.innerText = phaseName; timeEl.className = isBloodMoon ? "text-red-500 font-bold glitch-text" : "text-cyan-400"; } const sunX = Math.cos(celestialAngle) * 120; const sunY = Math.sin(celestialAngle) * 120; const sunZ = 30; sunMesh.position.set(player.pos.x + sunX, player.pos.y + Math.max(-10, sunY), player.pos.z + sunZ); sunLight.position.copy(sunMesh.position); sunLight.target.position.copy(player.pos); sunLight.shadow.camera.position.copy(sunLight.position); const moonX = Math.cos(celestialAngle + Math.PI) * 120; const moonY = Math.sin(celestialAngle + Math.PI) * 120; const moonZ = -30; moonMesh.position.set(player.pos.x + moonX, player.pos.y + Math.max(-10, moonY), player.pos.z + moonZ); moonLight.position.set(player.pos.x + moonX, player.pos.y + Math.max(-10, moonY), player.pos.z + moonZ); moonLight.target.position.copy(player.pos); if (isBloodMoon) { moonMat.color.setHex(0xff0000); } else { moonMat.color.setHex(0xdddddd); } if (starsField) { starsField.material.opacity += (starOpacity - starsField.material.opacity) * 0.05; starsField.position.copy(player.pos); } if (isBloodMoon && !isPaused) { startScaryGlitches(); if (blackoutTimer <= 0 && Math.random() < 0.0015 && (now - lastBlackoutTrigger > 12000)) { blackoutTimer = 3.0; lastBlackoutTrigger = now; audio.playScream(2); } } else if (!insideGrayBiome && !insideTowerBiome) { stopScaryGlitches(); } const blackoutOverlay = document.getElementById('blackout-overlay'); if (blackoutTimer > 0) { blackoutTimer -= dt; if (blackoutOverlay) blackoutOverlay.style.opacity = "1"; } else { if (blackoutOverlay) blackoutOverlay.style.opacity = "0"; } const currentBiomeId = getActiveSpecialBiome(player.pos.x, player.pos.z); const towerBiomeIdx = getTowerBiomeIndex(player.pos.x, player.pos.z); const playerInsideTowerBiome = towerBiomeIdx >= 0; if (playerInsideTowerBiome && !isPaused) { if (!insideTowerBiome || insideTowerBiomeIdx !== towerBiomeIdx) { insideTowerBiome = true; insideTowerBiomeIdx = towerBiomeIdx; const biomeEl = document.getElementById('ui-biome'); if (biomeEl) { biomeEl.innerText = "ESKİ KULELER (Old Towers)"; biomeEl.className = "text-yellow-600 font-black"; } stopScaryGlitches(); audio.stopScaryDrone(); reactivateTowerGuardsOnBiomeEnter(towerBiomeIdx); } } else if (currentBiomeId > 0 && !isPaused) { targetSkyColor = 0x030101; targetFogDensity = 0.12; targetAmbientIntensity = 0.02; targetSunIntensity = 0.02; audio.startScaryDrone(); if (!insideGrayBiome || insideGrayBiomeId !== currentBiomeId) { insideGrayBiome = true; insideGrayBiomeId = currentBiomeId; const biomeEl = document.getElementById('ui-biome'); if (biomeEl) { biomeEl.innerText = `HUKA'S GRAY LANDS (Biyom ${currentBiomeId})`; biomeEl.className = "text-red-600 font-black glitch-text"; } startScaryGlitches(); } } else { if (insideTowerBiome) { const exitedIdx = insideTowerBiomeIdx; if (exitedIdx >= 0) resetTowerGuardsOnBiomeExit(exitedIdx); insideTowerBiome = false; insideTowerBiomeIdx = -1; const biomeEl = document.getElementById('ui-biome'); if (biomeEl) { biomeEl.innerText = "Sakin Orman"; biomeEl.className = "text-green-400 font-bold"; } stopScaryGlitches(); audio.stopTowerHum(); audio.stopScaryDrone(); } if (insideGrayBiome) { insideGrayBiome = false; insideGrayBiomeId = 0; const biomeEl = document.getElementById('ui-biome'); if (biomeEl) { biomeEl.innerText = "Sakin Orman"; biomeEl.className = "text-green-400 font-bold"; } stopScaryGlitches(); audio.stopScaryDrone(); } } scene.fog.color.lerp(new THREE.Color(targetSkyColor), 0.03); scene.fog.density += (targetFogDensity - scene.fog.density) * 0.03; ambientLight.intensity += (targetAmbientIntensity - ambientLight.intensity) * 0.03; sunLight.intensity += (targetSunIntensity - sunLight.intensity) * 0.03; if (hemiLight) { hemiLight.intensity += (targetHemiIntensity - hemiLight.intensity) * 0.03; hemiLight.color.lerp(new THREE.Color(targetHemiSky), 0.03); hemiLight.groundColor.lerp(new THREE.Color(targetHemiGround), 0.03); } if (moonLight) { moonLight.intensity += (targetMoonIntensity - moonLight.intensity) * 0.03; } if (playerTorch) { const torchTarget = isNightPhase && !isBloodMoon ? 1.8 : (isBloodMoon ? 0.6 : 0); playerTorch.intensity += (torchTarget - playerTorch.intensity) * 0.08; } renderer.setClearColor(scene.fog.color); scene.background.lerp(new THREE.Color(targetSkyColor), 0.03); creepyObservers.forEach((obs) => { const targetPos = new THREE.Vector3(player.pos.x, obs.obj.position.y, player.pos.z); obs.obj.lookAt(targetPos); const distanceToObserver = player.pos.distanceTo(obs.obj.position); if (distanceToObserver < 6.5 && !isPaused && currentBiomeId === obs.id) { const timeNow = performance.now(); if (timeNow - lastScreamTime > 4500) { audio.playScream(2); lastScreamTime = timeNow; const scaryOverlay = document.getElementById('scary-overlay'); if (scaryOverlay) { scaryOverlay.style.backgroundColor = `rgba(${160 + Math.random() * 95}, 0, 0, ${Math.random() * 0.7 + 0.3})`; } } } }); grayBiomeTrees.forEach(tree => { const chunkParts = tree.chunkKey.split(','); const tcx = parseInt(chunkParts[0]); const tcz = parseInt(chunkParts[1]); const distToChunk = Math.sqrt((tcx - px) ** 2 + (tcz - pz) ** 2); if (distToChunk > VIEW_DISTANCE) { tree.obj.visible = false; } else { tree.obj.visible = true; if (insideGrayBiome && insideGrayBiomeId === tree.biomeId && !isPaused) { if (tree.obj.position.y > tree.startY - 7) { tree.obj.position.y -= 0.04; tree.obj.scale.setScalar(Math.max(0.01, tree.obj.scale.x - 0.005)); } else if (!tree.isSunk) { tree.isSunk = true; scene.remove(tree.obj); } } else { if (tree.obj.position.y < tree.startY) { if(tree.isSunk) { scene.add(tree.obj); tree.isSunk = false; } tree.obj.position.y += 0.08; tree.obj.scale.setScalar(Math.min(1.0, tree.obj.scale.x + 0.01)); } } } }); let sleepingCount = 0; const totalChunkCount = chunkMeshes.size; const camDirection = new THREE.Vector3(); camera.getWorldDirection(camDirection); chunkMeshes.forEach((meshGroup, key) => { const parts = key.split(','); const cx = parseInt(parts[0]); const cz = parseInt(parts[1]); const chunkCenterX = (cx * CHUNK_SIZE) + (CHUNK_SIZE / 2); const chunkCenterZ = (cz * CHUNK_SIZE) + (CHUNK_SIZE / 2); const chunkCenter = new THREE.Vector3(chunkCenterX, player.pos.y, chunkCenterZ); const toChunk = new THREE.Vector3().subVectors(chunkCenter, player.pos); const dist = toChunk.length(); toChunk.normalize(); const dot = camDirection.dot(toChunk); if (dist > (VIEW_DISTANCE * CHUNK_SIZE + 6) || (dist > 14 && dot < 0.15)) { meshGroup.visible = false; sleepingCount++; } else { meshGroup.visible = true; } }); const sleepingEl = document.getElementById('gpu-sleeping'); if (sleepingEl) { sleepingEl.innerText = sleepingCount; } const totalChunksEl = document.getElementById('gpu-total-chunks'); if (totalChunksEl) { totalChunksEl.innerText = totalChunkCount; } if (!isPaused) { for (let x = px - VIEW_DISTANCE; x <= px + VIEW_DISTANCE; x++) { for (let z = pz - VIEW_DISTANCE; z <= pz + VIEW_DISTANCE; z++) { createChunk(x, z); } } } renderer.render(scene, camera); }