Numex yayın: public/engine.js
This commit is contained in:
parent
84ab0bd227
commit
01023201ce
262
public/engine.js
Normal file
262
public/engine.js
Normal 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);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user