raycaster-3d/public/engine.js

263 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* =====================================================================
* 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);
}
}