1438 lines
55 KiB
JavaScript
1438 lines
55 KiB
JavaScript
// ========================================================
|
|
// STARDEW VALLEY STYLE FOREST MAP - 100x100
|
|
// ========================================================
|
|
const MAP_SIZE = 100; // 100x100 velika mapa!
|
|
const FARM_SIZE = 8; // 8x8 MICRO FARM - Začetek igre!
|
|
const FARM_CENTER_X = 50; // Center mape (50,50)
|
|
const FARM_CENTER_Y = 50; // Center mape (50,50)
|
|
|
|
// Zapuščene hiše lokacije - ODSTRANJENO ZA TESTIRANJE!
|
|
const ABANDONED_HOUSES = [
|
|
// { x: 25, y: 25, size: 5 },
|
|
// { x: 75, y: 30, size: 6 },
|
|
// { x: 40, y: 75, size: 4 }
|
|
]; // PRAZNO - brez hiš!
|
|
|
|
// ========================================================
|
|
// GOZD KONSTANTE - STARDEW VALLEY STYLE (OPTIMIZIRANO)
|
|
// ========================================================
|
|
const TREE_DENSITY = 0.0; // 0% - BREZ DREVES!
|
|
const PURPLE_TREE_CHANCE = 0.30; // 30% vijolčnih dreves
|
|
const FRUIT_TREE_CHANCE = 0.20; // 20% sadnih dreves
|
|
const ROCK_DENSITY_THRESHOLD = 0.60; // Prag za skupine skal
|
|
|
|
// ========================================================
|
|
// RUDNIK KONSTANTE (za terrainTypes)
|
|
// ========================================================
|
|
const TILE_PAVEMENT = 16; // ID za prehodno ploščico
|
|
const TILE_MINE_WALL = 81; // ID za zid rudnika
|
|
const TILE_STONE_ORE = 82; // ID za navadni kamen
|
|
const TILE_IRON_ORE = 83; // ID za železovo rudo
|
|
|
|
// ========================================================
|
|
// RIBNIK KONSTANTE
|
|
// ========================================================
|
|
const POND_CENTER_X = 30; // Center ribnika
|
|
const POND_CENTER_Y = 30; // Center ribnika
|
|
const POND_RADIUS = 4; // Radij ribnika (8x8)
|
|
|
|
// Terrain Generator System
|
|
class TerrainSystem {
|
|
constructor(scene, width = 100, height = 100, seed = null) {
|
|
this.scene = scene;
|
|
this.width = width;
|
|
this.height = height;
|
|
|
|
// 🎲 SEED-BASED GENERATION
|
|
this.seed = seed || Date.now().toString();
|
|
console.log(`🌍 TerrainSystem initialized with seed: ${this.seed}`);
|
|
|
|
// Seeded RNG - vedno isti rezultati za isti seed!
|
|
this.rng = this.createSeededRNG(this.seed);
|
|
|
|
this.iso = new IsometricUtils(48, 24);
|
|
this.noise = new PerlinNoise(this.hashCode(this.seed));
|
|
|
|
this.tiles = [];
|
|
this.decorations = [];
|
|
this.decorationsMap = new Map();
|
|
this.cropsMap = new Map();
|
|
this.tileHealthMap = new Map(); // Global register zdravja ploščic
|
|
|
|
this.visibleTiles = new Map();
|
|
this.visibleDecorations = new Map();
|
|
this.visibleCrops = new Map();
|
|
|
|
// Water animation sistem
|
|
this.waterTiles = []; // Array za water tiles
|
|
this.waterAnimationTimer = 0;
|
|
|
|
// Rain particles za ribnik
|
|
this.rainEmitter = null;
|
|
this.splashEmitters = []; // Array splash emitterjev
|
|
|
|
this.tilePool = {
|
|
active: [],
|
|
inactive: [],
|
|
get: () => {
|
|
if (this.tilePool.inactive.length > 0) {
|
|
const s = this.tilePool.inactive.pop();
|
|
s.setVisible(true);
|
|
return s;
|
|
}
|
|
const s = this.scene.add.sprite(0, 0, 'dirt');
|
|
s.setOrigin(0.5, 0.5);
|
|
return s;
|
|
},
|
|
release: (sprite) => {
|
|
sprite.setVisible(false);
|
|
this.tilePool.inactive.push(sprite);
|
|
}
|
|
};
|
|
|
|
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);
|
|
}
|
|
};
|
|
|
|
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 },
|
|
SAND: { name: 'sand', height: 0.2, color: 0xdddd44 },
|
|
GRASS_FULL: { name: 'grass_full', height: 0.35, color: 0x44aa44 },
|
|
GRASS_TOP: { name: 'grass_top', height: 0.45, color: 0x66cc66 },
|
|
DIRT: { name: 'dirt', height: 0.5, color: 0x8b4513 },
|
|
STONE: { name: 'stone', height: 0.7, color: 0x888888 },
|
|
PAVEMENT: { name: 'pavement', height: 0.6, color: 0x777777 },
|
|
RUINS: { name: 'ruins', height: 0.6, color: 0x555555 },
|
|
WALL_EDGE: { name: 'WALL_EDGE', height: 0.8, color: 0x505050, solid: true }, // OBZIDJE
|
|
PATH: { name: 'path', height: -1, color: 0xc2b280 },
|
|
FARMLAND: { name: 'farmland', height: -1, color: 0x5c4033 },
|
|
// MINE TYPES
|
|
MINE_FLOOR: { name: 'mine_floor', height: 0, color: 0x333333, id: TILE_PAVEMENT },
|
|
MINE_WALL: { name: 'mine_wall', height: 1, color: 0x1a1a1a, id: TILE_MINE_WALL },
|
|
ORE_STONE: { name: 'ore_stone', height: 0.5, color: 0x555555, id: TILE_STONE_ORE },
|
|
ORE_IRON: { name: 'ore_iron', height: 0.5, color: 0x884444, id: TILE_IRON_ORE }
|
|
};
|
|
|
|
this.offsetX = 0;
|
|
this.offsetY = 0;
|
|
|
|
this.generatedChunks = new Set();
|
|
this.chunkSize = 10;
|
|
|
|
// FOREST GENERATION: Tracking postavljenih dreves za preverjanje razdalje
|
|
this.placedTrees = []; // Seznam vseh postavljenih dreves { x, y }
|
|
this.MIN_TREE_DISTANCE_SQUARED = 2 * 2; // Minimalna razdalja med drevesi (2 tiles)
|
|
|
|
// Init tiles array with NULLs
|
|
this.tiles = Array.from({ length: this.height }, () => Array(this.width).fill(null));
|
|
}
|
|
|
|
/**
|
|
* Create seeded random number generator
|
|
* Uses Mulberry32 algorithm for consistent randomness
|
|
*/
|
|
createSeededRNG(seed) {
|
|
let h = this.hashCode(seed);
|
|
return function () {
|
|
h = Math.imul(h ^ (h >>> 16), 2246822507);
|
|
h = Math.imul(h ^ (h >>> 13), 3266489909);
|
|
h = (h ^= h >>> 16) >>> 0;
|
|
return h / 4294967296; // Return 0-1
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Convert string seed to numeric hash
|
|
*/
|
|
hashCode(str) {
|
|
let hash = 0;
|
|
for (let i = 0; i < str.length; i++) {
|
|
const char = str.charCodeAt(i);
|
|
hash = ((hash << 5) - hash) + char;
|
|
hash = hash & hash; // Convert to 32bit integer
|
|
}
|
|
return Math.abs(hash);
|
|
}
|
|
|
|
/**
|
|
* Preveri, ali je nova lokacija (newX, newY) dovolj oddaljena od vseh
|
|
* že postavljenih dreves. Uporablja kvadrat razdalje za hitrejšo optimizacijo.
|
|
*/
|
|
isTreeLocationFarEnough(newX, newY) {
|
|
for (let i = 0; i < this.placedTrees.length; i++) {
|
|
const existingTree = this.placedTrees[i];
|
|
|
|
// Izračunaj kvadrat razdalje
|
|
const dx = newX - existingTree.x;
|
|
const dy = newY - existingTree.y;
|
|
const distanceSq = (dx * dx) + (dy * dy);
|
|
|
|
// Če je razdalja manjša od zahtevane minimalne razdalje, zavrni lokacijo
|
|
if (distanceSq < this.MIN_TREE_DISTANCE_SQUARED) {
|
|
return false;
|
|
}
|
|
}
|
|
return true; // Lokacija je dovolj oddaljena
|
|
}
|
|
|
|
createTileTextures() {
|
|
const tileWidth = 48;
|
|
const tileHeight = 60;
|
|
const P = 0; // NO PADDING - seamless tiles!
|
|
|
|
const types = Object.values(this.terrainTypes);
|
|
|
|
// POSEBNA OBDELAVA ZA VODO - 2D Smooth Pond/Lake Style!
|
|
if (!this.scene.textures.exists('water')) {
|
|
const waterGraphics = this.scene.make.graphics({ x: 0, y: 0, add: false });
|
|
|
|
// RICH BLUE WATER - Stardew Valley style gradient
|
|
waterGraphics.fillGradientStyle(
|
|
0x1a5f7a, 0x1a5f7a, // Deep blue (top)
|
|
0x2a7fbc, 0x2a7fbc // Medium blue (bottom)
|
|
);
|
|
waterGraphics.fillRect(0, 0, 48, 48);
|
|
|
|
// SMOOTH WAVE HIGHLIGHTS (organic circles)
|
|
waterGraphics.fillStyle(0x4aa3d0, 0.4);
|
|
waterGraphics.fillCircle(10, 10, 12);
|
|
waterGraphics.fillCircle(35, 25, 10);
|
|
waterGraphics.fillCircle(20, 38, 8);
|
|
|
|
// Soft shimmer spots
|
|
waterGraphics.fillStyle(0x7fc9e8, 0.3);
|
|
waterGraphics.fillCircle(15, 20, 6);
|
|
waterGraphics.fillCircle(38, 12, 5);
|
|
|
|
// Bright reflection highlights
|
|
waterGraphics.fillStyle(0xffffff, 0.2);
|
|
waterGraphics.fillCircle(12, 15, 4);
|
|
waterGraphics.fillCircle(32, 30, 3);
|
|
|
|
// NO BORDER - seamless tiles!
|
|
waterGraphics.generateTexture('water', 48, 48);
|
|
waterGraphics.destroy();
|
|
console.log('🌊 Smooth pond water texture created (Stardew Valley style)!');
|
|
}
|
|
|
|
types.forEach((type) => {
|
|
if (this.scene.textures.exists(type.name)) return;
|
|
|
|
const graphics = this.scene.make.graphics({ x: 0, y: 0, add: false });
|
|
|
|
// Koordinate z upoštevanjem paddinga
|
|
const xs = P;
|
|
const xe = 48 + P;
|
|
const midX = 24 + P;
|
|
|
|
const topY = P;
|
|
const midY = 12 + P;
|
|
const bottomY = 24 + P;
|
|
const depth = 20;
|
|
|
|
// 1. STRANICE (Faces)
|
|
// Left Face
|
|
const cLeft = 0x8B4513; // RJAVA DIRT - Left face
|
|
graphics.fillStyle(cLeft);
|
|
graphics.beginPath();
|
|
graphics.moveTo(midX, bottomY);
|
|
graphics.lineTo(midX, bottomY + depth);
|
|
graphics.lineTo(xs, midY + depth);
|
|
graphics.lineTo(xs, midY);
|
|
graphics.closePath();
|
|
graphics.fill();
|
|
// graphics.lineStyle(2, cLeft); // NO STROKE!
|
|
// graphics.strokePath();
|
|
|
|
// Right Face
|
|
const cRight = 0x6B3410; // RJAVA DIRT - Right face (temnejša)
|
|
graphics.fillStyle(cRight);
|
|
graphics.beginPath();
|
|
graphics.moveTo(xe, midY);
|
|
graphics.lineTo(xe, midY + depth);
|
|
graphics.lineTo(midX, bottomY + depth);
|
|
graphics.lineTo(midX, bottomY);
|
|
graphics.closePath();
|
|
graphics.fill();
|
|
// graphics.lineStyle(2, cRight); // NO STROKE!
|
|
// graphics.strokePath();
|
|
|
|
// 2. ZGORNJA PLOSKEV (Top Face)
|
|
graphics.fillStyle(type.color);
|
|
graphics.beginPath();
|
|
graphics.moveTo(xs, midY);
|
|
graphics.lineTo(midX, topY);
|
|
graphics.lineTo(xe, midY);
|
|
graphics.lineTo(midX, bottomY);
|
|
graphics.closePath();
|
|
graphics.fill();
|
|
// graphics.lineStyle(2, type.color); // NO STROKE!
|
|
// graphics.strokePath();
|
|
|
|
// Highlight - REMOVED for seamless tiles
|
|
// graphics.lineStyle(1, 0xffffff, 0.15);
|
|
// graphics.beginPath();
|
|
// graphics.moveTo(xs, midY);
|
|
// graphics.lineTo(midX, topY);
|
|
// graphics.lineTo(xe, midY);
|
|
// graphics.strokePath();
|
|
|
|
// 3. DETAJLI - ENHANCED!
|
|
if (type.name.includes('grass')) {
|
|
// Darker grass spots (rich texture)
|
|
graphics.fillStyle(Phaser.Display.Color.IntegerToColor(type.color).darken(15).color, 0.4);
|
|
for (let i = 0; i < 12; i++) {
|
|
const rx = xs + 8 + Math.random() * 32;
|
|
const ry = topY + 3 + Math.random() * 18;
|
|
const size = 1 + Math.random() * 2;
|
|
graphics.fillCircle(rx, ry, size);
|
|
}
|
|
|
|
// Lighter grass highlights (freshness)
|
|
graphics.fillStyle(Phaser.Display.Color.IntegerToColor(type.color).lighten(20).color, 0.5);
|
|
for (let i = 0; i < 8; i++) {
|
|
const rx = xs + 10 + Math.random() * 28;
|
|
const ry = topY + 4 + Math.random() * 16;
|
|
graphics.fillCircle(rx, ry, 1);
|
|
}
|
|
|
|
// Medium grass blades
|
|
graphics.fillStyle(Phaser.Display.Color.IntegerToColor(type.color).lighten(10).color, 0.6);
|
|
for (let i = 0; i < 6; i++) {
|
|
const rx = xs + 12 + Math.random() * 24;
|
|
const ry = topY + 5 + Math.random() * 14;
|
|
graphics.fillRect(rx, ry, 1, 2);
|
|
}
|
|
}
|
|
|
|
// DIRT texture - Enhanced!
|
|
if (type.name.includes('dirt') || type.name === 'DIRT') {
|
|
// Darker dirt clumps
|
|
graphics.fillStyle(Phaser.Display.Color.IntegerToColor(type.color).darken(20).color, 0.5);
|
|
for (let i = 0; i < 10; i++) {
|
|
const rx = xs + 8 + Math.random() * 32;
|
|
const ry = topY + 3 + Math.random() * 18;
|
|
const size = 2 + Math.random() * 3;
|
|
graphics.fillCircle(rx, ry, size);
|
|
}
|
|
|
|
// Lighter dirt spots
|
|
graphics.fillStyle(Phaser.Display.Color.IntegerToColor(type.color).lighten(15).color, 0.4);
|
|
for (let i = 0; i < 8; i++) {
|
|
const rx = xs + 10 + Math.random() * 28;
|
|
const ry = topY + 4 + Math.random() * 16;
|
|
graphics.fillCircle(rx, ry, 1.5);
|
|
}
|
|
|
|
// Small stones in dirt
|
|
graphics.fillStyle(0x9b8f77, 0.6);
|
|
for (let i = 0; i < 5; i++) {
|
|
const rx = xs + 12 + Math.random() * 24;
|
|
const ry = topY + 5 + Math.random() * 14;
|
|
graphics.fillRect(rx, ry, 2, 2);
|
|
}
|
|
}
|
|
|
|
if (type.name.includes('stone') || type.name.includes('ruins')) {
|
|
// Dark stone spots
|
|
graphics.fillStyle(0x404040, 0.6);
|
|
for (let i = 0; i < 12; i++) {
|
|
const rx = xs + 6 + Math.random() * 36;
|
|
const ry = topY + 3 + Math.random() * 18;
|
|
const size = 2 + Math.random() * 4;
|
|
graphics.fillCircle(rx, ry, size);
|
|
}
|
|
|
|
// Medium gray spots
|
|
graphics.fillStyle(0x606060, 0.5);
|
|
for (let i = 0; i < 10; i++) {
|
|
const rx = xs + 8 + Math.random() * 32;
|
|
const ry = topY + 4 + Math.random() * 16;
|
|
const size = 1.5 + Math.random() * 3;
|
|
graphics.fillCircle(rx, ry, size);
|
|
}
|
|
|
|
// Lighter highlights
|
|
graphics.fillStyle(0xa0a0a0, 0.4);
|
|
for (let i = 0; i < 8; i++) {
|
|
const rx = xs + 10 + Math.random() * 28;
|
|
const ry = topY + 5 + Math.random() * 14;
|
|
graphics.fillCircle(rx, ry, 1);
|
|
}
|
|
|
|
// Crack lines
|
|
graphics.lineStyle(1, 0x303030, 0.3);
|
|
for (let i = 0; i < 3; i++) {
|
|
graphics.beginPath();
|
|
graphics.moveTo(xs + Math.random() * 48, topY + Math.random() * 20);
|
|
graphics.lineTo(xs + Math.random() * 48, topY + Math.random() * 20);
|
|
graphics.strokePath();
|
|
}
|
|
}
|
|
|
|
if (type.name.includes('pavement')) {
|
|
graphics.lineStyle(1, 0x555555, 0.5);
|
|
graphics.beginPath();
|
|
graphics.moveTo(xs + 12, midY + 6);
|
|
graphics.lineTo(xs + 36, midY - 6);
|
|
graphics.strokePath();
|
|
}
|
|
|
|
graphics.generateTexture(type.name, tileWidth + P * 2, tileHeight + P * 2);
|
|
graphics.destroy();
|
|
});
|
|
|
|
// ANIMATED WATER FRAMES - 4 frame animacija za tekoč tok!
|
|
this.createWaterFrames();
|
|
}
|
|
|
|
createWaterFrames() {
|
|
const tileWidth = 48;
|
|
const tileHeight = 48;
|
|
|
|
// 🌊 SMOOTH ANIMATED POND WATER (Stardew Valley style)
|
|
for (let frame = 0; frame < 4; frame++) {
|
|
const graphics = this.scene.make.graphics({ x: 0, y: 0, add: false });
|
|
|
|
// 1. BASE WATER - Rich gradient
|
|
graphics.fillGradientStyle(
|
|
0x1a5f7a, 0x1a5f7a, // Deep blue
|
|
0x2a7fbc, 0x2a7fbc // Medium blue
|
|
);
|
|
graphics.fillRect(0, 0, tileWidth, tileHeight);
|
|
|
|
// 2. ANIMATED WAVE CIRCLES (moving highlights)
|
|
const waveOffset = frame * 3;
|
|
|
|
// Primary wave circles
|
|
graphics.fillStyle(0x4aa3d0, 0.35);
|
|
graphics.fillCircle(10 + waveOffset, 10, 12 - frame);
|
|
graphics.fillCircle(35 - waveOffset, 25, 10 + frame * 0.5);
|
|
graphics.fillCircle(20, 38 + (frame % 2), 8);
|
|
|
|
// Secondary shimmer
|
|
graphics.fillStyle(0x7fc9e8, 0.25);
|
|
graphics.fillCircle(15 + (frame * 2), 20, 6);
|
|
graphics.fillCircle(38 - frame, 12, 5);
|
|
graphics.fillCircle(8, 32 + frame, 4);
|
|
|
|
// 3. BRIGHT REFLECTION SPOTS (twinkling)
|
|
graphics.fillStyle(0xffffff, 0.15 + (frame % 2) * 0.1);
|
|
graphics.fillCircle(12, 15, 3 + (frame % 2));
|
|
graphics.fillCircle(32 + (frame % 3), 30, 2);
|
|
graphics.fillCircle(25, 8, 2);
|
|
|
|
graphics.generateTexture(`water_frame_${frame}`, tileWidth, tileHeight);
|
|
graphics.destroy();
|
|
}
|
|
console.log('🌊 Smooth animated pond water created!');
|
|
}
|
|
|
|
generate() {
|
|
this.createTileTextures();
|
|
this.createWaterFrames(); // Ustvari water animation frames!
|
|
|
|
console.log('🌍 Initializing World (Zone Streaming Mode)...');
|
|
|
|
// Generate ONLY the starting area (Farm)
|
|
// Farm is at 20,20. Let's load 3x3 chunks around it.
|
|
const centerCx = Math.floor(FARM_CENTER_X / this.chunkSize);
|
|
const centerCy = Math.floor(FARM_CENTER_Y / this.chunkSize);
|
|
|
|
for (let cy = centerCy - 2; cy <= centerCy + 2; cy++) {
|
|
for (let cx = centerCx - 2; cx <= centerCx + 2; cx++) {
|
|
this.generateChunk(cx, cy);
|
|
}
|
|
}
|
|
|
|
console.log('✅ World Init Complete. Loaded ${this.generatedChunks.size} chunks.');
|
|
|
|
// DEBUG: Check how many water tiles were generated
|
|
let waterCount = 0;
|
|
for (let y = 0; y < this.height; y++) {
|
|
for (let x = 0; x < this.width; x++) {
|
|
if (this.tiles[y] && this.tiles[y][x] && this.tiles[y][x].type === 'water') {
|
|
waterCount++;
|
|
}
|
|
}
|
|
}
|
|
console.log(`🌊 DEBUG: Generated ${waterCount} water tiles in world`);
|
|
}
|
|
|
|
generateChunk(cx, cy) {
|
|
const key = `${cx},${cy}`;
|
|
if (this.generatedChunks.has(key)) return;
|
|
|
|
// Bounds check
|
|
if (cx < 0 || cy < 0 || cx * this.chunkSize >= this.width || cy * this.chunkSize >= this.height) return;
|
|
|
|
this.generatedChunks.add(key);
|
|
// console.log(`🔄 Streaming Chunk: [${cx}, ${cy}]`);
|
|
|
|
const startX = cx * this.chunkSize;
|
|
const startY = cy * this.chunkSize;
|
|
const endX = Math.min(startX + this.chunkSize, this.width);
|
|
const endY = Math.min(startY + this.chunkSize, this.height);
|
|
|
|
const validPositions = []; // Local valid positions for this chunk
|
|
|
|
for (let y = startY; y < endY; y++) {
|
|
for (let x = startX; x < endX; x++) {
|
|
|
|
// --- STARDEW VALLEY FOREST GENERATION ---
|
|
let terrainType = this.terrainTypes.GRASS_FULL; // Vsa mapa je trava!
|
|
|
|
// Farm Override - ZELENA PLATFORMA!
|
|
if (Math.abs(x - FARM_CENTER_X) <= FARM_SIZE / 2 && Math.abs(y - FARM_CENTER_Y) <= FARM_SIZE / 2) {
|
|
terrainType = this.terrainTypes.GRASS_FULL;
|
|
}
|
|
|
|
// Zapuščene hiše - DIRT tiles
|
|
let isHouse = false;
|
|
ABANDONED_HOUSES.forEach(house => {
|
|
if (x >= house.x && x < house.x + house.size &&
|
|
y >= house.y && y < house.y + house.size) {
|
|
terrainType = this.terrainTypes.DIRT; // Hiša na DIRT
|
|
isHouse = true;
|
|
}
|
|
});
|
|
|
|
// RIBNIK - okrogel ribnik z vodo!
|
|
const distToPond = Math.sqrt(
|
|
Math.pow(x - POND_CENTER_X, 2) +
|
|
Math.pow(y - POND_CENTER_Y, 2)
|
|
);
|
|
if (distToPond <= POND_RADIUS) {
|
|
terrainType = this.terrainTypes.WATER; // Voda!
|
|
}
|
|
|
|
// 🏔️ HEIGHT GENERATION (2.5D Elevation)
|
|
// Using second Perlin noise layer for smooth hills
|
|
const heightNoise = this.noise.noise(x * 0.05, y * 0.05); // Low frequency = smooth hills
|
|
const rawHeight = (heightNoise + 1) * 2.5; // Convert -1..1 to 0..5 range
|
|
const elevationHeight = Math.floor(rawHeight); // Discrete levels (0-5)
|
|
|
|
// Create Tile Data
|
|
this.tiles[y][x] = {
|
|
type: terrainType.name,
|
|
texture: terrainType.name,
|
|
hasDecoration: false,
|
|
hasCrop: false,
|
|
solid: terrainType.solid || false,
|
|
isHouse: isHouse,
|
|
height: elevationHeight // 🏔️ NEW: Elevation data (0-5)
|
|
};
|
|
|
|
// Track valid positions for decorations (TREES!)
|
|
if (terrainType.name === 'grass_full' && !isHouse) {
|
|
const isFarm = Math.abs(x - FARM_CENTER_X) <= (FARM_SIZE / 2 + 2) && Math.abs(y - FARM_CENTER_Y) <= (FARM_SIZE / 2 + 2);
|
|
if (!isFarm) {
|
|
validPositions.push({ x, y });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// --- STARDEW VALLEY FOREST DECORATION PASS ---
|
|
const farmMinX = FARM_CENTER_X - FARM_SIZE / 2;
|
|
const farmMaxX = FARM_CENTER_X + FARM_SIZE / 2;
|
|
const farmMinY = FARM_CENTER_Y - FARM_SIZE / 2;
|
|
const farmMaxY = FARM_CENTER_Y + FARM_SIZE / 2;
|
|
|
|
// DREVESA - 3% gostota z preverjanjem razdalje!
|
|
validPositions.forEach(pos => {
|
|
const rand = Math.random();
|
|
|
|
// 3% chance za drevo
|
|
if (rand < TREE_DENSITY) {
|
|
// PREVERJANJE RAZDALJE: Ali je ta lokacija dovolj oddaljena?
|
|
if (!this.isTreeLocationFarEnough(pos.x, pos.y)) {
|
|
return; // Preskoči to lokacijo - preblizu drugim drevesom!
|
|
}
|
|
|
|
const treeRand = Math.random();
|
|
let treeType;
|
|
|
|
// 30% VIJOLČNA DREVESA
|
|
if (treeRand < PURPLE_TREE_CHANCE) {
|
|
treeType = 'tree_purple';
|
|
}
|
|
// 20% SADNA DREVESA
|
|
else if (treeRand < PURPLE_TREE_CHANCE + FRUIT_TREE_CHANCE) {
|
|
const fruitTypes = ['tree_apple', 'tree_pear', 'tree_cherry'];
|
|
treeType = fruitTypes[Math.floor(Math.random() * fruitTypes.length)];
|
|
}
|
|
// 50% NAVADNA DREVESA
|
|
else {
|
|
const normalTrees = ['tree_green_final', 'tree_blue_final', 'tree_sapling'];
|
|
treeType = normalTrees[Math.floor(Math.random() * normalTrees.length)];
|
|
}
|
|
|
|
// DODAJ DREVO
|
|
this.addDecoration(pos.x, pos.y, treeType);
|
|
this.placedTrees.push({ x: pos.x, y: pos.y });
|
|
}
|
|
});
|
|
|
|
// KONEC generateChunk
|
|
}
|
|
|
|
updateChunks(camera) {
|
|
// Calculate which chunks are in view
|
|
const view = camera.worldView;
|
|
const buffer = 100; // Load slightly outside view
|
|
|
|
const p1 = this.iso.toGrid(view.x - buffer, view.y - buffer);
|
|
const p2 = this.iso.toGrid(view.x + view.width + buffer, view.y + view.height + buffer);
|
|
|
|
const minCx = Math.floor(Math.min(p1.x, p2.x) / this.chunkSize);
|
|
const maxCx = Math.ceil(Math.max(p1.x, p2.x) / this.chunkSize);
|
|
const minCy = Math.floor(Math.min(p1.y, p2.y) / this.chunkSize);
|
|
const maxCy = Math.ceil(Math.max(p1.y, p2.y) / this.chunkSize);
|
|
|
|
for (let cy = minCy; cy <= maxCy; cy++) {
|
|
for (let cx = minCx; cx <= maxCx; cx++) {
|
|
this.generateChunk(cx, cy);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Retained helper methods...
|
|
|
|
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);
|
|
|
|
// Chance to drop Blueprint
|
|
if (this.scene.blueprintSystem) {
|
|
this.scene.blueprintSystem.tryDropBlueprint(x, y, 'mining');
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
init(offsetX, offsetY) {
|
|
this.offsetX = offsetX;
|
|
this.offsetY = offsetY;
|
|
|
|
// Ustvari rain particles nad ribnikom
|
|
this.createRainOnPond();
|
|
}
|
|
|
|
createRainOnPond() {
|
|
// Izračunaj screen pozicijo ribnika
|
|
const pondScreenPos = this.iso.toScreen(POND_CENTER_X, POND_CENTER_Y);
|
|
const pondX = pondScreenPos.x + this.offsetX;
|
|
const pondY = pondScreenPos.y + this.offsetY;
|
|
|
|
// Ustvari particle texture (modra kapljica)
|
|
const graphics = this.scene.make.graphics({ x: 0, y: 0, add: false });
|
|
graphics.fillStyle(0x4488ff, 1);
|
|
graphics.fillCircle(2, 2, 2);
|
|
graphics.generateTexture('raindrop', 4, 4);
|
|
graphics.destroy();
|
|
|
|
// Rain emitter - pada v ribnik
|
|
this.rainEmitter = this.scene.add.particles(pondX, pondY - 100, 'raindrop', {
|
|
x: { min: -50, max: 50 },
|
|
y: 0,
|
|
lifespan: 1000,
|
|
speedY: { min: 200, max: 300 },
|
|
scale: { start: 0.5, end: 0.2 },
|
|
alpha: { start: 0.8, end: 0.3 },
|
|
frequency: 100,
|
|
blendMode: 'ADD'
|
|
});
|
|
|
|
// Dodaj rain sound effect
|
|
if (this.scene.soundManager) {
|
|
this.scene.soundManager.playRainSound();
|
|
}
|
|
|
|
console.log('🌧️ Rain particles created over pond!');
|
|
}
|
|
|
|
|
|
setTile(x, y, type) {
|
|
if (x >= 0 && x < this.width && y >= 0 && y < this.height) {
|
|
this.tiles[y][x].type = type;
|
|
}
|
|
}
|
|
|
|
placeTree(x, y, tileType) {
|
|
// 1. Safety Checks
|
|
if (!tileType || !tileType.includes('grass')) return;
|
|
|
|
const isFarm = Math.abs(x - FARM_CENTER_X) <= FARM_SIZE / 2 + 2;
|
|
const isCity = x >= CITY_START_X - 2 && x < CITY_START_X + CITY_SIZE + 2 && y >= CITY_START_Y - 2 && y < CITY_START_Y + CITY_SIZE + 2;
|
|
if (isFarm || isCity) return;
|
|
|
|
// 2. Noise for clustering (Forests)
|
|
const noiseVal = this.noise.noise(x * 0.1 + 123.45, y * 0.1 + 678.90);
|
|
|
|
// KLJUČNO PREVERJANJE: Ali tu že stoji drug Sprite?
|
|
const key = `${x},${y}`;
|
|
if (this.decorationsMap.has(key)) return;
|
|
if (this.tiles[y][x].hasDecoration) return;
|
|
|
|
// 3. Selection Logic
|
|
let shouldPlace = false;
|
|
let type = window.SPRITE_TREE_HEALTHY || 'tree_green_final';
|
|
|
|
// USER LOGIC: Gostota 40% če smo nad pragom (Gozd)
|
|
if (noiseVal > TREE_DENSITY_THRESHOLD && Math.random() < 0.4) {
|
|
shouldPlace = true;
|
|
}
|
|
// Fallback: Redka posamična drevesa (1.5%)
|
|
else if (Math.random() < 0.015) {
|
|
shouldPlace = true;
|
|
}
|
|
|
|
if (shouldPlace) {
|
|
// Variants Logic
|
|
const r = Math.random();
|
|
// 50% možnosti, da je drevo komaj začelo rasti (Sapling)
|
|
if (r < 0.50) type = window.SPRITE_TREE_SAPLING || 'tree_sapling';
|
|
else if (r < 0.60) type = window.SPRITE_TREE_DEAD || 'tree_dead_final';
|
|
else if (r < 0.65) type = window.SPRITE_TREE_BLUE || 'tree_blue_final';
|
|
// Ostalo (35%) je odraslo drevo
|
|
}
|
|
|
|
// 4. Placement
|
|
if (shouldPlace) {
|
|
this.addDecoration(x, y, type);
|
|
}
|
|
}
|
|
|
|
placeRock(x, y, tileType) {
|
|
if (!tileType || !tileType.includes('grass') && !tileType.includes('dirt')) return;
|
|
|
|
const isFarm = Math.abs(x - FARM_CENTER_X) <= FARM_SIZE / 2 + 2;
|
|
const isCity = x >= CITY_START_X - 2 && x < CITY_START_X + CITY_SIZE + 2 && y >= CITY_START_Y - 2 && y < CITY_START_Y + CITY_SIZE + 2;
|
|
if (isFarm || isCity) return;
|
|
|
|
// KLJUČNO PREVERJANJE: Ali tu že stoji drug Sprite?
|
|
const key = `${x},${y}`;
|
|
if (this.decorationsMap.has(key)) return;
|
|
if (this.tiles[y][x].hasDecoration) return;
|
|
|
|
// Noise for Rock Clusters
|
|
const noiseVal = this.noise.noise(x * 0.15 + 99.99, y * 0.15 + 88.88);
|
|
|
|
let shouldPlace = false;
|
|
let type = 'rock_asset'; // Default lep kamen
|
|
|
|
// USER LOGIC: Gostota 40% če smo nad pragom (Skalovje)
|
|
if (noiseVal > ROCK_DENSITY_THRESHOLD && Math.random() < 0.4) {
|
|
shouldPlace = true;
|
|
}
|
|
// Fallback: Redke posamične skale (1%)
|
|
else if (Math.random() < 0.01) {
|
|
shouldPlace = true;
|
|
}
|
|
|
|
if (shouldPlace) {
|
|
this.addDecoration(x, y, type);
|
|
}
|
|
}
|
|
|
|
placeStructure(gridX, gridY, type) {
|
|
if (type === 'ruin') {
|
|
for (let y = 0; y < 6; y++) {
|
|
for (let x = 0; x < 6; x++) {
|
|
// TEMP DISABLED: if (Math.random() > 0.6) this.addDecoration(gridX + x, gridY + y, 'fence');
|
|
this.setTile(gridX + x, gridY + y, 'stone');
|
|
}
|
|
}
|
|
}
|
|
if (type === 'arena') {
|
|
const size = 12;
|
|
for (let y = 0; y < size; y++) {
|
|
for (let x = 0; x < size; x++) {
|
|
const tx = gridX + x;
|
|
const ty = gridY + y;
|
|
|
|
this.setTile(tx, ty, 'stone');
|
|
if (x === 0 || x === size - 1 || y === 0 || y === size - 1) {
|
|
if (!(x === Math.floor(size / 2) && y === size - 1)) {
|
|
// TEMP DISABLED: this.addDecoration(tx, ty, 'fence');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
this.addDecoration(gridX + 6, gridY + 6, 'gravestone');
|
|
}
|
|
if (type === 'ruin_room') {
|
|
for (let y = 0; y < 5; y++) {
|
|
for (let x = 0; x < 5; x++) {
|
|
const tx = gridX + x;
|
|
const ty = gridY + y;
|
|
if (x > 0 && x < 4 && y > 0 && y < 4) {
|
|
this.setTile(tx, ty, 'ruins');
|
|
}
|
|
if (x === 0 || x === 4 || y === 0 || y === 4) {
|
|
const isCenter = (x === 2 || y === 2);
|
|
if (isCenter && Math.random() > 0.5) continue;
|
|
if (Math.random() > 0.3) {
|
|
// TEMP DISABLED: this.addDecoration(tx, ty, 'fence');
|
|
} else {
|
|
// User rocks in ruins
|
|
if (Math.random() > 0.5) {
|
|
const rType = Math.random() > 0.5 ? 'rock_1' : 'rock_2';
|
|
this.addDecoration(tx, ty, rType);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
addDecoration(gridX, gridY, type) {
|
|
if (gridX < 0 || gridX >= this.width || gridY < 0 || gridY >= this.height) return;
|
|
|
|
const key = `${gridX},${gridY}`;
|
|
if (this.decorationsMap.has(key)) return;
|
|
|
|
let scale = 1.0;
|
|
|
|
// POMANJŠANA DREVESA - Stardew Valley stil
|
|
if (type === 'rock_1' || type === 'rock_2') scale = 1.0; // Zmanjšano
|
|
else if (type === 'tree_green_new' || type === 'tree_blue_new' || type === 'tree_dead_new') scale = 0.04;
|
|
else if (type === 'flowers_new') scale = 0.02;
|
|
else if (type === 'fence') scale = 0.025;
|
|
else if (type === 'gravestone') scale = 0.03;
|
|
else if (type === 'hill_sprite') scale = 0.025;
|
|
else if (type.includes('_final')) scale = 0.7; // POMANJŠANO - final trees
|
|
else if (type.includes('purple') || type.includes('apple') || type.includes('pear') || type.includes('cherry')) {
|
|
scale = 0.6; // POMANJŠANO - novi sadni in vijolčni
|
|
}
|
|
else {
|
|
// Old Assets (Low Res)
|
|
if (type.includes('tree')) scale = 0.7 + Math.random() * 0.2; // POMANJŠANO (bilo 1.2-1.6)
|
|
else if (type.includes('rock')) scale = 0.6; // POMANJŠANO
|
|
else scale = 0.8; // POMANJŠANO
|
|
}
|
|
|
|
// Calculate Plant Day for Saplings (Growth System)
|
|
let plantDay = -1;
|
|
if (type.includes('sapling')) {
|
|
const w = this.scene.weatherSystem;
|
|
plantDay = w ? w.getDayCount() : 1;
|
|
// Random init offset for initial generation
|
|
if (!w) plantDay = 1 - Math.floor(Math.random() * 2);
|
|
}
|
|
|
|
// Determine if decoration is SOLID (blocking movement)
|
|
const typeLower = type.toLowerCase();
|
|
|
|
// Small decorations are NOT solid (can walk through)
|
|
const isSmallDecor = typeLower.includes('flower') ||
|
|
typeLower.includes('small_rock') ||
|
|
typeLower.includes('path_stone') ||
|
|
typeLower.includes('mushroom') ||
|
|
typeLower.includes('puddle') ||
|
|
typeLower.includes('fence'); // OGRAJE SO PREHODNE!
|
|
|
|
const isSolid = !isSmallDecor && (
|
|
typeLower.includes('tree') ||
|
|
typeLower.includes('sapling') ||
|
|
// ROCKS REMOVED - walkable now!
|
|
// typeLower.includes('rock') ||
|
|
// typeLower.includes('stone') ||
|
|
// fence REMOVED - it's walkable now!
|
|
typeLower.includes('wall') ||
|
|
typeLower.includes('signpost') ||
|
|
typeLower.includes('hill') ||
|
|
typeLower.includes('chest') ||
|
|
typeLower.includes('spawner') ||
|
|
typeLower.includes('ruin') ||
|
|
typeLower.includes('arena') ||
|
|
typeLower.includes('house') ||
|
|
typeLower.includes('gravestone') ||
|
|
typeLower.includes('bush') ||
|
|
typeLower.includes('fallen_log') ||
|
|
typeLower.includes('furnace') || // WORKSTATION
|
|
typeLower.includes('mint') // WORKSTATION
|
|
);
|
|
|
|
const decorData = {
|
|
gridX: gridX,
|
|
gridY: gridY,
|
|
type: type,
|
|
id: key,
|
|
maxHp: 10,
|
|
hp: 10,
|
|
scale: scale,
|
|
plantDay: plantDay, // Added for Growth System
|
|
solid: isSolid // TRDNOST (collision blocking)
|
|
};
|
|
this.decorations.push(decorData);
|
|
this.decorationsMap.set(key, decorData);
|
|
|
|
if (this.tiles[gridY] && this.tiles[gridY][gridX]) {
|
|
this.tiles[gridY][gridX].hasDecoration = true;
|
|
}
|
|
|
|
// Register Workstation
|
|
if (typeLower.includes('furnace') || typeLower.includes('mint')) {
|
|
if (this.scene.workstationSystem) {
|
|
// Determine type exactly
|
|
let stationType = 'furnace';
|
|
if (typeLower.includes('mint')) stationType = 'mint';
|
|
this.scene.workstationSystem.addStation(gridX, gridY, stationType);
|
|
}
|
|
}
|
|
}
|
|
|
|
setTileType(x, y, typeName) {
|
|
if (!this.tiles[y] || !this.tiles[y][x]) return;
|
|
this.tiles[y][x].type = typeName;
|
|
|
|
const key = `${x},${y}`;
|
|
if (this.visibleTiles.has(key)) {
|
|
const sprite = this.visibleTiles.get(key);
|
|
sprite.setTexture(typeName);
|
|
}
|
|
}
|
|
|
|
addCrop(x, y, cropData) {
|
|
const key = `${x},${y}`;
|
|
this.cropsMap.set(key, cropData);
|
|
this.tiles[y][x].hasCrop = true;
|
|
}
|
|
|
|
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}`);
|
|
}
|
|
}
|
|
|
|
getTile(x, y) {
|
|
if (this.tiles[y] && this.tiles[y][x]) {
|
|
return this.tiles[y][x];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
updateCulling(camera) {
|
|
this.updateChunks(camera);
|
|
|
|
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 neededTileKeys = new Set();
|
|
const neededDecorKeys = new Set();
|
|
const neededCropKeys = new Set();
|
|
|
|
const voxelOffset = 12;
|
|
|
|
for (let y = startY; y < endY; y++) {
|
|
for (let x = startX; x < endX; x++) {
|
|
const key = `${x},${y}`;
|
|
const tile = this.tiles[y][x];
|
|
|
|
if (tile) {
|
|
neededTileKeys.add(key);
|
|
if (!this.visibleTiles.has(key)) {
|
|
const sprite = this.tilePool.get();
|
|
|
|
// Use ANIMATED water frames (not static bubble texture!)
|
|
if (tile.type === 'water') {
|
|
sprite.setTexture('water_frame_0'); // Start with frame 0
|
|
sprite.isWater = true; // Mark for animation
|
|
|
|
// NO alpha tween - animation handles it
|
|
} else {
|
|
sprite.setTexture(tile.type);
|
|
}
|
|
|
|
const screenPos = this.iso.toScreen(x, y);
|
|
|
|
// 🌊 SKIP HEIGHT EFFECTS FOR WATER (prevents grid lines!)
|
|
if (tile.type === 'water') {
|
|
sprite.setPosition(
|
|
Math.round(screenPos.x + this.offsetX),
|
|
Math.round(screenPos.y + this.offsetY)
|
|
);
|
|
sprite.setScale(1.0);
|
|
sprite.clearTint();
|
|
} else {
|
|
// 🏔️ HEIGHT VISUALIZATION (2.5D Effect) - EXTREME!
|
|
const height = tile.height || 0;
|
|
|
|
// 1. Tint Effect (EXTREME CONTRAST - black valleys, white peaks)
|
|
// Height 0 = 0x666666 (dark gray), Height 5 = 0xffffff (pure white)
|
|
const tintValue = 0x666666 + (height * 0x333333);
|
|
sprite.setTint(tintValue);
|
|
|
|
// 2. Scale Variation (MASSIVE - 50% size increase!)
|
|
const scaleBonus = 1.0 + (height * 0.1); // Max +50% at height 5
|
|
sprite.setScale(scaleBonus);
|
|
|
|
// 3. Y-Offset (HUGE elevation - mountains!)
|
|
const elevationOffset = -(height * 15); // Each height level = 15px up!
|
|
|
|
sprite.setPosition(
|
|
Math.round(screenPos.x + this.offsetX),
|
|
Math.round(screenPos.y + this.offsetY + elevationOffset)
|
|
);
|
|
}
|
|
|
|
sprite.setDepth(this.iso.getDepth(x, y, this.iso.LAYER_FLOOR)); // Tiles = Floor
|
|
this.visibleTiles.set(key, sprite);
|
|
}
|
|
}
|
|
|
|
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(Math.round(screenPos.x + this.offsetX), Math.round(screenPos.y + this.offsetY - voxelOffset));
|
|
|
|
if (decor.type.includes('house') || decor.type.includes('market') || decor.type.includes('structure')) {
|
|
sprite.setOrigin(0.5, 0.8);
|
|
} else {
|
|
// DREVESA: Origin na dno, da Y-sort deluje
|
|
sprite.setOrigin(0.5, 1.0);
|
|
}
|
|
|
|
sprite.setTexture(decor.type);
|
|
sprite.setScale(decor.scale || 1.0);
|
|
|
|
if (decor.alpha !== undefined) {
|
|
sprite.setAlpha(decor.alpha);
|
|
}
|
|
|
|
// Layer Objects
|
|
sprite.setDepth(this.iso.getDepth(x, y, this.iso.LAYER_OBJECTS));
|
|
|
|
// 🎯 HYBRID POINTER EVENTS - Click-to-collect system
|
|
// Only for collectible resources (trees, rocks, etc.)
|
|
const isCollectible = decor.type.includes('tree') ||
|
|
decor.type.includes('rock') ||
|
|
decor.type.includes('bush') ||
|
|
decor.type.includes('flower');
|
|
|
|
if (isCollectible) {
|
|
// Make interactive with hand cursor
|
|
sprite.setInteractive({ useHandCursor: true });
|
|
|
|
// Store grid position for later use
|
|
sprite.setData('gridX', x);
|
|
sprite.setData('gridY', y);
|
|
sprite.setData('decorType', decor.type);
|
|
|
|
// HOVER EVENT - Yellow highlight
|
|
sprite.on('pointerover', () => {
|
|
sprite.setTint(0xffff00); // Yellow highlight
|
|
});
|
|
|
|
sprite.on('pointerout', () => {
|
|
sprite.clearTint(); // Remove highlight
|
|
});
|
|
|
|
// CLICK EVENT - Collect resource (with proximity check)
|
|
sprite.on('pointerdown', () => {
|
|
this.handleResourceClick(x, y, decor.type, sprite);
|
|
});
|
|
}
|
|
|
|
this.visibleDecorations.set(key, sprite);
|
|
}
|
|
}
|
|
|
|
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(Math.round(screenPos.x + this.offsetX), Math.round(screenPos.y + this.offsetY - voxelOffset));
|
|
|
|
const cropType = crop.type || 'wheat';
|
|
// Če je wheat, uporabimo 'crop_stage_' za nazaj združljivost s TextureGeneratorjem?
|
|
// TextureGenerator dela 'crop_stage_X'.
|
|
// Če dodam 'wheat_stage_X', moram posodobiti TextureGenerator.
|
|
// Za zdaj:
|
|
let textureKey = `crop_stage_${crop.stage}`;
|
|
if (cropType === 'corn') textureKey = `corn_stage_${crop.stage}`;
|
|
if (cropType === 'wheat' && this.scene.textures.exists('wheat_stage_1')) textureKey = `wheat_stage_${crop.stage}`;
|
|
|
|
sprite.setTexture(textureKey);
|
|
|
|
sprite.setOrigin(0.5, 1);
|
|
// Layer Objects (da igralec hodi okoli njih)
|
|
sprite.setDepth(this.iso.getDepth(x, y, this.iso.LAYER_OBJECTS));
|
|
this.visibleCrops.set(key, sprite);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const [key, sprite] of this.visibleTiles) {
|
|
if (!neededTileKeys.has(key)) {
|
|
sprite.setVisible(false);
|
|
this.tilePool.release(sprite);
|
|
this.visibleTiles.delete(key);
|
|
}
|
|
}
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
getTile(x, y) {
|
|
if (x >= 0 && x < this.width && y >= 0 && y < this.height) {
|
|
return this.tiles[y][x];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
update(delta) {
|
|
// Water animation (250ms per frame = 4 FPS)
|
|
this.waterAnimTimer = (this.waterAnimTimer || 0) + delta;
|
|
if (this.waterAnimTimer > 250) {
|
|
this.waterAnimTimer = 0;
|
|
this.waterCurrentFrame = ((this.waterCurrentFrame || 0) + 1) % 4;
|
|
|
|
// Update all water tiles
|
|
for (const [key, sprite] of this.visibleTiles) {
|
|
if (sprite.isWater) {
|
|
sprite.setTexture(`water_frame_${this.waterCurrentFrame}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
this.growthTimer = (this.growthTimer || 0) + delta;
|
|
if (this.growthTimer < 5000) return;
|
|
this.growthTimer = 0;
|
|
|
|
const weather = this.scene.weatherSystem;
|
|
if (!weather) return;
|
|
const currentDay = weather.getDayCount();
|
|
|
|
const healthyType = window.SPRITE_TREE_HEALTHY || 'tree_green_final';
|
|
|
|
for (const decor of this.decorationsMap.values()) {
|
|
if (decor.type.includes('sapling')) {
|
|
if (decor.plantDay !== undefined && decor.plantDay > -100 && (currentDay - decor.plantDay >= 3)) {
|
|
decor.type = healthyType;
|
|
decor.scale = 1.0;
|
|
decor.hp = 10;
|
|
decor.plantDay = -1;
|
|
|
|
const key = decor.id;
|
|
if (this.visibleDecorations.has(key)) {
|
|
const sprite = this.visibleDecorations.get(key);
|
|
sprite.setTexture(decor.type);
|
|
sprite.setScale(decor.scale);
|
|
this.scene.tweens.add({
|
|
targets: sprite,
|
|
scaleY: { from: 0.1, to: decor.scale },
|
|
duration: 800,
|
|
ease: 'Bounce.out'
|
|
});
|
|
console.log(`🌳 Tree grew on Day ${currentDay}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
placeStructure(x, y, type) {
|
|
// Generate textures if needed
|
|
if (type === 'chest' && !this.scene.textures.exists('chest')) {
|
|
TextureGenerator.createChestSprite(this.scene, 'chest');
|
|
}
|
|
if (type === 'spawner' && !this.scene.textures.exists('spawner')) {
|
|
TextureGenerator.createSpawnerSprite(this.scene, 'spawner');
|
|
}
|
|
if (type === 'ruin' && !this.scene.textures.exists('ruin')) {
|
|
TextureGenerator.createStructureSprite(this.scene, 'ruin', 'ruin');
|
|
}
|
|
if (type === 'arena' && !this.scene.textures.exists('arena')) {
|
|
// Arena uses ruin texture for now
|
|
TextureGenerator.createStructureSprite(this.scene, 'arena', 'ruin');
|
|
}
|
|
if (type.startsWith('signpost')) {
|
|
const textMap = { 'signpost_city': '→', 'signpost_farm': '←', 'signpost_both': '⇅' };
|
|
const text = textMap[type] || '?';
|
|
if (!this.scene.textures.exists(type)) {
|
|
TextureGenerator.createSignpostSprite(this.scene, type, text);
|
|
}
|
|
}
|
|
|
|
// Place as decoration
|
|
this.addDecoration(x, y, type);
|
|
console.log(`🏛️ Place ${type} at (${x}, ${y})`);
|
|
}
|
|
|
|
setSolid(x, y, isSolid) {
|
|
if (x >= 0 && x < this.width && y >= 0 && y < this.height) {
|
|
this.tiles[y][x].solid = isSolid;
|
|
}
|
|
}
|
|
|
|
// 🏔️ HEIGHT-AWARE COLLISION
|
|
// Returns true if tile is unwalkable (solid OR cliff)
|
|
isSolid(x, y, fromX = null, fromY = null) {
|
|
// Out of bounds = solid
|
|
if (x < 0 || x >= this.width || y < 0 || y >= this.height) {
|
|
return true;
|
|
}
|
|
|
|
const tile = this.tiles[y][x];
|
|
|
|
// 1. Check if tile itself is solid (walls, etc.)
|
|
if (tile.solid) {
|
|
return true;
|
|
}
|
|
|
|
// 2. Check height difference (cliff detection)
|
|
if (fromX !== null && fromY !== null) {
|
|
const fromTile = this.getTile(fromX, fromY);
|
|
if (fromTile) {
|
|
const fromHeight = fromTile.height || 0;
|
|
const toHeight = tile.height || 0;
|
|
const heightDiff = Math.abs(toHeight - fromHeight);
|
|
|
|
// Can't walk over height difference > 1 (cliffs!)
|
|
if (heightDiff > 1) {
|
|
console.log(`🏔️ Blocked by cliff! Height diff: ${heightDiff} (from ${fromHeight} to ${toHeight})`);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false; // Walkable
|
|
}
|
|
|
|
/**
|
|
* 🎯 HYBRID RESOURCE CLICK HANDLER
|
|
* Handles click-to-collect with proximity check
|
|
* @param {number} x - Grid X position
|
|
* @param {number} y - Grid Y position
|
|
* @param {string} decorType - Type of decoration (tree, rock, etc.)
|
|
* @param {Phaser.GameObjects.Sprite} sprite - The clicked sprite
|
|
*/
|
|
handleResourceClick(x, y, decorType, sprite) {
|
|
// 1. Get player position
|
|
if (!this.scene.player) {
|
|
console.warn('⚠️ Player not found');
|
|
return;
|
|
}
|
|
|
|
const playerPos = this.scene.player.getPosition();
|
|
const playerX = playerPos.x;
|
|
const playerY = playerPos.y;
|
|
|
|
// 2. PROXIMITY CHECK - Player must be within 3 tiles
|
|
const distance = Phaser.Math.Distance.Between(playerX, playerY, x, y);
|
|
const MAX_DISTANCE = 3; // 3 tiles
|
|
|
|
if (distance > MAX_DISTANCE) {
|
|
// Too far - show warning
|
|
console.log(`⚠️ Too far! Distance: ${distance.toFixed(1)} tiles`);
|
|
|
|
// Visual feedback - shake sprite
|
|
this.scene.tweens.add({
|
|
targets: sprite,
|
|
x: sprite.x + 5,
|
|
duration: 50,
|
|
yoyo: true,
|
|
repeat: 2
|
|
});
|
|
|
|
// Floating text
|
|
if (this.scene.events) {
|
|
this.scene.events.emit('show-floating-text', {
|
|
x: sprite.x,
|
|
y: sprite.y - 50,
|
|
text: 'Preblizu!',
|
|
color: '#ff4444'
|
|
});
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// 3. TOOL CHECK - Player needs correct tool
|
|
const requiredTool = this.getRequiredTool(decorType);
|
|
const hasTool = this.scene.player.hasToolEquipped(requiredTool);
|
|
|
|
if (!hasTool && requiredTool) {
|
|
console.log(`⚠️ Need tool: ${requiredTool}`);
|
|
|
|
// Floating text
|
|
if (this.scene.events) {
|
|
this.scene.events.emit('show-floating-text', {
|
|
x: sprite.x,
|
|
y: sprite.y - 50,
|
|
text: `Potrebuješ: ${requiredTool}`,
|
|
color: '#ff4444'
|
|
});
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// 4. COLLECT - Damage decoration (uses existing HP system)
|
|
console.log(`✅ Collecting ${decorType} at (${x}, ${y})`);
|
|
|
|
// Use existing damage system (maintains HP logic)
|
|
const result = this.damageDecoration(x, y, 1); // 1 hit per click
|
|
|
|
// Optional: Instant collect mode (if you want 1-click collect)
|
|
// this.damageDecoration(x, y, 999);
|
|
|
|
// Sound effect
|
|
if (this.scene.soundManager) {
|
|
if (decorType.includes('tree')) {
|
|
this.scene.soundManager.playChopSound();
|
|
} else if (decorType.includes('rock')) {
|
|
this.scene.soundManager.playMineSound();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get required tool for decoration type
|
|
*/
|
|
getRequiredTool(decorType) {
|
|
if (decorType.includes('tree')) return 'axe';
|
|
if (decorType.includes('rock')) return 'pickaxe';
|
|
if (decorType.includes('bush')) return 'axe';
|
|
return null; // No tool required (flowers, etc.)
|
|
}
|
|
|
|
// Water Animation Update - DISABLED (using tweens now)
|
|
update(time, delta) {
|
|
// Water animation je zdaj implementirana z Phaser tweens
|
|
// Ni potrebe po ročnem frame update-u
|
|
}
|
|
}
|