feat: Complete 2D Visual Overhaul - Isometric to Flat Top-Down

- NEW: Flat2DTerrainSystem.js (375 lines)
- NEW: map2d_data.js procedural map (221 lines)
- MODIFIED: GameScene async create, 2D terrain integration
- MODIFIED: Player.js flat 2D positioning
- MODIFIED: game.js disabled pixelArt for smooth rendering
- FIXED: 15+ bugs (updateCulling, isometric conversions, grid lines)
- ADDED: Phase 28 to TASKS.md
- DOCS: DNEVNIK.md session summary

Result: Working flat 2D game with Stardew Valley style!
Time: 5.5 hours
This commit is contained in:
2025-12-14 17:12:40 +01:00
parent c3dd39e1a6
commit 80bddf5d61
37 changed files with 8164 additions and 1800 deletions

View File

@@ -0,0 +1,344 @@
// Crafting System - Handles recipe management and item crafting
class CraftingSystem {
constructor(scene) {
this.scene = scene;
this.recipes = {};
this.categories = [];
this.unlockedRecipes = new Set();
// Crafting queue
this.craftingQueue = [];
this.isCrafting = false;
this.currentCraft = null;
this.craftProgress = 0;
console.log('🛠️ CraftingSystem initialized');
}
async loadRecipes() {
try {
// Load recipes from JSON file
const response = await fetch('data/recipes.json');
const data = await response.json();
this.recipes = data.recipes;
this.categories = data.categories;
// Initialize unlocked recipes
Object.keys(this.recipes).forEach(recipeId => {
const recipe = this.recipes[recipeId];
if (recipe.unlocked) {
this.unlockedRecipes.add(recipeId);
}
});
console.log(`✅ Loaded ${Object.keys(this.recipes).length} recipes`);
console.log(`🔓 ${this.unlockedRecipes.size} unlocked recipes`);
return true;
} catch (error) {
console.error('❌ Failed to load recipes:', error);
return false;
}
}
// Get all recipes (optionally filtered by category)
getRecipes(category = 'all') {
const recipeList = Object.values(this.recipes);
if (category === 'all') {
return recipeList;
}
return recipeList.filter(recipe => recipe.category === category);
}
// Get only unlocked recipes
getUnlockedRecipes(category = 'all') {
return this.getRecipes(category).filter(recipe =>
this.unlockedRecipes.has(recipe.id)
);
}
// Check if recipe is unlocked
isUnlocked(recipeId) {
return this.unlockedRecipes.has(recipeId);
}
// Unlock a recipe
unlockRecipe(recipeId) {
if (this.recipes[recipeId]) {
this.unlockedRecipes.add(recipeId);
console.log(`🔓 Unlocked recipe: ${this.recipes[recipeId].name}`);
// Notify UI
this.scene.events.emit('recipe-unlocked', recipeId);
return true;
}
return false;
}
// Check if player has required ingredients
canCraft(recipeId) {
const recipe = this.recipes[recipeId];
if (!recipe) return false;
// Check if unlocked
if (!this.isUnlocked(recipeId)) {
return { canCraft: false, reason: 'locked' };
}
// Check ingredients
const inventory = this.scene.inventorySystem;
if (!inventory) {
return { canCraft: false, reason: 'no_inventory' };
}
const missing = [];
for (const [itemId, requiredAmount] of Object.entries(recipe.ingredients)) {
const hasAmount = inventory.getItemCount(itemId);
if (hasAmount < requiredAmount) {
missing.push({
item: itemId,
required: requiredAmount,
has: hasAmount,
need: requiredAmount - hasAmount
});
}
}
if (missing.length > 0) {
return { canCraft: false, reason: 'missing_ingredients', missing };
}
return { canCraft: true };
}
// Start crafting an item
craftItem(recipeId) {
const recipe = this.recipes[recipeId];
if (!recipe) {
console.warn(`⚠️ Recipe not found: ${recipeId}`);
return false;
}
// Check if can craft
const check = this.canCraft(recipeId);
if (!check.canCraft) {
console.warn(`⚠️ Cannot craft ${recipe.name}: ${check.reason}`);
return false;
}
// Consume ingredients
const inventory = this.scene.inventorySystem;
for (const [itemId, amount] of Object.entries(recipe.ingredients)) {
inventory.removeItem(itemId, amount);
}
// Add to crafting queue
this.craftingQueue.push({
recipeId: recipeId,
recipe: recipe,
startTime: Date.now(),
duration: recipe.craftTime || 1000
});
console.log(`🔨 Started crafting: ${recipe.name}`);
// Start crafting if not already crafting
if (!this.isCrafting) {
this.startNextCraft();
}
// Emit event
this.scene.events.emit('craft-started', recipeId);
// Play sound
if (this.scene.soundManager && this.scene.soundManager.playCraftSound) {
this.scene.soundManager.playCraftSound();
}
return true;
}
// Start next item in queue
startNextCraft() {
if (this.craftingQueue.length === 0) {
this.isCrafting = false;
this.currentCraft = null;
this.craftProgress = 0;
return;
}
this.isCrafting = true;
this.currentCraft = this.craftingQueue[0];
this.craftProgress = 0;
console.log(`⏳ Processing: ${this.currentCraft.recipe.name}`);
}
// Update crafting progress
update(delta) {
if (!this.isCrafting || !this.currentCraft) return;
const elapsed = Date.now() - this.currentCraft.startTime;
this.craftProgress = Math.min(1.0, elapsed / this.currentCraft.duration);
// Check if finished
if (this.craftProgress >= 1.0) {
this.completeCraft();
}
// Emit progress event for UI
this.scene.events.emit('craft-progress', {
recipe: this.currentCraft.recipe,
progress: this.craftProgress
});
}
// Complete current craft
completeCraft() {
if (!this.currentCraft) return;
const recipe = this.currentCraft.recipe;
// Add result to inventory
const inventory = this.scene.inventorySystem;
inventory.addItem(recipe.result.item, recipe.result.quantity);
console.log(`✅ Crafted: ${recipe.result.quantity}x ${recipe.name}`);
// Emit event
this.scene.events.emit('craft-complete', {
recipeId: recipe.id,
item: recipe.result.item,
quantity: recipe.result.quantity
});
// Play sound
if (this.scene.soundManager && this.scene.soundManager.playSuccessSound) {
this.scene.soundManager.playSuccessSound();
}
// Show floating text
if (this.scene.player && this.scene.player.sprite) {
const text = `+${recipe.result.quantity} ${recipe.name}`;
this.scene.events.emit('floating-text', {
x: this.scene.player.sprite.x,
y: this.scene.player.sprite.y - 50,
text: text,
color: '#00ff00'
});
}
// Remove from queue
this.craftingQueue.shift();
// Start next craft
this.startNextCraft();
}
// Cancel current craft
cancelCraft() {
if (!this.currentCraft) return false;
const recipe = this.currentCraft.recipe;
// Refund ingredients (partial refund based on progress)
const refundPercent = 1.0 - this.craftProgress;
const inventory = this.scene.inventorySystem;
for (const [itemId, amount] of Object.entries(recipe.ingredients)) {
const refundAmount = Math.floor(amount * refundPercent);
if (refundAmount > 0) {
inventory.addItem(itemId, refundAmount);
}
}
console.log(`❌ Cancelled crafting: ${recipe.name} (${Math.floor(refundPercent * 100)}% refund)`);
// Remove from queue
this.craftingQueue.shift();
// Emit event
this.scene.events.emit('craft-cancelled', recipe.id);
// Start next
this.startNextCraft();
return true;
}
// Get current crafting info
getCurrentCraft() {
if (!this.currentCraft) return null;
return {
recipe: this.currentCraft.recipe,
progress: this.craftProgress,
timeRemaining: this.currentCraft.duration * (1 - this.craftProgress)
};
}
// Get queue length
getQueueLength() {
return this.craftingQueue.length;
}
// Clear entire queue
clearQueue() {
// Refund all queued items
this.craftingQueue.forEach(craft => {
const inventory = this.scene.inventorySystem;
for (const [itemId, amount] of Object.entries(craft.recipe.ingredients)) {
inventory.addItem(itemId, amount);
}
});
this.craftingQueue = [];
this.isCrafting = false;
this.currentCraft = null;
this.craftProgress = 0;
console.log('🗑️ Cleared crafting queue');
}
// Save crafting state
getSaveData() {
return {
unlockedRecipes: Array.from(this.unlockedRecipes),
craftingQueue: this.craftingQueue.map(craft => ({
recipeId: craft.recipeId,
startTime: craft.startTime,
duration: craft.duration
})),
currentProgress: this.craftProgress
};
}
// Load crafting state
loadSaveData(data) {
if (!data) return;
// Restore unlocked recipes
if (data.unlockedRecipes) {
this.unlockedRecipes = new Set(data.unlockedRecipes);
}
// Restore crafting queue
if (data.craftingQueue && data.craftingQueue.length > 0) {
this.craftingQueue = data.craftingQueue.map(saved => ({
recipeId: saved.recipeId,
recipe: this.recipes[saved.recipeId],
startTime: saved.startTime,
duration: saved.duration
}));
this.startNextCraft();
}
console.log('💾 Loaded crafting state');
}
}

