FAZA 1: Terrain generation with Perlin noise and isometric view - Ready for testing

This commit is contained in:
2025-12-06 17:54:45 +01:00
parent 7e20dff962
commit d61df381cb
7 changed files with 630 additions and 20 deletions

View File

@@ -2,6 +2,8 @@
class GameScene extends Phaser.Scene {
constructor() {
super({ key: 'GameScene' });
this.terrainSystem = null;
this.terrainContainer = null;
}
create() {
@@ -11,23 +13,31 @@ class GameScene extends Phaser.Scene {
const width = this.cameras.main.width;
const height = this.cameras.main.height;
// Testno besedilo - potrditev da scena deluje
const testText = this.add.text(width / 2, height / 2, 'FAZA 0: Setup Complete!\n\nGame Scene Active', {
fontFamily: 'Courier New',
fontSize: '32px',
fill: '#00ff41',
align: 'center'
});
testText.setOrigin(0.5);
// Setup kamere
this.cameras.main.setBackgroundColor('#1a1a2e');
// Inicializiraj terrain sistem - 100x100 mapa
console.log('🌍 Initializing terrain...');
this.terrainSystem = new TerrainSystem(this, 100, 100);
this.terrainSystem.generate();
this.terrainContainer = this.terrainSystem.render(width / 2, 100);
// Kamera kontrole
this.setupCamera();
// UI elementi
this.createUI();
// Debug info
const debugText = this.add.text(10, 10, 'FAZA 0 TEST\nElectron + Phaser OK', {
this.debugText = this.add.text(10, 10, '', {
fontFamily: 'Courier New',
fontSize: '14px',
fontSize: '12px',
fill: '#ffffff',
backgroundColor: '#000000',
padding: { x: 10, y: 5 }
padding: { x: 5, y: 3 }
});
this.debugText.setScrollFactor(0);
this.debugText.setDepth(1000);
// FPS counter
this.fpsText = this.add.text(10, height - 30, 'FPS: 60', {
@@ -35,14 +45,125 @@ class GameScene extends Phaser.Scene {
fontSize: '14px',
fill: '#00ff41'
});
this.fpsText.setScrollFactor(0);
this.fpsText.setDepth(1000);
console.log('✅ Faza 0 setup complete - ready for manual testing!');
console.log('✅ GameScene ready - FAZA 1!');
}
update() {
setupCamera() {
const cam = this.cameras.main;
// Zoom kontrole (Mouse Wheel)
this.input.on('wheel', (pointer, gameObjects, deltaX, deltaY, deltaZ) => {
const zoomSpeed = 0.001;
const newZoom = Phaser.Math.Clamp(
cam.zoom - deltaY * zoomSpeed,
0.3,
2.0
);
cam.setZoom(newZoom);
});
// Pan kontrole (Right click + drag)
this.input.on('pointermove', (pointer) => {
if (pointer.rightButtonDown()) {
cam.scrollX -= (pointer.x - pointer.prevPosition.x) / cam.zoom;
cam.scrollY -= (pointer.y - pointer.prevPosition.y) / cam.zoom;
}
});
// WASD za kamera kontrolo (alternativa)
this.cursors = this.input.keyboard.addKeys({
up: Phaser.Input.Keyboard.KeyCodes.W,
down: Phaser.Input.Keyboard.KeyCodes.S,
left: Phaser.Input.Keyboard.KeyCodes.A,
right: Phaser.Input.Keyboard.KeyCodes.D,
zoomIn: Phaser.Input.Keyboard.KeyCodes.Q,
zoomOut: Phaser.Input.Keyboard.KeyCodes.E
});
}
createUI() {
const width = this.cameras.main.width;
// Naslov
const title = this.add.text(width / 2, 20, 'FAZA 1: Generacija Terena', {
fontFamily: 'Courier New',
fontSize: '20px',
fill: '#00ff41',
fontStyle: 'bold'
});
title.setOrigin(0.5, 0);
title.setScrollFactor(0);
title.setDepth(1000);
// Kontrole info
const controlsText = this.add.text(width - 10, 10,
'Kontrole:\n' +
'WASD - Pan\n' +
'Q/E - Zoom\n' +
'Mouse Wheel - Zoom\n' +
'Right Click - Pan',
{
fontFamily: 'Courier New',
fontSize: '11px',
fill: '#888888',
backgroundColor: '#000000',
padding: { x: 5, y: 3 },
align: 'right'
}
);
controlsText.setOrigin(1, 0);
controlsText.setScrollFactor(0);
controlsText.setDepth(1000);
}
update(time, delta) {
// Update FPS
if (this.fpsText) {
this.fpsText.setText(`FPS: ${Math.round(this.game.loop.actualFps)}`);
}
// Kamera movement (WASD)
const cam = this.cameras.main;
const panSpeed = 5;
if (this.cursors) {
if (this.cursors.up.isDown) {
cam.scrollY -= panSpeed;
}
if (this.cursors.down.isDown) {
cam.scrollY += panSpeed;
}
if (this.cursors.left.isDown) {
cam.scrollX -= panSpeed;
}
if (this.cursors.right.isDown) {
cam.scrollX += panSpeed;
}
// Zoom
if (this.cursors.zoomIn.isDown) {
cam.setZoom(Phaser.Math.Clamp(cam.zoom + 0.01, 0.3, 2.0));
}
if (this.cursors.zoomOut.isDown) {
cam.setZoom(Phaser.Math.Clamp(cam.zoom - 0.01, 0.3, 2.0));
}
}
// Debug info update
if (this.debugText) {
const pointer = this.input.activePointer;
const worldX = Math.round(pointer.worldX);
const worldY = Math.round(pointer.worldY);
this.debugText.setText(
`FAZA 1 - Terrain System\n` +
`Zoom: ${cam.zoom.toFixed(2)}\n` +
`Camera: (${Math.round(cam.scrollX)}, ${Math.round(cam.scrollY)})\n` +
`Mouse: (${worldX}, ${worldY})`
);
}
}
}

View File

@@ -0,0 +1,126 @@
// Terrain Generator System
// Generira proceduralni isometrični teren
class TerrainSystem {
constructor(scene, width = 100, height = 100) {
this.scene = scene;
this.width = width;
this.height = height;
this.iso = new IsometricUtils(48, 24);
this.noise = new PerlinNoise(Date.now());
this.tiles = [];
this.tileSprites = [];
// Tipi terena z threshold vrednostmi
this.terrainTypes = {
WATER: { threshold: 0.3, color: 0x2166aa, name: 'water' },
SAND: { threshold: 0.4, color: 0xf4e7c6, name: 'sand' },
GRASS: { threshold: 0.65, color: 0x5cb85c, name: 'grass' },
DIRT: { threshold: 0.75, color: 0x8b6f47, name: 'dirt' },
STONE: { threshold: 1.0, color: 0x7d7d7d, name: 'stone' }
};
}
// Generiraj teren
generate() {
console.log(`🌍 Generating terrain: ${this.width}x${this.height}...`);
// Generiraj tile podatke
for (let y = 0; y < this.height; y++) {
this.tiles[y] = [];
for (let x = 0; x < this.width; x++) {
const noiseValue = this.noise.getNormalized(x, y, 0.05, 4);
const terrainType = this.getTerrainType(noiseValue);
this.tiles[y][x] = {
gridX: x,
gridY: y,
type: terrainType.name,
color: terrainType.color,
height: noiseValue
};
}
}
console.log('✅ Terrain data generated!');
}
// Določi tip terena glede na noise vrednost
getTerrainType(value) {
for (const type of Object.values(this.terrainTypes)) {
if (value < type.threshold) {
return type;
}
}
return this.terrainTypes.STONE;
}
// Renderaj teren (visual sprites)
render(offsetX = 0, offsetY = 300) {
console.log('🎨 Rendering terrain sprites...');
const container = this.scene.add.container(offsetX, offsetY);
// Renderaj vse tile-e
for (let y = 0; y < this.height; y++) {
for (let x = 0; x < this.width; x++) {
const tile = this.tiles[y][x];
const screenPos = this.iso.toScreen(x, y);
// Kreira diamond (romb) obliko za isometric tile
const graphics = this.scene.add.graphics();
// Osnovna barva
const baseColor = tile.color;
graphics.fillStyle(baseColor, 1);
// Nariši isometric tile (diamond shape)
const tileWidth = this.iso.tileWidth;
const tileHeight = this.iso.tileHeight;
graphics.beginPath();
graphics.moveTo(screenPos.x, screenPos.y); // Top
graphics.lineTo(screenPos.x + tileWidth / 2, screenPos.y + tileHeight / 2); // Right
graphics.lineTo(screenPos.x, screenPos.y + tileHeight); // Bottom
graphics.lineTo(screenPos.x - tileWidth / 2, screenPos.y + tileHeight / 2); // Left
graphics.closePath();
graphics.fillPath();
// Outline za boljšo vidljivost
graphics.lineStyle(1, 0x000000, 0.2);
graphics.strokePath();
// Dodaj v container
container.add(graphics);
// Shrani referenco
this.tileSprites.push({
graphics: graphics,
tile: tile,
depth: this.iso.getDepth(x, y)
});
}
}
// Sortiraj po depth
container.setDepth(0);
console.log(`✅ Rendered ${this.tileSprites.length} tiles!`);
return container;
}
// Pridobi tile na določenih grid koordinatah
getTile(gridX, gridY) {
if (gridX >= 0 && gridX < this.width && gridY >= 0 && gridY < this.height) {
return this.tiles[gridY][gridX];
}
return null;
}
// Screen koordinate -> tile
getTileAtScreen(screenX, screenY) {
const grid = this.iso.toGrid(screenX, screenY);
return this.getTile(grid.x, grid.y);
}
}

View File

@@ -0,0 +1,62 @@
// Isometric Utilities
// Konverzija med kartezičnimi in izometričnimi koordinatami
class IsometricUtils {
constructor(tileWidth = 48, tileHeight = 24) {
this.tileWidth = tileWidth;
this.tileHeight = tileHeight;
}
// Kartezične (grid) koordinate -> Isometrične (screen) koordinate
toScreen(gridX, gridY) {
const screenX = (gridX - gridY) * (this.tileWidth / 2);
const screenY = (gridX + gridY) * (this.tileHeight / 2);
return { x: screenX, y: screenY };
}
// Isometrične (screen) koordinate -> Kartezične (grid) koordinate
toGrid(screenX, screenY) {
const gridX = (screenX / (this.tileWidth / 2) + screenY / (this.tileHeight / 2)) / 2;
const gridY = (screenY / (this.tileHeight / 2) - screenX / (this.tileWidth / 2)) / 2;
return { x: Math.floor(gridX), y: Math.floor(gridY) };
}
// Izračun depth (z-index) za pravilno sortiranje
getDepth(gridX, gridY) {
return gridX + gridY;
}
// Izračun centerja tile-a
getTileCenter(gridX, gridY) {
const screen = this.toScreen(gridX, gridY);
return {
x: screen.x,
y: screen.y + this.tileHeight / 2
};
}
// Preveri ali je grid koordinata znotraj meja
isInBounds(gridX, gridY, mapWidth, mapHeight) {
return gridX >= 0 && gridX < mapWidth && gridY >= 0 && gridY < mapHeight;
}
// Dobi sosednje tile-e (NSEW)
getNeighbors(gridX, gridY, mapWidth, mapHeight) {
const neighbors = [];
const directions = [
{ x: 0, y: -1 }, // North
{ x: 1, y: 0 }, // East
{ x: 0, y: 1 }, // South
{ x: -1, y: 0 } // West
];
for (const dir of directions) {
const nx = gridX + dir.x;
const ny = gridY + dir.y;
if (this.isInBounds(nx, ny, mapWidth, mapHeight)) {
neighbors.push({ x: nx, y: ny });
}
}
return neighbors;
}
}

88
src/utils/PerlinNoise.js Normal file
View File

@@ -0,0 +1,88 @@
// Perlin Noise Generator
// Implementacija za proceduralno generacijo terena
class PerlinNoise {
constructor(seed = Math.random()) {
this.seed = seed;
this.permutation = this.generatePermutation();
}
generatePermutation() {
const p = [];
for (let i = 0; i < 256; i++) {
p[i] = i;
}
// Fisher-Yates shuffle z seed-om
let random = this.seed;
for (let i = 255; i > 0; i--) {
random = (random * 9301 + 49297) % 233280;
const j = Math.floor((random / 233280) * (i + 1));
[p[i], p[j]] = [p[j], p[i]];
}
// Podvoji permutacijo
return [...p, ...p];
}
fade(t) {
return t * t * t * (t * (t * 6 - 15) + 10);
}
lerp(t, a, b) {
return a + t * (b - a);
}
grad(hash, x, y) {
const h = hash & 3;
const u = h < 2 ? x : y;
const v = h < 2 ? y : x;
return ((h & 1) === 0 ? u : -u) + ((h & 2) === 0 ? v : -v);
}
noise(x, y) {
const X = Math.floor(x) & 255;
const Y = Math.floor(y) & 255;
x -= Math.floor(x);
y -= Math.floor(y);
const u = this.fade(x);
const v = this.fade(y);
const p = this.permutation;
const a = p[X] + Y;
const aa = p[a];
const ab = p[a + 1];
const b = p[X + 1] + Y;
const ba = p[b];
const bb = p[b + 1];
return this.lerp(v,
this.lerp(u, this.grad(p[aa], x, y), this.grad(p[ba], x - 1, y)),
this.lerp(u, this.grad(p[ab], x, y - 1), this.grad(p[bb], x - 1, y - 1))
);
}
// Octave noise za bolj kompleksne terene
octaveNoise(x, y, octaves = 4, persistence = 0.5) {
let total = 0;
let frequency = 1;
let amplitude = 1;
let maxValue = 0;
for (let i = 0; i < octaves; i++) {
total += this.noise(x * frequency, y * frequency) * amplitude;
maxValue += amplitude;
amplitude *= persistence;
frequency *= 2;
}
return total / maxValue;
}
// Generira normalizirano vrednost med 0 in 1
getNormalized(x, y, scale = 0.1, octaves = 4) {
const value = this.octaveNoise(x * scale, y * scale, octaves);
return (value + 1) / 2; // Normalizacija iz [-1, 1] v [0, 1]
}
}