Numex yayın: public/engine.js

This commit is contained in:
numex-admin 2026-09-09 22:39:29 +00:00
parent 84ab0bd227
commit 01023201ce

262
public/engine.js Normal file
View File

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