View File

@@ -0,0 +1,386 @@
// Flat2DTerrainSystem - Complete 2D top-down tile rendering
// Replaces isometric TerrainSystem for Stardew Valley style
class Flat2DTerrainSystem {
constructor(scene) {
this.scene = scene;
this.tileSize = 48;
this.width = 100;
this.height = 100;
// Tile map data
this.tiles = [];
// Rendering containers
this.groundLayer = null;
this.pathsLayer = null;
this.decorLayer = null;
// Textures ready flag
this.texturesReady = false;
console.log('🎨 Flat2DTerrainSystem initialized');
}
async generate() {
console.log('🗺️ Generating flat 2D map...');
// Create textures first
this.createTileTextures();
// Load map data
if (typeof Map2DData !== 'undefined') {
this.tiles = Map2DData.generateMap();
console.log('✅ Map data generated:', this.tiles.length, 'rows');
} else {
console.error('❌ Map2DData not loaded!');
this.createFallbackMap();
}
// Render the map
this.renderMap();
console.log('✅ Flat 2D map ready!');
}
createTileTextures() {
const graphics = this.scene.make.graphics({ x: 0, y: 0, add: false });
const size = this.tileSize;
// GRASS - VIBRANT RICH GREEN! 🌿
graphics.clear();
graphics.fillStyle(0x59b36a); // BRIGHT rich green!
graphics.fillRect(0, 0, size, size);
// Add grass texture - DARKER spots
for (let i = 0; i < 15; i++) {
const x = Math.random() * size;
const y = Math.random() * size;
graphics.fillStyle(0x3a8d4f, 0.5);
graphics.fillCircle(x, y, 2 + Math.random() * 3);
}
// LIGHTER highlights
for (let i = 0; i < 10; i++) {
const x = Math.random() * size;
const y = Math.random() * size;
graphics.fillStyle(0x7ad389, 0.6);
graphics.fillCircle(x, y, 1.5);
}
graphics.generateTexture('tile2d_grass', size, size);
// GRASS WITH FLOWERS
graphics.clear();
graphics.fillStyle(0x4a9d5f);
graphics.fillRect(0, 0, size, size);
// Grass texture
for (let i = 0; i < 10; i++) {
graphics.fillStyle(0x3a8d4f, 0.4);
graphics.fillCircle(Math.random() * size, Math.random() * size, 1.5);
}
// Small flowers
const flowerColors = [0xff6b6b, 0xffd93d, 0x6bcbff];
for (let i = 0; i < 3; i++) {
graphics.fillStyle(flowerColors[Math.floor(Math.random() * 3)]);
graphics.fillCircle(Math.random() * size, Math.random() * size, 2);
}
graphics.generateTexture('tile2d_grass_flowers', size, size);
// DIRT - VIBRANT BROWN! 🟤
graphics.clear();
graphics.fillStyle(0xa87f5a); // BRIGHT brown!
graphics.fillRect(0, 0, size, size);
// Dirt texture - darker clumps
for (let i = 0; i < 20; i++) {
graphics.fillStyle(0x7a5f3a, 0.6);
graphics.fillCircle(Math.random() * size, Math.random() * size, 3 + Math.random() * 4);
}
// Lighter spots
for (let i = 0; i < 12; i++) {
graphics.fillStyle(0xc89f6f, 0.5);
graphics.fillCircle(Math.random() * size, Math.random() * size, 2);
}
graphics.generateTexture('tile2d_dirt', size, size);
// DIRT EDGE - Transition to grass
graphics.clear();
graphics.fillGradientStyle(0x8b6f47, 0x8b6f47, 0x6a9d5f, 0x6a9d5f, 1);
graphics.fillRect(0, 0, size, size);
graphics.generateTexture('tile2d_dirt_edge', size, size);
// WATER - BRIGHT BLUE! 💧
graphics.clear();
graphics.fillStyle(0x3498db); // VIBRANT blue!
graphics.fillRect(0, 0, size, size);
// Water highlights - darker depth
for (let i = 0; i < 8; i++) {
graphics.fillStyle(0x2078ab, 0.4);
graphics.fillCircle(Math.random() * size, Math.random() * size, 4 + Math.random() * 6);
}
// Light reflections
for (let i = 0; i < 12; i++) {
graphics.fillStyle(0x5dade2, 0.5);
graphics.fillCircle(Math.random() * size, Math.random() * size, 2);
}
// White sparkles
for (let i = 0; i < 10; i++) {
graphics.fillStyle(0xffffff, 0.6);
graphics.fillCircle(Math.random() * size, Math.random() * size, 1);
}
graphics.generateTexture('tile2d_water', size, size);
// WATER EDGE - Lighter border
graphics.clear();
graphics.fillGradientStyle(0x4aacdc, 0x4aacdc, 0x1a5f7a, 0x1a5f7a, 0.7);
graphics.fillRect(0, 0, size, size);
graphics.generateTexture('tile2d_water_edge', size, size);
// STONE - Gray
graphics.clear();
graphics.fillStyle(0x808080);
graphics.fillRect(0, 0, size, size);
for (let i = 0; i < 12; i++) {
graphics.fillStyle(0x606060, 0.6);
graphics.fillCircle(Math.random() * size, Math.random() * size, 2 + Math.random() * 3);
}
graphics.generateTexture('tile2d_stone', size, size);
graphics.destroy();
this.texturesReady = true;
console.log('✅ Tile textures created');
}
renderMap() {
// Create layer containers
this.groundLayer = this.scene.add.container(0, 0);
this.pathsLayer = this.scene.add.container(0, 0);
this.decorLayer = this.scene.add.container(0, 0);
// Set depths
this.groundLayer.setDepth(1);
this.pathsLayer.setDepth(2);
this.decorLayer.setDepth(3);
const size = this.tileSize;
// Render all tiles
for (let y = 0; y < this.height; y++) {
for (let x = 0; x < this.width; x++) {
const tile = this.tiles[y][x];
const worldX = x * size;
const worldY = y * size;
// Get texture key from tile type
const textureKey = this.getTileTexture(tile.base);
// Create tile sprite
const tileSprite = this.scene.add.image(worldX, worldY, textureKey);
tileSprite.setOrigin(0, 0);
tileSprite.setDisplaySize(size, size);
// Add to appropriate layer
if (tile.base <= 1) {
this.groundLayer.add(tileSprite);
} else {
this.pathsLayer.add(tileSprite);
}
// Add decoration if exists
if (tile.decoration) {
this.addDecoration(x, y, tile.decoration);
}
}
}
console.log('✅ Map rendered: 3 layers created');
}
getTileTexture(tileType) {
const types = Map2DData.tileTypes;
switch (tileType) {
case types.GRASS: return 'tile2d_grass';
case types.GRASS_FLOWERS: return 'tile2d_grass_flowers';
case types.DIRT: return 'tile2d_dirt';
case types.DIRT_EDGE: return 'tile2d_dirt_edge';
case types.WATER: return 'tile2d_water';
case types.WATER_EDGE: return 'tile2d_water_edge';
case types.STONE: return 'tile2d_stone';
default: return 'tile2d_grass';
}
}
addDecoration(gridX, gridY, decorType) {
const size = this.tileSize;
const worldX = gridX * size + size / 2;
const worldY = gridY * size + size / 2;
const types = Map2DData.tileTypes;
let sprite;
switch (decorType) {
case types.TREE:
sprite = this.createTree(worldX, worldY);
break;
case types.FLOWER_RED:
sprite = this.createFlower(worldX, worldY, 0xff6b6b);
break;
case types.FLOWER_YELLOW:
sprite = this.createFlower(worldX, worldY, 0xffd93d);
break;
case types.FLOWER_BLUE:
sprite = this.createFlower(worldX, worldY, 0x6bcbff);
break;
case types.LILY_PAD:
sprite = this.createLilyPad(worldX, worldY);
break;
case types.BUSH:
sprite = this.createBush(worldX, worldY);
break;
case 'puddle':
sprite = this.createPuddle(worldX, worldY);
break;
}
if (sprite) {
this.decorLayer.add(sprite);
}
}
createTree(x, y) {
const graphics = this.scene.add.graphics();
// Trunk
graphics.fillStyle(0x8B4513);
graphics.fillRect(x - 6, y, 12, 20);
// Crown (round)
graphics.fillStyle(0x2d5016, 0.9);
graphics.fillCircle(x, y - 10, 18);
graphics.fillStyle(0x3a6b1f, 0.8);
graphics.fillCircle(x - 5, y - 12, 14);
graphics.fillCircle(x + 5, y - 8, 12);
graphics.fillStyle(0x4a8d2f, 0.7);
graphics.fillCircle(x, y - 15, 10);
return graphics;
}
createFlower(x, y, color) {
const graphics = this.scene.add.graphics();
// Petals
graphics.fillStyle(color);
for (let i = 0; i < 5; i++) {
const angle = (Math.PI * 2 * i) / 5;
const px = x + Math.cos(angle) * 3;
const py = y + Math.sin(angle) * 3;
graphics.fillCircle(px, py, 2);
}
// Center
graphics.fillStyle(0xFFEB3B);
graphics.fillCircle(x, y, 2);
return graphics;
}
createLilyPad(x, y) {
const graphics = this.scene.add.graphics();
// Lily pad (green circle)
graphics.fillStyle(0x4a8d2f);
graphics.fillCircle(x, y, 8);
// Pink flower
graphics.fillStyle(0xFF69B4);
for (let i = 0; i < 5; i++) {
const angle = (Math.PI * 2 * i) / 5;
const px = x + Math.cos(angle) * 3;
const py = y + Math.sin(angle) * 3;
graphics.fillCircle(px, py, 2);
}
graphics.fillStyle(0xFFD700);
graphics.fillCircle(x, y, 1.5);
return graphics;
}
createBush(x, y) {
const graphics = this.scene.add.graphics();
graphics.fillStyle(0x3a6b1f, 0.9);
graphics.fillCircle(x, y, 10);
graphics.fillCircle(x - 6, y + 2, 8);
graphics.fillCircle(x + 6, y + 2, 8);
graphics.fillStyle(0x4a8d2f, 0.7);
graphics.fillCircle(x, y - 3, 6);
return graphics;
}
createPuddle(x, y) {
// Use existing puddle sprite if available
if (this.scene.textures.exists('luza_sprite')) {
const sprite = this.scene.add.image(x, y, 'luza_sprite');
sprite.setScale(0.8);
sprite.setAlpha(0.4);
return sprite;
}
// Fallback
const graphics = this.scene.add.graphics();
graphics.fillStyle(0x4488bb, 0.5);
graphics.fillEllipse(x, y, 12, 8);
return graphics;
}
createFallbackMap() {
// Create simple fallback if Map2DData fails
for (let y = 0; y < this.height; y++) {
this.tiles[y] = [];
for (let x = 0; x < this.width; x++) {
this.tiles[y][x] = {
base: 0, // Grass
decoration: null,
walkable: true
};
}
}
}
getTile(x, y) {
if (x < 0 || x >= this.width || y < 0 || y >= this.height) {
return null;
}
// Safety check: ensure tiles array is initialized
if (!this.tiles || !this.tiles[y]) {
return null;
}
return this.tiles[y][x];
}
isWalkable(x, y) {
const tile = this.getTile(x, y);
return tile ? tile.walkable : false;
}
update(time, delta) {
// Reserved for animations (water waves, etc)
}
destroy() {
if (this.groundLayer) this.groundLayer.destroy();
if (this.pathsLayer) this.pathsLayer.destroy();
if (this.decorLayer) this.decorLayer.destroy();
}
}

