// Terrain Generator System // Generira proceduralni isometrični teren in skrbi za optimizacijo (Tilemap + Culling) 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.decorations = []; this.decorationsMap = new Map(); this.cropsMap = new Map(); this.visibleDecorations = new Map(); this.visibleCrops = new Map(); // Pool for Decorations (Trees, Rocks, etc.) this.decorationPool = { active: [], inactive: [], get: () => { if (this.decorationPool.inactive.length > 0) { const s = this.decorationPool.inactive.pop(); s.setVisible(true); s.clearTint(); return s; } return this.scene.add.sprite(0, 0, 'tree'); }, release: (sprite) => { sprite.setVisible(false); this.decorationPool.inactive.push(sprite); } }; // Pool for Crops this.cropPool = { active: [], inactive: [], get: () => { if (this.cropPool.inactive.length > 0) { const s = this.cropPool.inactive.pop(); s.setVisible(true); return s; } return this.scene.add.sprite(0, 0, 'crop_stage_1'); }, release: (sprite) => { sprite.setVisible(false); this.cropPool.inactive.push(sprite); } }; this.terrainTypes = { WATER: { name: 'water', height: 0, color: 0x4444ff, index: 0 }, SAND: { name: 'sand', height: 0.2, color: 0xdddd44, index: 1 }, GRASS_FULL: { name: 'grass_full', height: 0.35, color: 0x44aa44, index: 2 }, GRASS_TOP: { name: 'grass_top', height: 0.45, color: 0x66cc66, index: 3 }, DIRT: { name: 'dirt', height: 0.5, color: 0x8b4513, index: 4 }, STONE: { name: 'stone', height: 0.7, color: 0x888888, index: 5 }, PATH: { name: 'path', height: -1, color: 0xc2b280, index: 6 }, FARMLAND: { name: 'farmland', height: -1, color: 0x5c4033, index: 7 } }; this.offsetX = 0; this.offsetY = 0; } createTileTextures() { // Create a single spritesheet for tiles (Tilemap Optimization) const graphics = this.scene.make.graphics({ x: 0, y: 0, add: false }); const tileWidth = 48; const tileHeight = 32; // 24 for iso + 8 depth const types = Object.values(this.terrainTypes); // Draw all tiles horizontally types.forEach((type, index) => { // Update index just in case type.index = index; const x = index * tileWidth; graphics.fillStyle(type.color); // Draw Isometic Tile (Diamond + Thickness) const top = 0; const midX = x + 24; const midY = 12; const bottomY = 24; const depth = 8; // Top Face graphics.beginPath(); graphics.moveTo(midX, top); graphics.lineTo(x + 48, midY); graphics.lineTo(midX, bottomY); graphics.lineTo(x, midY); graphics.closePath(); graphics.fill(); // Add stroke to prevent seams/gaps (Robust Fix) graphics.lineStyle(2, type.color); graphics.strokePath(); // Thickness (Right) graphics.fillStyle(Phaser.Display.Color.IntegerToColor(type.color).darken(20).color); graphics.beginPath(); graphics.moveTo(x + 48, midY); graphics.lineTo(x + 48, midY + depth); graphics.lineTo(midX, bottomY + depth); graphics.lineTo(midX, bottomY); graphics.closePath(); graphics.fill(); // Thickness (Left) graphics.fillStyle(Phaser.Display.Color.IntegerToColor(type.color).darken(40).color); graphics.beginPath(); graphics.moveTo(midX, bottomY); graphics.lineTo(midX, bottomY + depth); graphics.lineTo(x, midY + depth); graphics.lineTo(x, midY); graphics.closePath(); graphics.fill(); // Detail (Grass) if (type.name.includes('grass')) { graphics.fillStyle(0x339933); // Darker green blades for (let i = 0; i < 15; i++) { const rx = x + 8 + Math.random() * 32; const ry = 4 + Math.random() * 16; graphics.fillRect(rx, ry, 2, 2); } } // Detail (Dirt) if (type.name.includes('dirt')) { graphics.fillStyle(0x5c4033); for (let i = 0; i < 8; i++) { const rx = x + 8 + Math.random() * 32; const ry = 4 + Math.random() * 16; graphics.fillRect(rx, ry, 2, 2); } } }); graphics.generateTexture('terrain_tileset', tileWidth * types.length, tileHeight); graphics.destroy(); } generate() { this.createTileTextures(); for (let y = 0; y < this.height; y++) { this.tiles[y] = []; for (let x = 0; x < this.width; x++) { const nx = x * 0.1; const ny = y * 0.1; const elevation = this.noise.noise(nx, ny); let terrainType = this.terrainTypes.WATER; if (elevation > this.terrainTypes.SAND.height) terrainType = this.terrainTypes.SAND; if (elevation > this.terrainTypes.GRASS_FULL.height) terrainType = this.terrainTypes.GRASS_FULL; if (elevation > this.terrainTypes.DIRT.height) terrainType = this.terrainTypes.DIRT; if (elevation > this.terrainTypes.STONE.height) terrainType = this.terrainTypes.STONE; this.tiles[y][x] = { type: terrainType.name, texture: terrainType.name, hasDecoration: false, hasCrop: false }; // Vegetation logic (Rich World) if (x > 5 && x < this.width - 5 && y > 5 && y < this.height - 5) { let decorType = null; let maxHp = 1; let scale = 1.0; if (terrainType.name.includes('grass')) { const rand = Math.random(); if (elevation > 0.6 && rand < 0.1) { decorType = 'bush'; maxHp = 5; } else if (rand < 0.15) { // Common trees decorType = 'tree'; maxHp = 5; const sizeRand = Math.random(); if (sizeRand < 0.2) scale = 0.8; else if (sizeRand < 0.8) scale = 1.0 + Math.random() * 0.3; else scale = 1.3; } else if (rand < 0.18) { // Rocks decorType = 'rock'; maxHp = 8; scale = 1.2 + Math.random() * 0.5; } else if (rand < 0.19) { decorType = 'gravestone'; maxHp = 10; } else if (rand < 0.30) { decorType = 'flower'; maxHp = 1; } } else if (terrainType.name === 'dirt' && Math.random() < 0.05) { decorType = 'bush'; maxHp = 3; } if (decorType) { const key = `${x},${y}`; const decorData = { gridX: x, gridY: y, type: decorType, id: key, maxHp: maxHp, hp: maxHp, scale: scale }; this.decorations.push(decorData); this.decorationsMap.set(key, decorData); this.tiles[y][x].hasDecoration = true; } } } } console.log('✅ Terrain and decorations generated!'); // --- TILEMAP IMPLEMENTATION (Performance) --- if (this.map) this.map.destroy(); this.map = this.scene.make.tilemap({ tileWidth: this.iso.tileWidth, // 48 tileHeight: this.iso.tileHeight, // 24 width: this.width, height: this.height, orientation: Phaser.Tilemaps.Orientation.ISOMETRIC }); // 48x32 tileset const tileset = this.map.addTilesetImage('terrain_tileset', 'terrain_tileset', 48, 32); this.layer = this.map.createBlankLayer('Ground', tileset, this.offsetX, this.offsetY); for (let y = 0; y < this.height; y++) { for (let x = 0; x < this.width; x++) { const t = this.tiles[y][x]; const typeDef = Object.values(this.terrainTypes).find(tt => tt.name === t.type); if (typeDef) { this.layer.putTileAt(typeDef.index, x, y); } } } this.layer.setDepth(0); // Ground level } damageDecoration(x, y, amount) { const key = `${x},${y}`; const decor = this.decorationsMap.get(key); if (!decor) return false; decor.hp -= amount; if (this.visibleDecorations.has(key)) { const sprite = this.visibleDecorations.get(key); sprite.setTint(0xff0000); this.scene.time.delayedCall(100, () => sprite.clearTint()); this.scene.tweens.add({ targets: sprite, x: sprite.x + 2, duration: 50, yoyo: true, repeat: 1 }); } if (decor.hp <= 0) { this.removeDecoration(x, y); return 'destroyed'; } return 'hit'; } removeDecoration(x, y) { const key = `${x},${y}`; const decor = this.decorationsMap.get(key); if (!decor) return; if (this.visibleDecorations.has(key)) { const sprite = this.visibleDecorations.get(key); sprite.setVisible(false); this.decorationPool.release(sprite); this.visibleDecorations.delete(key); } this.decorationsMap.delete(key); const index = this.decorations.indexOf(decor); if (index > -1) this.decorations.splice(index, 1); if (this.tiles[y] && this.tiles[y][x]) this.tiles[y][x].hasDecoration = false; return decor.type; } placeStructure(x, y, structureType) { if (this.decorationsMap.has(`${x},${y}`)) return false; const decorData = { gridX: x, gridY: y, type: structureType, id: `${x},${y}`, maxHp: 5, hp: 5 }; this.decorations.push(decorData); this.decorationsMap.set(decorData.id, decorData); const tile = this.getTile(x, y); if (tile) tile.hasDecoration = true; this.lastCullX = -9999; return true; } setTileType(x, y, typeName) { if (!this.tiles[y] || !this.tiles[y][x]) return; const typeDef = Object.values(this.terrainTypes).find(t => t.name === typeName); if (!typeDef) return; this.tiles[y][x].type = typeName; // Tilemap update if (this.layer) { this.layer.putTileAt(typeDef.index, x, y); } } addCrop(x, y, cropData) { const key = `${x},${y}`; this.cropsMap.set(key, cropData); this.tiles[y][x].hasCrop = true; this.lastCullX = -9999; } removeCrop(x, y) { const key = `${x},${y}`; if (this.cropsMap.has(key)) { if (this.visibleCrops.has(key)) { const sprite = this.visibleCrops.get(key); sprite.setVisible(false); this.cropPool.release(sprite); this.visibleCrops.delete(key); } this.cropsMap.delete(key); this.tiles[y][x].hasCrop = false; } } updateCropVisual(x, y, stage) { const key = `${x},${y}`; if (this.visibleCrops.has(key)) { const sprite = this.visibleCrops.get(key); sprite.setTexture(`crop_stage_${stage}`); } } init(offsetX, offsetY) { this.offsetX = offsetX; this.offsetY = offsetY; } getTile(x, y) { if (this.tiles[y] && this.tiles[y][x]) { return this.tiles[y][x]; } return null; } updateCulling(camera) { // Culling for Decorations & Crops (Tiles controlled by Tilemap) const view = camera.worldView; let buffer = 200; if (this.scene.settings && this.scene.settings.viewDistance === 'LOW') buffer = 50; const left = view.x - buffer - this.offsetX; const top = view.y - buffer - this.offsetY; const right = view.x + view.width + buffer - this.offsetX; const bottom = view.y + view.height + buffer - this.offsetY; const p1 = this.iso.toGrid(left, top); const p2 = this.iso.toGrid(right, top); const p3 = this.iso.toGrid(left, bottom); const p4 = this.iso.toGrid(right, bottom); const minGridX = Math.floor(Math.min(p1.x, p2.x, p3.x, p4.x)); const maxGridX = Math.ceil(Math.max(p1.x, p2.x, p3.x, p4.x)); const minGridY = Math.floor(Math.min(p1.y, p2.y, p3.y, p4.y)); const maxGridY = Math.ceil(Math.max(p1.y, p2.y, p3.y, p4.y)); const startX = Math.max(0, minGridX); const endX = Math.min(this.width, maxGridX); const startY = Math.max(0, minGridY); const endY = Math.min(this.height, maxGridY); const neededDecorKeys = new Set(); const neededCropKeys = new Set(); for (let y = startY; y < endY; y++) { for (let x = startX; x < endX; x++) { const key = `${x},${y}`; // DECORATIONS const decor = this.decorationsMap.get(key); if (decor) { neededDecorKeys.add(key); if (!this.visibleDecorations.has(key)) { const sprite = this.decorationPool.get(); const screenPos = this.iso.toScreen(x, y); sprite.setPosition(screenPos.x + this.offsetX, screenPos.y + this.offsetY); // Origin adjusted for volumetric sprites // Trees/Rocks usually look best with origin (0.5, 0.9) to sit on the ground if (decor.type.includes('house') || decor.type.includes('market') || decor.type.includes('structure')) { sprite.setOrigin(0.5, 0.8); } else { sprite.setOrigin(0.5, 0.9); } // Texture & Scale sprite.setTexture(decor.type); sprite.setScale(decor.scale || 1.0); sprite.setDepth(this.iso.getDepth(x, y) + 1); this.visibleDecorations.set(key, sprite); } } // CROPS const crop = this.cropsMap.get(key); if (crop) { neededCropKeys.add(key); if (!this.visibleCrops.has(key)) { const sprite = this.cropPool.get(); const screenPos = this.iso.toScreen(x, y); sprite.setPosition(screenPos.x + this.offsetX, screenPos.y + this.offsetY); sprite.setTexture(`crop_stage_${crop.stage}`); // Crop origin sprite.setOrigin(0.5, 1); // Crop depth sprite.setDepth(this.iso.getDepth(x, y) + 0.5); this.visibleCrops.set(key, sprite); } } } } // Cleanup for (const [key, sprite] of this.visibleDecorations) { if (!neededDecorKeys.has(key)) { sprite.setVisible(false); this.decorationPool.release(sprite); this.visibleDecorations.delete(key); } } for (const [key, sprite] of this.visibleCrops) { if (!neededCropKeys.has(key)) { sprite.setVisible(false); this.cropPool.release(sprite); this.visibleCrops.delete(key); } } } }