FAZA 17: 2.5D Minecraft-Style Terrain + Y-Layer Stacking + Custom Sprites
COMPLETED FEATURES: Custom Sprite Integration: - Player, Zombie, Merchant sprites (0.2 scale) - 11 custom sprites + 5 asset packs loaded - Auto-transparency processing (white/brown removal) - Gravestone system with atlas extraction 2.5D Minecraft-Style Terrain: - Volumetric blocks with 25px thickness - Strong left/right side shading (30%/50% darker) - Minecraft-style texture patterns (grass, dirt, stone) - Crisp black outlines for definition Y-Layer Stacking System: - GRASS_FULL: All green (elevation > 0.7) - GRASS_TOP: Green top + brown sides (elevation 0.4-0.7) - DIRT: All brown (elevation < 0.4) - Dynamic terrain depth based on height Floating Island World Edge: - Stone cliff walls at map borders - 2-tile transition zone - Elevation flattening for cliff drop-off effect - 100x100 world with defined boundaries Performance & Polish: - Canvas renderer for pixel-perfect sharpness - CSS image-rendering: crisp-edges - willReadFrequently optimization - No Canvas2D warnings Technical: - 3D volumetric trees and rocks - Hybrid rendering (2.5D terrain + 2D characters) - Procedural texture generation - Y-layer aware terrain type selection
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// Terrain Generator System
|
||||
// Generira proceduralni isometrični teren
|
||||
// Generira proceduralni isometrični teren in skrbi za optimizacijo (Culling, Object Pooling)
|
||||
class TerrainSystem {
|
||||
constructor(scene, width = 100, height = 100) {
|
||||
this.scene = scene;
|
||||
@@ -10,43 +10,670 @@ class TerrainSystem {
|
||||
this.noise = new PerlinNoise(Date.now());
|
||||
|
||||
this.tiles = [];
|
||||
this.tileSprites = [];
|
||||
this.decorations = []; // Array za save/load compat
|
||||
this.decorationsMap = new Map(); // Fast lookup key->decor
|
||||
this.cropsMap = new Map(); // Store dynamic crops separately
|
||||
|
||||
// Tipi terena z threshold vrednostmi
|
||||
// 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' },
|
||||
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' }
|
||||
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 },
|
||||
|
||||
FARMLAND: { threshold: 999, color: 0x4a3c2a, name: 'farmland', texture: 'tile_farmland', yLayer: 0 }
|
||||
};
|
||||
}
|
||||
|
||||
// Generiraj teren
|
||||
// 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 = 25; // Minecraft-style thickness (increased from 10)
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// 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: ${this.width}x${this.height}...`);
|
||||
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);
|
||||
const terrainType = this.getTerrainType(noiseValue);
|
||||
|
||||
// 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);
|
||||
|
||||
// === 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,
|
||||
color: terrainType.color,
|
||||
height: noiseValue
|
||||
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 === 'grass') {
|
||||
const rand = Math.random();
|
||||
|
||||
// Na hribih več kamnov
|
||||
if (elevation > 0.6 && rand < 0.05) {
|
||||
decorType = 'bush'; // Kamni (bomo kasneje naredili 'stone' tip)
|
||||
maxHp = 5;
|
||||
} else if (rand < 0.01) {
|
||||
decorType = 'tree';
|
||||
maxHp = 5;
|
||||
} else if (rand < 0.015) {
|
||||
decorType = 'gravestone'; // 💀 Nagrobniki
|
||||
maxHp = 10; // Težje uničiti
|
||||
} else if (rand < 0.1) {
|
||||
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 data generated!');
|
||||
console.log('✅ Terrain and decorations data generated!');
|
||||
}
|
||||
|
||||
// Določi tip terena glede na noise vrednost
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Crop Logic (render before decor or after? Same layer mostly)
|
||||
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
|
||||
);
|
||||
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);
|
||||
sprite.setPosition(
|
||||
decorPos.x + this.offsetX,
|
||||
decorPos.y + this.offsetY + this.iso.tileHeight / 2
|
||||
);
|
||||
|
||||
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;
|
||||
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) {
|
||||
@@ -56,71 +683,10 @@ class TerrainSystem {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user