diff --git a/FAZA_1_CHECKLIST.md b/FAZA_1_CHECKLIST.md new file mode 100644 index 0000000..7d05510 --- /dev/null +++ b/FAZA_1_CHECKLIST.md @@ -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 diff --git a/README.md b/README.md index 88d456c..37ad515 100644 --- a/README.md +++ b/README.md @@ -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 --- diff --git a/index.html b/index.html index 24e0340..5431a01 100644 --- a/index.html +++ b/index.html @@ -1,5 +1,6 @@ + @@ -10,13 +11,13 @@ padding: 0; box-sizing: border-box; } - + body { background: #000; overflow: hidden; font-family: 'Courier New', monospace; } - + #game-container { display: flex; justify-content: center; @@ -26,16 +27,25 @@ } +
- + - + + + + + + + + - + + \ No newline at end of file diff --git a/src/scenes/GameScene.js b/src/scenes/GameScene.js index 3ff0fcb..bb6de86 100644 --- a/src/scenes/GameScene.js +++ b/src/scenes/GameScene.js @@ -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})` + ); + } } } diff --git a/src/systems/TerrainSystem.js b/src/systems/TerrainSystem.js new file mode 100644 index 0000000..a462563 --- /dev/null +++ b/src/systems/TerrainSystem.js @@ -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); + } +} diff --git a/src/utils/IsometricUtils.js b/src/utils/IsometricUtils.js new file mode 100644 index 0000000..bf185a0 --- /dev/null +++ b/src/utils/IsometricUtils.js @@ -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; + } +} diff --git a/src/utils/PerlinNoise.js b/src/utils/PerlinNoise.js new file mode 100644 index 0000000..f036741 --- /dev/null +++ b/src/utils/PerlinNoise.js @@ -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] + } +}