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);
}