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

197
FAZA_1_CHECKLIST.md Normal file
View File

@@ -0,0 +1,197 @@
# FAZA 1: Generacija Terena - Checklist
**Status:** ✅ PRIPRAVLJEN ZA TESTIRANJE
**Datum:** 2025-12-06
---
## ✅ Opravila (Developer)
- [x] Implementacija Perlin Noise generatorja
- [x] Kreacija IsometricUtils (konverzija koordinat)
- [x] Implementacija TerrainSystem
- [x] Definicija 5 tipov terena (voda, pesek, trava, zemlja, kamen)
- [x] Generacija 100x100 mape
- [x] Renderanje isometričnih tile-ov (diamond shapes)
- [x] Kamera kontrole (WASD + mouse)
- [x] Zoom funkcionalnost (Q/E + mouse wheel)
- [x] Debug UI (koordinate, zoom, FPS)
- [x] Posodobitev index.html z novimi skriptami
**VSE OPRAVILA ZAKLJUČENA**
---
## 🧪 Ročno testiranje (Naročnik)
### Test 1: Generacija Terena
**Ukaz:** `npm start` → pritisni SPACE v menu
**Pričakovani rezultat:**
- [ ] Teren se generira (100x100 tiles)
- [ ] Vidnih je 5 različnih tipov terena:
- [ ] Voda (modra #2166aa)
- [ ] Pesek (bež #f4e7c6)
- [ ] Trava (zelena #5cb85c)
- [ ] Zemlja (rjava #8b6f47)
- [ ] Kamen (siva #7d7d7d)
- [ ] Tile-i so v isometrični diamond obliki
- [ ] Teren izgleda naraven (Perlin noise deluje)
**Status:** ⏳ ČAKA NA TESTIRANJE
---
### Test 2: Isometrični Pogled
**Pričakovani rezultat:**
- [ ] Mapa je v 2.5D isometričnem pogledu
- [ ] Tile-i so pravilno poravnani (diamond grid)
- [ ] Depth sorting pravilen (zadnji tile-i so vidni pred sprednjimi)
- [ ] Nobenih prekrivanj ali lukenj v mapi
**Status:** ⏳ ČAKA NA TESTIRANJE
---
### Test 3: Kamera - WASD
**Ukazi:** W (gor), A (levo), S (dol), D (desno)
**Pričakovani rezultat:**
- [ ] W - kamera se premakne navzgor
- [ ] S - kamera se premakne navzdol
- [ ] A - kamera se premakne levo
- [ ] D - kamera se premakne desno
- [ ] Smooth gibanje (brez lagganja)
**Status:** ⏳ ČAKA NA TESTIRANJE
---
### Test 4: Kamera - Mouse
**Ukazi:**
- Right click + drag = pan
- Mouse wheel = zoom
**Pričakovani rezultat:**
- [ ] Right click + drag premika kamero
- [ ] Mouse wheel scroll gor = zoom out
- [ ] Mouse wheel scroll dol = zoom in
- [ ] Zoom range: 0.3x - 2.0x
**Status:** ⏳ ČAKA NA TESTIRANJE
---
### Test 5: Zoom - Tipkovnica
**Ukazi:** Q (zoom in), E (zoom out)
**Pričakovani rezultat:**
- [ ] Q povečuje zoom
- [ ] E zmanjšuje zoom
- [ ] Smooth zoom animacija
- [ ] Zoom je omejen (min 0.3, max 2.0)
**Status:** ⏳ ČAKA NA TESTIRANJE
---
### Test 6: UI in Debug Info
**Pričakovani rezultat:**
- [ ] Naslov: "FAZA 1: Generacija Terena" (zgoraj, zelena barva)
- [ ] Kontrole info (zgoraj desno)
- [ ] Debug info (zgoraj levo):
- [ ] Zoom vrednost prikazana
- [ ] Kamera koordinate
- [ ] Mouse koordinate
- [ ] FPS counter (spodaj levo) ~ 60 FPS
**Status:** ⏳ ČAKA NA TESTIRANJE
---
### Test 7: Performance
**Pričakovani rezultat:**
- [ ] FPS: 55-60 (stabilen) pri počitku
- [ ] FPS: 50+ pri premikanju kamere
- [ ] Brez stutteringa pri zoom-u
- [ ] Teren se generira v < 2 sekundi
- [ ] Smooth renderanje vseh 10,000 tile-ov
**Status:** ⏳ ČAKA NA TESTIRANJE
---
### Test 8: Vizualna Kvaliteta
**Pričakovani rezultat:**
- [ ] Teren izgleda naraven (ne random)
- [ ] Tekoči prehodi med tipi terena
- [ ] Črne outline črte vidne med tile-i
- [ ] Barve so razločne in lepe
- [ ] Brez graphical glitch-ov
**Status:** ⏳ ČAKA NA TESTIRANJE
---
## 📋 Potrditev Naročnika
```
FAZA 1: [STATUS]
- Testirano: [DA/NE]
- Datum testiranja: ___________
- Opombe:
- Test 1: [✅/❌]
- Test 2: [✅/❌]
- Test 3: [✅/❌]
- Test 4: [✅/❌]
- Test 5: [✅/❌]
- Test 6: [✅/❌]
- Test 7: [✅/❌]
- Test 8: [✅/❌]
ODOBRENO ZA FAZO 2: [DA/NE]
Podpis naročnika: _____________
```
---
## 🚨 V primeru težav
### Težava: Teren se ne generira / črn zaslon
**Rešitev:**
- Preveri konzolo za error-je (F12)
- Preveri da so vse skripte v index.html pravilno vključene
- Reload: Ctrl+R
### Težava: FPS prenizek (<40)
**Rešitev:**
- To je normalno za 100x100 mapo (10,000 tile-ov)
- Če je FPS < 30, preveri TaskManager za CPU/GPU usage
### Težava: Kamera se ne premika
**Rešitev:**
- Poskusi mouse right-click + drag
- Preveri da je okno v fokusu
### Težava: Teren izgleda preveč random (ne naraven)
**Rešitev:**
- To je normalno - Perlin noise lahko ustvari različne pattern-e
- Za testiranje samo preveri da je 5 različnih barv vidnih
---
## ➡️ Naslednji koraki (po odobritvi)
Ko naročnik potrdi FAZO 1, se začne:
**FAZA 2: Igralec in Gibanje**
- Player sprite (32x32px pixel art)
- WASD gibanje igralca (ne kamere!)
- Depth sorting za igralca
- Kolizija z robovi mape
- Barvne sheme za igralca

View File

@@ -56,13 +56,19 @@ novafarma/
## 🎮 Trenutni Status
**FAZA 0: ✅ COMPLETE**
**FAZA 0: ✅ APPROVED** (2025-12-06)
- Setup projekta
- Git inicializacija
- Electron + Phaser integracija
- Osnovne scene (Boot, Preload, Game)
**Naslednja faza:** FAZA 1 - Generacija Terena
**FAZA 1: ✅ COMPLETE - Čaka na testiranje**
- Perlin Noise terrain generator
- 100x100 isometrična mapa
- 5 tipov terena (voda, pesek, trava, zemlja, kamen)
- Kamera kontrole (WASD, mouse pan, zoom)
**Naslednja faza:** FAZA 2 - Igralec in Gibanje
---

View File

@@ -1,5 +1,6 @@
<!DOCTYPE html>
<html lang="sl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -26,16 +27,25 @@
}
</style>
</head>
<body>
<div id="game-container"></div>
<!-- Phaser 3 -->
<script src="node_modules/phaser/dist/phaser.js"></script>
<!-- Utilities -->
<script src="src/utils/PerlinNoise.js"></script>
<script src="src/utils/IsometricUtils.js"></script>
<!-- Systems -->
<script src="src/systems/TerrainSystem.js"></script>
<!-- Game Files -->
<script src="src/scenes/BootScene.js"></script>
<script src="src/scenes/PreloadScene.js"></script>
<script src="src/scenes/GameScene.js"></script>
<script src="src/game.js"></script>
</body>
</html>

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]
}
}