View File

@@ -112,4 +112,13 @@ class InventorySystem {
}
return total >= count;
}
/**
* Alias for addItem() - for simple crafting system compatibility
* @param {string} itemKey - Item type/key
* @param {number} quantity - Amount to add
*/
addItemToInventory(itemKey, quantity) {
return this.addItem(itemKey, quantity);
}
}

View File

@@ -38,13 +38,20 @@ const POND_RADIUS = 4; // Radij ribnika (8x8)
// Terrain Generator System
class TerrainSystem {
constructor(scene, width = 100, height = 100) {
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(Date.now());
this.noise = new PerlinNoise(this.hashCode(this.seed));
this.tiles = [];
this.decorations = [];
@@ -151,6 +158,33 @@ class TerrainSystem {
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.
@@ -175,34 +209,41 @@ class TerrainSystem {
createTileTextures() {
const tileWidth = 48;
const tileHeight = 60;
const P = 2; // PADDING (Margin) za preprečevanje črt
const P = 0; // NO PADDING - seamless tiles!
const types = Object.values(this.terrainTypes);
// POSEBNA OBDELAVA ZA VODO - 2D Stardew Valley Style!
// 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 });
// TEMNA MODRA VODA - dobro vidna!
// RICH BLUE WATER - Stardew Valley style gradient
waterGraphics.fillGradientStyle(
0x0a3d62, 0x0a3d62, // Temno modra (zgoraj)
0x1e5f8c, 0x1e5f8c // Srednje modra (spodaj)
0x1a5f7a, 0x1a5f7a, // Deep blue (top)
0x2a7fbc, 0x2a7fbc // Medium blue (bottom)
);
waterGraphics.fillRect(0, 0, 48, 48);
// Svetli highlights za valovanje
waterGraphics.fillStyle(0x3a8fc2, 0.5);
waterGraphics.fillCircle(12, 12, 10);
waterGraphics.fillCircle(36, 28, 8);
waterGraphics.fillCircle(24, 38, 6);
// 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);
// Temnejši border za kontrast
waterGraphics.lineStyle(2, 0x062a40, 1);
waterGraphics.strokeRect(0, 0, 48, 48);
// 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('🌊 2D Water texture created (Stardew Valley style)!');
console.log('🌊 Smooth pond water texture created (Stardew Valley style)!');
}
types.forEach((type) => {
@@ -231,8 +272,8 @@ class TerrainSystem {
graphics.lineTo(xs, midY);
graphics.closePath();
graphics.fill();
graphics.lineStyle(2, cLeft); // Overdraw
graphics.strokePath();
// graphics.lineStyle(2, cLeft); // NO STROKE!
// graphics.strokePath();
// Right Face
const cRight = 0x6B3410; // RJAVA DIRT - Right face (temnejša)
@@ -244,8 +285,8 @@ class TerrainSystem {
graphics.lineTo(midX, bottomY);
graphics.closePath();
graphics.fill();
graphics.lineStyle(2, cRight);
graphics.strokePath();
// graphics.lineStyle(2, cRight); // NO STROKE!
// graphics.strokePath();
// 2. ZGORNJA PLOSKEV (Top Face)
graphics.fillStyle(type.color);
@@ -256,32 +297,107 @@ class TerrainSystem {
graphics.lineTo(midX, bottomY);
graphics.closePath();
graphics.fill();
graphics.lineStyle(2, type.color); // Overdraw
graphics.strokePath();
// graphics.lineStyle(2, type.color); // NO STROKE!
// graphics.strokePath();
// Highlight
graphics.lineStyle(1, 0xffffff, 0.15);
graphics.beginPath();
graphics.moveTo(xs, midY);
graphics.lineTo(midX, topY);
graphics.lineTo(xe, midY);
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
// 3. DETAJLI - ENHANCED!
if (type.name.includes('grass')) {
graphics.fillStyle(Phaser.Display.Color.IntegerToColor(type.color).lighten(10).color);
// 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')) {
graphics.fillStyle(0x444444);
for (let i = 0; i < 6; i++) {
const rx = xs + 8 + Math.random() * 30;
// 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;
graphics.fillRect(rx, ry, 3, 3);
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();
}
}
@@ -304,90 +420,43 @@ class TerrainSystem {
createWaterFrames() {
const tileWidth = 48;
const tileHeight = 48;
const P = 2;
// Generiraj 4 frame-e za water animacijo
// 🌊 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 });
const xs = P;
const xe = 48 + P;
const midX = 24 + P;
const topY = P;
const midY = 12 + P;
const bottomY = 24 + P;
const depth = 14;
// 1. BASE WATER - Rich gradient
graphics.fillGradientStyle(
0x1a5f7a, 0x1a5f7a, // Deep blue
0x2a7fbc, 0x2a7fbc // Medium blue
);
graphics.fillRect(0, 0, tileWidth, tileHeight);
// 1. STRANICE (DARK BLUE - kot na sliki!)
// Left Face - temno modra
const cLeft = 0x0066aa;
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();
// 2. ANIMATED WAVE CIRCLES (moving highlights)
const waveOffset = frame * 3;
// Right Face - še temnejša modra
const cRight = 0x004488;
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();
// 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);
// 2. TOP SURFACE - SVETLO CYAN (kot na sliki!)
const waterColor = 0x33ccff;
graphics.fillStyle(waterColor);
graphics.beginPath();
graphics.moveTo(xs, midY);
graphics.lineTo(midX, topY);
graphics.lineTo(xe, midY);
graphics.lineTo(midX, bottomY);
graphics.closePath();
graphics.fill();
// 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. WAVE PATTERN
const offset = frame * 3;
graphics.lineStyle(1, 0x66ddff, 0.3);
// 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);
for (let i = 0; i < 3; i++) {
graphics.beginPath();
const baseY = topY + 6 + i * 5;
for (let px = xs; px <= xe; px += 2) {
const relativeX = px - xs;
const waveOffset = Math.sin((relativeX + offset + i * 10) * 0.15) * 1.5;
const py = baseY + waveOffset;
if (px === xs) graphics.moveTo(px, py);
else graphics.lineTo(px, py);
}
graphics.strokePath();
}
// 4. SPARKLE POINTS
graphics.fillStyle(0xffffff);
const sparkles = [
{ x: midX - 10 + (frame * 2) % 20, y: midY + 3 },
{ x: midX + 8 - (frame * 3) % 16, y: midY + 8 },
{ x: midX - 4 + Math.floor(frame * 1.5) % 8, y: midY + 13 }
];
sparkles.forEach(s => {
graphics.fillRect(s.x, s.y, 1, 1);
graphics.fillRect(s.x - 2, s.y, 1, 1);
graphics.fillRect(s.x + 2, s.y, 1, 1);
graphics.fillRect(s.x, s.y - 2, 1, 1);
graphics.fillRect(s.x, s.y + 2, 1, 1);
});
graphics.generateTexture(`water_frame_${frame}`, tileWidth + P * 2, tileHeight + P * 2);
graphics.generateTexture(`water_frame_${frame}`, tileWidth, tileHeight);
graphics.destroy();
}
console.log('🌊 Water frames created!');
console.log('🌊 Smooth animated pond water created!');
}
generate() {
@@ -468,6 +537,12 @@ class TerrainSystem {
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,
@@ -475,7 +550,8 @@ class TerrainSystem {
hasDecoration: false,
hasCrop: false,
solid: terrainType.solid || false,
isHouse: isHouse
isHouse: isHouse,
height: elevationHeight // 🏔️ NEW: Elevation data (0-5)
};
// Track valid positions for decorations (TREES!)
@@ -736,7 +812,7 @@ class TerrainSystem {
if (type === 'ruin') {
for (let y = 0; y < 6; y++) {
for (let x = 0; x < 6; x++) {
if (Math.random() > 0.6) this.addDecoration(gridX + x, gridY + y, 'fence');
// TEMP DISABLED: if (Math.random() > 0.6) this.addDecoration(gridX + x, gridY + y, 'fence');
this.setTile(gridX + x, gridY + y, 'stone');
}
}
@@ -751,7 +827,7 @@ class TerrainSystem {
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)) {
this.addDecoration(tx, ty, 'fence');
// TEMP DISABLED: this.addDecoration(tx, ty, 'fence');
}
}
}
@@ -770,7 +846,7 @@ class TerrainSystem {
const isCenter = (x === 2 || y === 2);
if (isCenter && Math.random() > 0.5) continue;
if (Math.random() > 0.3) {
this.addDecoration(tx, ty, 'fence');
// TEMP DISABLED: this.addDecoration(tx, ty, 'fence');
} else {
// User rocks in ruins
if (Math.random() > 0.5) {
@@ -970,25 +1046,48 @@ class TerrainSystem {
if (!this.visibleTiles.has(key)) {
const sprite = this.tilePool.get();
// Use water texture with animation support
// Use ANIMATED water frames (not static bubble texture!)
if (tile.type === 'water') {
sprite.setTexture('water');
sprite.setTexture('water_frame_0'); // Start with frame 0
sprite.isWater = true; // Mark for animation
// ANIMACIJA: Dodaj alpha tween za valovanje
this.scene.tweens.add({
targets: sprite,
alpha: 0.7,
duration: 1000,
yoyo: true,
repeat: -1,
ease: 'Sine.easeInOut'
});
// NO alpha tween - animation handles it
} else {
sprite.setTexture(tile.type);
}
const screenPos = this.iso.toScreen(x, y);
sprite.setPosition(Math.round(screenPos.x + this.offsetX), Math.round(screenPos.y + this.offsetY));
// 🌊 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);
}
@@ -1019,6 +1118,38 @@ class TerrainSystem {
// 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);
}
}
@@ -1166,11 +1297,136 @@ class TerrainSystem {
}
}
isSolid(x, y) {
if (x >= 0 && x < this.width && y >= 0 && y < this.height) {
return this.tiles[y][x].solid || false;
// 🏔️ 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;
}
return true; // Out of bounds = solid
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)