diff --git a/public/engine.js b/public/engine.js new file mode 100644 index 0000000..bd1015d --- /dev/null +++ b/public/engine.js @@ -0,0 +1,262 @@ +/* ===================================================================== + * engine.js — SAF 2D CANVAS RAYCASTING MOTORU (Wolfenstein 1992 klonu) + * --------------------------------------------------------------------- + * Three.js / Babylon.js / WebGL KULLANILMAZ. + * Tüm "3D" görüntü, ekranın her dikey piksel sütunu için fırlatılan + * ışınların (DDA) 2D canvas 'fillRect' boyaması ile üretilir. + * ===================================================================== */ + +'use strict'; + +/* ---------------- HARİTA (16 x 16) ---------------- + * 0 = boş 1 = neon mavi 2 = yeşil 3 = kırmızı tuğla + * Harita dış sınırları her zaman duvarla kaplı (labirent kapalı). */ +const MAP = [ + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + [1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 3, 0, 0, 0, 1], + [1, 0, 0, 2, 0, 0, 1, 0, 0, 0, 0, 3, 0, 0, 0, 1], + [1, 0, 0, 2, 0, 0, 1, 0, 0, 1, 1, 3, 0, 0, 0, 1], + [1, 0, 0, 2, 0, 0, 1, 0, 0, 1, 0, 0, 0, 2, 0, 1], + [1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 2, 0, 1], + [1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1], + [1, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1], + [1, 0, 2, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 1, 0, 1], + [1, 0, 0, 0, 1, 0, 1, 1, 3, 1, 1, 0, 0, 0, 0, 1], + [1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], + [1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 3, 0, 1], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], +]; + +/* Duvar türü -> renk paleti + * Her duvar: taban renk + yan(kenar) deskendirme vurgusu. */ +const WALL_PALETTE = { + 1: { base: '#2fd4ff', edge: '#17b8e0', dark: '#0a6b86' }, // neon mavi (VFD 'Mavi') + 2: { base: '#4ce06a', edge: '#31c24e', dark: '#167537' }, // yeşil + 3: { base: '#ff5f4d', edge: '#d63f2f', dark: '#8c1f16' } // kırmızı tuğla tonu +}; + +const GRID = MAP[0].length; // 16 +const CELL = 1; // mantık birimi olarak hücre kenarı = 1 + +/* ---------------- OYUNCU ---------------- + * Isın sınıfını (NESNE) true dünyada W, S, A, D ve ok tuşlarıyla + * döndürüp ilerletiyoruz. */ +const player = { + x: 2.5, // harita birimi cinsinden başlangıç (boş hücre 1.5 civarı) + y: 2.5, + angle: 0.52, // radyan cinsinden bakış (sağa) +}; + +/* Görüntü çözünürlüğü (retro düşük çözünürlük: dilimler görünür olur) */ +const RAY_W = 4; // her sütun genişliği (px) — düşük tut → retro +const FOV = (60 * Math.PI) / 180; // 60 derece görüş alanı + +/* Zemin-tavan degrade renkleri */ +const CEIL_TOP = '#14161c'; // koyu gri-siyah +const CEIL_BOTTOM = '#22262e'; +const FLOOR_TOP = '#241f23'; // koyu füme +const FLOOR_BOTTOM= '#120f11'; + +/* Yön vektörü yardımcıları */ +function dirX(a) { return Math.cos(a); } +function dirY(a) { return Math.sin(a); } + +/* ---------------- DDA RAYCAST (tek ışın) ---------------- + * Bir sütun için en yakın duvarı DDA algoritması ile arar. + * Dönüş: { dist (dik mesafe), side(0=N-S,1=E-W), wallTip, cell } + * side: 0 → duvarın dikey(NS) yüzü, 1 → yatay(EW) yüzü. */ +function castRay(ox, oy, ang, maxDist) { + const mapX = Math.floor(ox), mapY = Math.floor(oy); + const dx = dirX(ang), dy = dirY(ang); + + // Başlangıç ışın ~ hücre kenarı mesafeleri (DDA adımları) + let deltaDistX = Math.abs(dx) < 1e-9 ? 1e9 : Math.abs(1 / dx); + let deltaDistY = Math.abs(dy) < 1e-9 ? 1e9 : Math.abs(1 / dy); + + let stepX, stepY, sideDistX, sideDistY; + if (dx < 0) { stepX = -1; sideDistX = (ox - mapX) * deltaDistX; } + else { stepX = 1; sideDistX = (mapX + 1 - ox) * deltaDistX; } + if (dy < 0) { stepY = -1; sideDistY = (oy - mapY) * deltaDistY; } + else { stepY = 1; sideDistY = (mapY + 1 - oy) * deltaDistY; } + + let side = 0; // 0=NS duvarı, 1=EW duvarı + let cellX = mapX, cellY = mapY; + let tile = 0; + let guard = 0; + + while (tile === 0) { + if (sideDistX < sideDistY) { + sideDistX += deltaDistX; + cellX += stepX; + side = 0; + } else { + sideDistY += deltaDistY; + cellY += stepY; + side = 1; + } + if (cellX < 0 || cellY < 0 || cellX >= GRID || cellY >= GRID) break; + tile = MAP[cellY][cellX]; + if (++guard > GRID * 4) break; // sonsuz döngü koruması + } + + // Dik (perpendicular) mesafe — FISHEYE düzeltmesi burada uygulanır. + let perp = side === 0 + ? (sideDistX - deltaDistX) + : (sideDistY - deltaDistY); + + if (perp > maxDist) perp = maxDist; + return { dist: perp, side, wallTip: tile, cellX, cellY }; +} + +/* ---------------- GÖRÜNTÜLEME ---------------- + * Kanvası her karede, sütun sütun DDA ışın atarak boyar. + * Fişgözü düzeltmesi: dik mesafe alındığından duvarlar eğri değil. */ +function renderScene(ctx, w, h) { + ctx.imageSmoothingEnabled = false; + + // ---- Tavan (dikey dilimler halinde koyu gri → siyah degrade) ---- + drawVerticalGradient(ctx, w, 0, h / 2, CEIL_TOP, CEIL_BOTTOM); + + // ---- Zemin (dikey dilimler, koyu füme degrade) ---- + drawVerticalGradient(ctx, w, h / 2, h / 2, FLOOR_TOP, FLOOR_BOTTOM); + + // ---- Duvarlar: her sütun için bir ışın ---- + const numCols = Math.floor(w / RAY_W); + const halfH = h / 2; + const planeLen = Math.tan(FOV / 2); // projeksiyon düzlemi genişliği + const zBuffer = new Float32Array(numCols).fill(32); // sprite için duvar derinliği + + for (let col = 0; col < numCols; col++) { + // Sütunun merkez açısı: oyuncu açısı + [(col+0.5)/numCols - 0.5] * FOV + const f = (col + 0.5) / numCols - 0.5; // -0.5 .. +0.5 + const rayAng = player.angle + f * FOV; + + const hit = castRay(player.x, player.y, rayAng, 32); + zBuffer[col] = hit.dist; // sprite z-test için kaydet + + // Dik mesafe zaten düzeltilmiş → kavisli görünüm yok. + // Çizgi yüksekliği perspektif projeksiyonu (w genişliğinde bir + // ölçek; canvas yüksekliğine oranla kalibreli). + const wallH = (h * planeLen) / hit.dist; // pixel + const drawH = Math.min(wallH, h * 20); + const topY = halfH - drawH / 2; + + // Derinlik gölgelendirme: uzak mesafe → koyulaştır. + const shade = Math.max(0.22, Math.min(1, 1.6 / (0.35 + hit.dist))); + + let clr = WALL_PALETTE[hit.wallTip] || WALL_PALETTE[1]; + let colr; + if (hit.side === 1) colr = clr.edge; // EW yüzü biraz koyu (kenar detayı) + else colr = clr.base; // NS yüzü ana renk + + // RGB koyulaştır (z-gölgelendirme) + const rgb = hexToRgb(colr); + const r = Math.round(rgb.r * shade); + const g = Math.round(rgb.g * shade); + const b = Math.round(rgb.b * shade); + ctx.fillStyle = `rgb(${r},${g},${b})`; + + const x = col * RAY_W; + ctx.fillRect(x, topY, RAY_W, drawH); + ctx.fillStyle = 'rgba(0,0,0,0.55)'; + if (col % 4 === 0) { + ctx.fillRect(x, topY, RAY_W, 1); // üst kontur + ctx.fillRect(x, topY + drawH - 1, RAY_W, 1); // alt kontur + } + } + + // ---- Sprite'lar (duvar dışı dekor) ---- + renderSprites(ctx, w, h, halfH, zBuffer); +} + +/* Dikey degrade dolgusu (tavan/zemin dilimleri). */ +function drawVerticalGradient(ctx, w, yTop, height, topRGB, botRGB) { + if (height <= 0) return; + const g = ctx.createLinearGradient(0, yTop, 0, yTop + height); + g.addColorStop(0, topRGB); + g.addColorStop(1, botRGB); + ctx.fillStyle = g; + ctx.fillRect(0, yTop, w, height); +} + +/* #rrggbb -> {r,g,b} */ +function hexToRgb(hex) { + const m = hex.replace('#', ''); + return { + r: parseInt(m.substring(0, 2), 16), + g: parseInt(m.substring(2, 4), 16), + b: parseInt(m.substring(4, 6), 16) + }; +} + +/* ===================================================================== + * SPRITE RENDER (duvar dışı dekor objeleri) + * Harita hücre merkezlerine konur; z-buffer ile doğru sırada çizilir. + * --------------------------------------------------------------------- + * Her sprite: { x, y (harita birimi), color, size (birim, ~0.35) } + * ===================================================================== */ +const SPRITES = [ + { x: 7.5, y: 3.2, color: '#ffd23c', size: 0.30 }, // sarı kristal + { x: 12.2, y: 5.4, color: '#ff7ac8', size: 0.30 }, // pembe kristal + { x: 3.4, y: 12.6,color: '#9be14d', size: 0.34 }, // yeşil küp (koza) + { x: 10.4, y: 11.1,color: '#7fd7ff', size: 0.30 }, // buz kristali +]; + +/* Sprite'ları perspektife göre (en uzaktan en yakına) projekte edip + * z-buffer (duvar derinliği) karşılaştırmasıyla çizer. + * - Kamera düzlemi: FOV genişliğindeki sanal projeksiyon. + * - Her sprite duvarın ARKASINDA kalırsa görünmez → gerçekçi. */ +function renderSprites(ctx, w, h, halfH, zBuffer) { + const numCols = zBuffer.length; + const px = player.x, py = player.y, pa = player.angle; + + // Uzaklığa göre sırala (en uzak önce çizilir) + const ordered = SPRITES + .map((s) => ({ s, d: Math.hypot(s.x - px, s.y - py) })) + .sort((a, b) => b.d - a.d); + + for (const { s, d } of ordered) { + // Oyuncuya göre vektör ve açı + const rx = s.x - px, ry = s.y - py; + let relAng = Math.atan2(ry, rx) - pa; + // radyan normalize: -π..π + while (relAng > Math.PI) relAng -= Math.PI * 2; + while (relAng < -Math.PI) relAng += Math.PI * 2; + + // FOV dışına taşan sprite'ı atla + if (Math.abs(relAng) > FOV / 2 + 0.12) continue; + + // Nokta sprite'ın ekran-X'i: [-1..1] haritasına çevir + const xN = Math.tan(relAng) / Math.tan(FOV / 2); // -1..1 + const screenX = (xN * 0.5 + 0.5) * w; + const col = Math.round(screenX / RAY_W); + if (col < 0 || col >= numCols) continue; + // z-test: sprite duvardan UZAKTAYSA (d > wallDist) çizme + const wallDist = zBuffer[col]; + if (d > wallDist) continue; + + // Perspektif boyut (dikleştirilmiş mesafe kullan) + const inv = 1 / Math.max(0.1, d * Math.cos(relAng)); + const scale = h * Math.tan(FOV / 2) * inv; // sprite yüksekliği = scale piksel + const pxH = Math.round(s.size * scale * 2); + if (pxH < 2) continue; + + const pxW = pxH; // kare sprite + const top = halfH - pxH * 0.95; // yere oturur gibi + const left = screenX - pxW / 2; + + // uzaklık gölgelendirme (duvarlarla uyumlu) + const shade = Math.max(0.3, Math.min(1, 1.6 / (0.4 + d))); + const rgb = hexToRgb(s.color); + ctx.fillStyle = `rgb(${Math.round(rgb.r * shade)},${Math.round(rgb.g * shade)},${Math.round(rgb.b * shade)})`; + ctx.fillRect(left, top, pxW, pxH); + + // Geniş yüzeye parlak çekirdek (retro kristal hissi) + const inner = Math.round(pxW * 0.45); + ctx.fillStyle = `rgba(255,255,255,${0.35 * shade})`; + ctx.fillRect(left + (pxW - inner) / 2, top + (pxH - inner) / 2, inner, inner); + } +}