// Terrain Generator System // Generira proceduralni isometrični teren in skrbi za optimizacijo (Culling, Object Pooling) 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 = []; // Array za save/load compat this.decorationsMap = new Map(); // Fast lookup key->decor this.cropsMap = new Map(); // Store dynamic crops separately // Render state monitoring this.visibleTiles = new Map(); // Key: "x,y", Value: Sprite this.visibleDecorations = new Map(); // Key: "x,y", Value: Sprite this.visibleCrops = new Map(); // Key: "x,y", Value: Sprite this.offsetX = 0; this.offsetY = 0; // Object Pools this.tilePool = new ObjectPool( () => { const sprite = this.scene.add.image(0, 0, 'tile_grass'); sprite.setOrigin(0.5, 0); // Isometrični tiles imajo origin zgoraj/center ali po potrebi return sprite; }, (sprite) => { sprite.setVisible(true); sprite.setAlpha(1); sprite.clearTint(); } ); this.decorationPool = new ObjectPool( () => { const sprite = this.scene.add.sprite(0, 0, 'flower'); sprite.setOrigin(0.5, 1); return sprite; }, (sprite) => { sprite.setVisible(true); sprite.setAlpha(1); sprite.clearTint(); // Reset damage tint } ); this.cropPool = new ObjectPool( () => { const sprite = this.scene.add.sprite(0, 0, 'crop_stage_1'); // Default texture logic needed sprite.setOrigin(0.5, 1); return sprite; }, (sprite) => { sprite.setVisible(true); sprite.setAlpha(1); } ); // Tipi terena z threshold vrednostmi + Y-LAYER STACKING this.terrainTypes = { WATER: { threshold: 0.3, color: 0x2166aa, name: 'water', texture: 'tile_water', yLayer: -1 }, SAND: { threshold: 0.4, color: 0xf4e7c6, name: 'sand', texture: 'tile_sand', yLayer: 0 }, // Y-LAYER GRASS VARIANTS (A, B, C systém) GRASS_FULL: { threshold: 0.50, color: 0x5cb85c, name: 'grass_full', texture: 'tile_grass_full', yLayer: 0 }, // A: Full grass GRASS_TOP: { threshold: 0.60, color: 0x5cb85c, name: 'grass_top', texture: 'tile_grass_top', yLayer: 1 }, // B: Grass top, dirt sides DIRT: { threshold: 0.75, color: 0x8b6f47, name: 'dirt', texture: 'tile_dirt', yLayer: 2 }, // C: Full dirt STONE: { threshold: 1.0, color: 0x7d7d7d, name: 'stone', texture: 'tile_stone', yLayer: 3 }, PATH: { threshold: 999, color: 0x9b7653, name: 'path', texture: 'tile_path', yLayer: 0 }, // Pot/Road FARMLAND: { threshold: 999, color: 0x4a3c2a, name: 'farmland', texture: 'tile_farmland', yLayer: 0 } }; } // Helper da dobi terrain type glede na elevation (Y-layer) getTerrainTypeByElevation(noiseValue, elevation) { // Osnovni terrain type iz noise let baseType = this.getTerrainType(noiseValue); // Če je grass, določi Y-layer variant glede na elevation if (baseType.name.includes('grass') || baseType === this.terrainTypes.GRASS_FULL || baseType === this.terrainTypes.GRASS_TOP) { if (elevation > 0.7) { return this.terrainTypes.GRASS_FULL; // A: Najvišja plast (full grass) } else if (elevation > 0.4) { return this.terrainTypes.GRASS_TOP; // B: Srednja (grass top, dirt sides) } else { return this.terrainTypes.DIRT; // C: Nizka (full dirt) } } return baseType; } // Generiraj teksture za tiles (da ne uporabljamo Počasnih Graphics objektov) createTileTextures() { console.log('🎨 Creating tile textures...'); for (const type of Object.values(this.terrainTypes)) { const key = `tile_${type.name}`; if (this.scene.textures.exists(key)) continue; // Check for custom grass tile if (type.name === 'grass' && this.scene.textures.exists('grass_tile')) { this.scene.textures.addImage(key, this.scene.textures.get('grass_tile').getSourceImage()); type.texture = key; continue; // Skip procedural generation } const tileW = this.iso.tileWidth; const tileH = this.iso.tileHeight; const thickness = 8; // Minimal thickness - nearly 2D const graphics = this.scene.make.graphics({ x: 0, y: 0, add: false }); // Helper for colors const baseColor = Phaser.Display.Color.IntegerToColor(type.color); let darkColor, darkerColor; // Y-LAYER STACKING: Different side colors based on layer if (type.name === 'grass_full') { // A: Full Grass Block - všechno zeleno darkColor = 0x5cb85c; // Green (lighter) darkerColor = 0x4a9d3f; // Green (darker) } else if (type.name === 'grass_top') { // B: Grass Top - zgoraj zeleno, stranice zemlja darkColor = 0x8b6f47; // Dirt color (brown) darkerColor = 0x5d4a2e; // Darker Dirt } else if (type.name === 'dirt') { // C: Full Dirt - vse rjavo darkColor = 0x8b6f47; // Dirt darkerColor = 0x654321; // Darker Dirt } else { // Standard block: Darken base color significantly darkColor = Phaser.Display.Color.IntegerToColor(type.color).darken(30).color; darkerColor = Phaser.Display.Color.IntegerToColor(type.color).darken(50).color; } // 1. Draw LEFT Side (Darker) - Minecraft volumetric effect graphics.fillStyle(darkColor, 1); graphics.beginPath(); graphics.moveTo(0, tileH / 2); // Left Corner graphics.lineTo(tileW / 2, tileH); // Bottom Corner graphics.lineTo(tileW / 2, tileH + thickness); // Bottom Corner (Lowered) graphics.lineTo(0, tileH / 2 + thickness); // Left Corner (Lowered) graphics.closePath(); graphics.fillPath(); // 2. Draw RIGHT Side (Darkest) - Strong shadow graphics.fillStyle(darkerColor, 1); graphics.beginPath(); graphics.moveTo(tileW / 2, tileH); // Bottom Corner graphics.lineTo(tileW, tileH / 2); // Right Corner graphics.lineTo(tileW, tileH / 2 + thickness); // Right Corner (Lowered) graphics.lineTo(tileW / 2, tileH + thickness); // Bottom Corner (Lowered) graphics.closePath(); graphics.fillPath(); // 3. Draw TOP Surface (bright) graphics.fillStyle(type.color, 1); graphics.beginPath(); graphics.moveTo(tileW / 2, 0); // Top graphics.lineTo(tileW, tileH / 2); // Right graphics.lineTo(tileW / 2, tileH); // Bottom graphics.lineTo(0, tileH / 2); // Left graphics.closePath(); graphics.fillPath(); // 4. Add Minecraft-style texture pattern on top if (type.name === 'grass_full' || type.name === 'grass_top') { // Grass texture: Random pixel pattern graphics.fillStyle(0x4a9d3f, 0.3); // Slightly darker green for (let i = 0; i < 8; i++) { const px = Math.random() * tileW; const py = Math.random() * tileH; graphics.fillRect(px, py, 2, 2); } } else if (type.name === 'dirt') { // Dirt texture: Darker spots graphics.fillStyle(0x6d5838, 0.4); for (let i = 0; i < 6; i++) { const px = Math.random() * tileW; const py = Math.random() * tileH; graphics.fillRect(px, py, 3, 3); } } else if (type.name === 'stone') { // Stone texture: Gray spots graphics.fillStyle(0x666666, 0.3); for (let i = 0; i < 10; i++) { const px = Math.random() * tileW; const py = Math.random() * tileH; graphics.fillRect(px, py, 2, 1); } } else if (type.name === 'path') { // Path texture: Small gravel stones graphics.fillStyle(0x7a5e42, 0.5); // Darker brown for (let i = 0; i < 12; i++) { const px = Math.random() * tileW; const py = Math.random() * tileH; graphics.fillRect(px, py, 2, 2); } } // 5. Crisp black outline for block definition graphics.lineStyle(1, 0x000000, 0.3); graphics.strokePath(); // Generate texture graphics.generateTexture(key, tileW, tileH + thickness); graphics.destroy(); // Update texture name in type def type.texture = key; } } createGravestoneSprite() { // Extract gravestone from objects_pack (approx position in atlas) // Gravestone appears to be around position row 4, column 4-5 in the pack const canvas = document.createElement('canvas'); const size = 32; // Approximate sprite size canvas.width = size; canvas.height = size; const ctx = canvas.getContext('2d', { willReadFrequently: true }); const sourceTexture = this.scene.textures.get('objects_pack'); const sourceImg = sourceTexture.getSourceImage(); // Extract gravestone (cross tombstone) - estimated coords // Adjust these values based on actual sprite sheet layout const sourceX = 240; // Approximate X position const sourceY = 160; // Approximate Y position ctx.drawImage(sourceImg, sourceX, sourceY, size, size, 0, 0, size, size); // Create texture this.scene.textures.addCanvas('gravestone', canvas); console.log('✅ Gravestone sprite extracted!'); } // Generiraj teren (data only) generate() { console.log(`🌍 Generating terrain data: ${this.width}x${this.height}...`); // Zagotovi teksture this.createTileTextures(); // Zagotovi decoration teksture - check for custom sprites first if (!this.scene.textures.exists('flower')) { TextureGenerator.createFlowerSprite(this.scene, 'flower'); } // Bush - use custom stone sprite if available if (this.scene.textures.exists('stone_sprite')) { // Use stone_sprite for bushes (rocks) if (!this.scene.textures.exists('bush')) { this.scene.textures.addImage('bush', this.scene.textures.get('stone_sprite').getSourceImage()); } } else if (!this.scene.textures.exists('bush')) { TextureGenerator.createBushSprite(this.scene, 'bush'); } // Tree - use custom tree sprite if available if (this.scene.textures.exists('tree_sprite')) { if (!this.scene.textures.exists('tree')) { this.scene.textures.addImage('tree', this.scene.textures.get('tree_sprite').getSourceImage()); } } else if (!this.scene.textures.exists('tree')) { TextureGenerator.createTreeSprite(this.scene, 'tree'); } // Gravestone - extract from objects_pack if (this.scene.textures.exists('objects_pack') && !this.scene.textures.exists('gravestone')) { this.createGravestoneSprite(); } // Crop textures for (let i = 1; i <= 4; i++) { if (!this.scene.textures.exists(`crop_stage_${i}`)) TextureGenerator.createCropSprite(this.scene, `crop_stage_${i}`, i); } this.decorationsMap.clear(); this.decorations = []; this.cropsMap.clear(); // 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); // Elevation (druga Perlin noise layer za hribe) let elevation = this.noise.getNormalized(x, y, 0.03, 3); // Get terrain type based on BOTH noise and elevation (Y-layer) let terrainType = this.getTerrainTypeByElevation(noiseValue, elevation); // === PATH GENERATION === // Use a separate noise layer for paths (higher frequency for winding roads) const pathNoise = this.noise.getNormalized(x, y, 0.08, 2); // Create minimal paths - if noise is in a very specific narrow band // Avoid water (0.3 threshold) and edges if (pathNoise > 0.48 && pathNoise < 0.52 && noiseValue > 0.35) { terrainType = this.terrainTypes.PATH; } // === FLOATING ISLAND EDGE === const edgeDistance = 2; // Tiles from edge (tighter border) const isNearEdge = x < edgeDistance || x >= this.width - edgeDistance || y < edgeDistance || y >= this.height - edgeDistance; const isEdge = x === 0 || x === this.width - 1 || y === 0 || y === this.height - 1; // Override terrain type at edges if (isEdge) { terrainType = this.terrainTypes.STONE; // Cliff wall (stone edge) } else if (isNearEdge) { // Keep Y-layer system active } // Flatten edges (cliff drop-off for floating island effect) if (isEdge) { elevation = 0; // Flat cliff wall } else if (isNearEdge) { elevation = Math.max(0, elevation - 0.2); // Slight dip near edge } this.tiles[y][x] = { gridX: x, gridY: y, type: terrainType.name, texture: terrainType.texture, height: noiseValue, elevation: elevation, // 0-1 (0=low, 1=high) yLayer: terrainType.yLayer, // Y-stacking layer hasDecoration: false, hasCrop: false }; // Generacija dekoracij (shranimo v data, ne ustvarjamo sprite-ov še) if (x > 5 && x < this.width - 5 && y > 5 && y < this.height - 5) { let decorType = null; let maxHp = 1; if (terrainType.name.includes('grass')) { // Check ANY grass type const rand = Math.random(); // Na hribih več kamnov if (elevation > 0.6 && rand < 0.05) { decorType = 'bush'; // Kamni maxHp = 5; } else if (rand < 0.025) { // Reduced to 2.5% trees (optimum balance) decorType = 'tree'; maxHp = 5; } else if (rand < 0.03) { decorType = 'gravestone'; // 💀 Nagrobniki maxHp = 10; // Težje uničiti } else if (rand < 0.08) { decorType = 'flower'; maxHp = 1; } } else if (terrainType.name === 'dirt' && Math.random() < 0.02) { decorType = 'bush'; maxHp = 3; } if (decorType) { const key = `${x},${y}`; const decorData = { gridX: x, gridY: y, type: decorType, id: key, maxHp: maxHp, hp: maxHp }; this.decorations.push(decorData); this.decorationsMap.set(key, decorData); this.tiles[y][x].hasDecoration = true; } } } } console.log('✅ Terrain and decorations data generated!'); } // DAMAGE / INTERACTION LOGIC damageDecoration(x, y, amount) { const key = `${x},${y}`; const decor = this.decorationsMap.get(key); if (!decor) return false; decor.hp -= amount; // Visual feedback (flash red) if (this.visibleDecorations.has(key)) { const sprite = this.visibleDecorations.get(key); sprite.setTint(0xff0000); this.scene.time.delayedCall(100, () => sprite.clearTint()); // Shake effect? 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; // Remove visual if (this.visibleDecorations.has(key)) { const sprite = this.visibleDecorations.get(key); sprite.setVisible(false); this.decorationPool.release(sprite); this.visibleDecorations.delete(key); } // Remove data this.decorationsMap.delete(key); // Remove from array (slow but needed for save compat for now) const index = this.decorations.indexOf(decor); if (index > -1) this.decorations.splice(index, 1); // Update tile flag if (this.tiles[y] && this.tiles[y][x]) { this.tiles[y][x].hasDecoration = false; } return decor.type; // Return type for dropping loot } placeStructure(x, y, structureType) { if (this.decorationsMap.has(`${x},${y}`)) return false; const decorData = { gridX: x, gridY: y, type: structureType, // 'struct_fence', etc. id: `${x},${y}`, maxHp: 5, hp: 5 }; // Add to data this.decorations.push(decorData); this.decorationsMap.set(decorData.id, decorData); // Update tile const tile = this.getTile(x, y); if (tile) tile.hasDecoration = true; // Force Visual Update immediately? // updateCulling will catch it on next frame, but to be safe: // Or leave it to update loop. return true; } // --- Dynamic Tile Modification --- 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; this.tiles[y][x].texture = typeDef.texture; // Force visual update if visible const key = `${x},${y}`; if (this.visibleTiles.has(key)) { const sprite = this.visibleTiles.get(key); sprite.setTexture(typeDef.texture); } } addCrop(x, y, cropData) { const key = `${x},${y}`; this.cropsMap.set(key, cropData); this.tiles[y][x].hasCrop = true; // updateCulling loop will pick it up on next frame } removeCrop(x, y) { const key = `${x},${y}`; if (this.cropsMap.has(key)) { // Remove visual 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}`); } } // Initialize rendering (called once) init(offsetX, offsetY) { this.offsetX = offsetX; this.offsetY = offsetY; } // Update culling (called every frame) updateCulling(camera) { const view = camera.worldView; const buffer = 200; 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; // Calculate visible bounding box (rough) 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 neededKeys = new Set(); 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}`; neededKeys.add(key); // Tile Logic if (!this.visibleTiles.has(key)) { const tilePos = this.iso.toScreen(x, y); const tileData = this.tiles[y][x]; const sprite = this.tilePool.get(); sprite.setTexture(tileData.texture); // Elevation effect: MOČAN vertikalni offset za hribe const elevationOffset = tileData.elevation * -25; // Povečano iz -10 na -25 sprite.setPosition( tilePos.x + this.offsetX, tilePos.y + this.offsetY + elevationOffset ); // DRAMATIČNO senčenje glede na višino if (tileData.type === 'grass') { let brightness = 1.0; if (tileData.elevation > 0.5) { // Visoko = svetlo (1.0 - 1.5) brightness = 1.0 + (tileData.elevation - 0.5) * 1.0; } else { // Nizko = temno (0.7 - 1.0) brightness = 0.7 + tileData.elevation * 0.6; } sprite.setTint(Phaser.Display.Color.GetColor( Math.min(255, Math.floor(92 * brightness)), Math.min(255, Math.floor(184 * brightness)), Math.min(255, Math.floor(92 * brightness)) )); } sprite.setDepth(this.iso.getDepth(x, y)); this.visibleTiles.set(key, sprite); } // Elevation effect matching tile logic const tileData = this.tiles[y][x]; const elevationOffset = tileData.elevation * -25; // Crop Logic if (this.tiles[y][x].hasCrop) { neededCropKeys.add(key); if (!this.visibleCrops.has(key)) { const cropData = this.cropsMap.get(key); if (cropData) { const cropPos = this.iso.toScreen(x, y); const sprite = this.cropPool.get(); sprite.setTexture(`crop_stage_${cropData.stage}`); sprite.setPosition( cropPos.x + this.offsetX, cropPos.y + this.offsetY + this.iso.tileHeight / 2 + elevationOffset ); const depth = this.iso.getDepth(x, y); sprite.setDepth(depth + 1); // Just slightly above tile this.visibleCrops.set(key, sprite); } } } // Decoration Logic if (this.tiles[y][x].hasDecoration) { neededDecorKeys.add(key); if (!this.visibleDecorations.has(key)) { // Fast lookup from map const decor = this.decorationsMap.get(key); if (decor) { const decorPos = this.iso.toScreen(x, y); const sprite = this.decorationPool.get(); sprite.setTexture(decor.type); // Apply same elevation offset as tile sprite.setPosition( decorPos.x + this.offsetX, decorPos.y + this.offsetY + this.iso.tileHeight / 2 + elevationOffset ); const depth = this.iso.getDepth(x, y); if (decor.type === 'flower') sprite.setDepth(depth + 1); else sprite.setDepth(depth + 1000); // Taller objects update depth sprite.flipX = (x + y) % 2 === 0; // INTERACTIVITY FIX: Allow clicking sprites directly sprite.setInteractive({ pixelPerfect: true, useHandCursor: true }); // Clear old listeners sprite.off('pointerdown'); // Add click listener sprite.on('pointerdown', (pointer) => { if (this.scene.interactionSystem) { // Manually trigger interaction logic this.scene.interactionSystem.handleDecorationClick(x, y); } }); this.visibleDecorations.set(key, sprite); } } } } } // Cleanup invisible tiles for (const [key, sprite] of this.visibleTiles) { if (!neededKeys.has(key)) { sprite.setVisible(false); this.tilePool.release(sprite); this.visibleTiles.delete(key); } } // Cleanup invisible decorations for (const [key, sprite] of this.visibleDecorations) { if (!neededDecorKeys.has(key)) { sprite.setVisible(false); this.decorationPool.release(sprite); this.visibleDecorations.delete(key); } } // Cleanup visible crops for (const [key, sprite] of this.visibleCrops) { if (!neededCropKeys.has(key)) { sprite.setVisible(false); this.cropPool.release(sprite); this.visibleCrops.delete(key); } } } // Helper functions getTerrainType(value) { for (const type of Object.values(this.terrainTypes)) { if (value < type.threshold) { return type; } } return this.terrainTypes.STONE; } getTile(gridX, gridY) { if (gridX >= 0 && gridX < this.width && gridY >= 0 && gridY < this.height) { return this.tiles[gridY][gridX]; } return null; } }