Files
novafarma/src/systems/TerrainSystem.js
2025-12-07 13:16:04 +01:00

569 lines
22 KiB
JavaScript

// 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;
// Culling optimization
this.lastCullX = -9999;
this.lastCullY = -9999;
// 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;
}
// Helper za določanje tipa terena glede na noise vrednost
getTerrainType(value) {
if (value < this.terrainTypes.WATER.threshold) return this.terrainTypes.WATER;
if (value < this.terrainTypes.SAND.threshold) return this.terrainTypes.SAND;
if (value < this.terrainTypes.GRASS_FULL.threshold) return this.terrainTypes.GRASS_FULL; // Fallback grass
if (value < this.terrainTypes.GRASS_TOP.threshold) return this.terrainTypes.GRASS_TOP;
if (value < this.terrainTypes.DIRT.threshold) return this.terrainTypes.DIRT;
return this.terrainTypes.STONE;
}
getTile(x, y) {
if (x >= 0 && x < this.width && y >= 0 && y < this.height) {
return this.tiles[y][x];
}
return null;
}
// 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();
// Generate texture
graphics.generateTexture(key, tileW, tileH + thickness);
graphics.destroy();
// Update texture name in type def
type.texture = key;
}
}
createGravestoneSprite() {
const canvas = document.createElement('canvas');
const size = 32;
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext('2d', { willReadFrequently: true });
if (this.scene.textures.exists('objects_pack')) {
const sourceTexture = this.scene.textures.get('objects_pack');
const sourceImg = sourceTexture.getSourceImage();
const sourceX = 240;
const sourceY = 160;
ctx.drawImage(sourceImg, sourceX, sourceY, size, size, 0, 0, size, size);
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();
if (!this.scene.textures.exists('flower')) TextureGenerator.createFlowerSprite(this.scene, 'flower');
if (this.scene.textures.exists('stone_sprite')) {
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');
}
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');
}
if (this.scene.textures.exists('objects_pack') && !this.scene.textures.exists('gravestone')) {
this.createGravestoneSprite();
}
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();
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);
let elevation = this.noise.getNormalized(x, y, 0.03, 3);
let terrainType = this.getTerrainTypeByElevation(noiseValue, elevation);
const pathNoise = this.noise.getNormalized(x, y, 0.08, 2);
if (pathNoise > 0.48 && pathNoise < 0.52 && noiseValue > 0.35) {
terrainType = this.terrainTypes.PATH;
}
const edgeDistance = 2;
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;
if (isEdge) terrainType = this.terrainTypes.STONE;
if (isEdge) elevation = 0;
else if (isNearEdge) elevation = Math.max(0, elevation - 0.2);
this.tiles[y][x] = {
gridX: x,
gridY: y,
type: terrainType.name,
texture: terrainType.texture,
height: noiseValue,
elevation: elevation,
yLayer: terrainType.yLayer,
hasDecoration: false,
hasCrop: false
};
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.05) {
decorType = 'bush';
maxHp = 5;
} else if (rand < 0.025) {
decorType = 'tree';
maxHp = 5;
const sizeRand = Math.random();
if (sizeRand < 0.2) scale = 0.25;
else if (sizeRand < 0.8) scale = 0.6 + Math.random() * 0.2;
else scale = 1.0;
} else if (rand < 0.03) {
decorType = 'gravestone';
maxHp = 10;
} 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,
scale: scale
};
this.decorations.push(decorData);
this.decorationsMap.set(key, decorData);
this.tiles[y][x].hasDecoration = true;
}
}
}
}
console.log('✅ Terrain and decorations generated!');
}
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;
this.tiles[y][x].texture = typeDef.texture;
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;
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;
}
updateCulling(camera) {
// Simple Culling
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 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++) {
// TILES
const key = `${x},${y}`;
neededKeys.add(key);
if (!this.visibleTiles.has(key)) {
const tile = this.tiles[y][x];
const screenPos = this.iso.toScreen(x, y);
const sprite = this.tilePool.get();
sprite.setPosition(screenPos.x + this.offsetX, screenPos.y + this.offsetY);
sprite.setTexture(tile.texture);
sprite.setDepth(this.iso.getDepth(x, y));
this.visibleTiles.set(key, sprite);
}
// 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);
// Fix for house sprite
if (decor.type.includes('house_sprite') || decor.type.includes('market_sprite')) {
sprite.setTexture(decor.type);
sprite.setScale(decor.scale || 1.0);
} else {
sprite.setTexture(decor.type);
sprite.setScale(decor.scale || 1);
}
// Origin adjusted for buildings
if (decor.type.includes('house') || decor.type.includes('market')) {
sprite.setOrigin(0.5, 0.8); // Adjusted origin for buildings
} else {
sprite.setOrigin(0.5, 1);
}
sprite.setDepth(this.iso.getDepth(x, y) + 1); // Above tile
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}`);
sprite.setDepth(this.iso.getDepth(x, y) + 0.5);
this.visibleCrops.set(key, sprite);
}
}
}
}
// Cleanup tiles
for (const [key, sprite] of this.visibleTiles) {
if (!neededKeys.has(key)) {
sprite.setVisible(false);
this.tilePool.release(sprite);
this.visibleTiles.delete(key);
}
}
// Cleanup decorations
for (const [key, sprite] of this.visibleDecorations) {
if (!neededDecorKeys.has(key)) {
sprite.setVisible(false);
this.decorationPool.release(sprite);
this.visibleDecorations.delete(key);
}
}
// Cleanup crops
for (const [key, sprite] of this.visibleCrops) {
if (!neededCropKeys.has(key)) {
sprite.setVisible(false);
this.cropPool.release(sprite);
this.visibleCrops.delete(key);
}
}
}
}