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

380
src/ui/CraftingUI.js Normal file
View File

@@ -0,0 +1,380 @@
// Crafting UI - Visual panel for crafting interface
class CraftingUI {
constructor(scene) {
this.scene = scene;
this.craftingSystem = scene.craftingSystem;
this.isOpen = false;
this.currentCategory = 'all';
this.selectedRecipe = null;
// UI elements
this.container = null;
this.panel = null;
this.categoryButtons = [];
this.recipeList = [];
this.detailsPanel = null;
this.createUI();
this.hide();
// Listen for crafting events
this.setupEventListeners();
console.log('🎨 CraftingUI initialized');
}
createUI() {
const width = this.scene.cameras.main.width;
const height = this.scene.cameras.main.height;
// Main container
this.container = this.scene.add.container(0, 0);
this.container.setDepth(10000);
this.container.setScrollFactor(0);
// Semi-transparent background overlay
const overlay = this.scene.add.rectangle(0, 0, width, height, 0x000000, 0.7);
overlay.setOrigin(0);
overlay.setInteractive();
this.container.add(overlay);
// Main panel (centered)
const panelWidth = 700;
const panelHeight = 500;
const panelX = width / 2 - panelWidth / 2;
const panelY = height / 2 - panelHeight / 2;
this.panel = this.scene.add.rectangle(panelX, panelY, panelWidth, panelHeight, 0x2a1810);
this.panel.setOrigin(0);
this.panel.setStrokeStyle(3, 0x4a3820);
this.container.add(this.panel);
// Title
const title = this.scene.add.text(width / 2, panelY + 20, '🛠️ CRAFTING', {
fontSize: '32px',
fontFamily: 'Georgia, serif',
fill: '#f4e4c1',
stroke: '#2d1b00',
strokeThickness: 4
}).setOrigin(0.5);
this.container.add(title);
// Close button
const closeBtn = this.scene.add.text(panelX + panelWidth - 40, panelY + 20, '✖', {
fontSize: '24px',
fill: '#ff6666'
}).setOrigin(0.5);
closeBtn.setInteractive({ useHandCursor: true });
closeBtn.on('pointerdown', () => this.hide());
closeBtn.on('pointerover', () => closeBtn.setScale(1.2));
closeBtn.on('pointerout', () => closeBtn.setScale(1.0));
this.container.add(closeBtn);
// Category buttons (top)
this.createCategoryButtons(panelX, panelY + 60, panelWidth);
// Recipe list (left side)
this.createRecipeListPanel(panelX + 10, panelY + 120, 300, 350);
// Details panel (right side)
this.createDetailsPanel(panelX + 320, panelY + 120, 370, 350);
}
createCategoryButtons(x, y, width) {
const categories = this.craftingSystem.categories;
const buttonWidth = (width - 40) / categories.length;
categories.forEach((category, index) => {
const btnX = x + 20 + (index * buttonWidth);
const btn = this.scene.add.rectangle(btnX, y, buttonWidth - 10, 40, 0x4a3820);
btn.setOrigin(0, 0.5);
btn.setStrokeStyle(2, 0x6a5840);
btn.setInteractive({ useHandCursor: true });
const text = this.scene.add.text(btnX + buttonWidth / 2 - 5, y, `${category.icon} ${category.name}`, {
fontSize: '14px',
fontFamily: 'Arial',
fill: '#d4c4a1'
}).setOrigin(0.5);
btn.on('pointerdown', () => this.selectCategory(category.id));
btn.on('pointerover', () => {
btn.setFillStyle(0x6a5840);
text.setScale(1.05);
});
btn.on('pointerout', () => {
btn.setFillStyle(0x4a3820);
text.setScale(1.0);
});
this.container.add(btn);
this.container.add(text);
this.categoryButtons.push({ category: category.id, btn, text });
});
}
createRecipeListPanel(x, y, width, height) {
// Background
const bg = this.scene.add.rectangle(x, y, width, height, 0x1a1410);
bg.setOrigin(0);
bg.setStrokeStyle(2, 0x4a3820);
this.container.add(bg);
// Title
const title = this.scene.add.text(x + width / 2, y + 10, 'Recipes', {
fontSize: '18px',
fontFamily: 'Georgia, serif',
fill: '#f4e4c1'
}).setOrigin(0.5, 0);
this.container.add(title);
// Store panel bounds for recipe items
this.recipePanelBounds = { x, y: y + 40, width, height: height - 40 };
}
createDetailsPanel(x, y, width, height) {
// Background
const bg = this.scene.add.rectangle(x, y, width, height, 0x1a1410);
bg.setOrigin(0);
bg.setStrokeStyle(2, 0x4a3820);
this.container.add(bg);
this.detailsPanelBounds = { x, y, width, height };
}
selectCategory(categoryId) {
this.currentCategory = categoryId;
this.refreshRecipeList();
// Update button styles
this.categoryButtons.forEach(({ category, btn, text }) => {
if (category === categoryId) {
btn.setFillStyle(0x6a5840);
text.setStyle({ fill: '#ffffff' });
} else {
btn.setFillStyle(0x4a3820);
text.setStyle({ fill: '#d4c4a1' });
}
});
}
refreshRecipeList() {
// Clear existing recipe items
this.recipeList.forEach(item => item.destroy());
this.recipeList = [];
// Get recipes for current category
const recipes = this.craftingSystem.getUnlockedRecipes(this.currentCategory);
const { x, y, width } = this.recipePanelBounds;
const itemHeight = 50;
recipes.forEach((recipe, index) => {
const itemY = y + (index * itemHeight);
// Check if can craft
const canCraft = this.craftingSystem.canCraft(recipe.id);
// Background
const bg = this.scene.add.rectangle(x + 5, itemY, width - 10, itemHeight - 5, 0x2a1810);
bg.setOrigin(0);
bg.setStrokeStyle(1, canCraft.canCraft ? 0x4a9d5f : 0x6a5840);
bg.setInteractive({ useHandCursor: true });
// Recipe name
const name = this.scene.add.text(x + 15, itemY + itemHeight / 2, recipe.name, {
fontSize: '16px',
fontFamily: 'Arial',
fill: canCraft.canCraft ? '#ffffff' : '#888888'
}).setOrigin(0, 0.5);
// Hover effect
bg.on('pointerover', () => {
bg.setFillStyle(0x3a2820);
name.setScale(1.05);
});
bg.on('pointerout', () => {
bg.setFillStyle(0x2a1810);
name.setScale(1.0);
});
// Click to select
bg.on('pointerdown', () => this.selectRecipe(recipe));
this.container.add(bg);
this.container.add(name);
this.recipeList.push(bg, name);
});
}
selectRecipe(recipe) {
this.selectedRecipe = recipe;
this.showRecipeDetails();
}
showRecipeDetails() {
if (!this.selectedRecipe) return;
// Clear existing details
if (this.detailsContent) {
this.detailsContent.forEach(item => item.destroy());
}
this.detailsContent = [];
const { x, y, width } = this.detailsPanelBounds;
const recipe = this.selectedRecipe;
// Recipe name
const name = this.scene.add.text(x + width / 2, y + 20, recipe.name, {
fontSize: '24px',
fontFamily: 'Georgia, serif',
fill: '#f4e4c1',
stroke: '#2d1b00',
strokeThickness: 2
}).setOrigin(0.5, 0);
this.detailsContent.push(name);
// Description
const desc = this.scene.add.text(x + 20, y + 60, recipe.description, {
fontSize: '14px',
fontFamily: 'Arial',
fill: '#d4c4a1',
wordWrap: { width: width - 40 }
});
this.detailsContent.push(desc);
// Ingredients title
const ingredTitle = this.scene.add.text(x + 20, y + 120, 'Required Ingredients:', {
fontSize: '16px',
fontFamily: 'Arial',
fill: '#f4e4c1',
fontStyle: 'bold'
});
this.detailsContent.push(ingredTitle);
// Ingredients list
const inventory = this.scene.inventorySystem;
let ingredY = y + 150;
for (const [itemId, required] of Object.entries(recipe.ingredients)) {
const has = inventory ? inventory.getItemCount(itemId) : 0;
const hasEnough = has >= required;
const text = this.scene.add.text(x + 30, ingredY, `${itemId}: ${has}/${required}`, {
fontSize: '14px',
fontFamily: 'Arial',
fill: hasEnough ? '#4a9d5f' : '#ff6666'
});
this.detailsContent.push(text);
ingredY += 25;
}
// Result
const resultText = this.scene.add.text(x + 20, ingredY + 20, `Produces: ${recipe.result.quantity}x ${recipe.result.item}`, {
fontSize: '16px',
fontFamily: 'Arial',
fill: '#4aa3d0',
fontStyle: 'bold'
});
this.detailsContent.push(resultText);
// Craft button
const canCraft = this.craftingSystem.canCraft(recipe.id);
const btnY = y + 320;
const craftBtn = this.scene.add.rectangle(x + width / 2, btnY, 200, 40, canCraft.canCraft ? 0x4a9d5f : 0x666666);
craftBtn.setStrokeStyle(2, 0x000000);
const btnText = this.scene.add.text(x + width / 2, btnY, canCraft.canCraft ? '🔨 CRAFT' : '❌ Cannot Craft', {
fontSize: '18px',
fontFamily: 'Arial',
fill: '#ffffff',
fontStyle: 'bold'
}).setOrigin(0.5);
if (canCraft.canCraft) {
craftBtn.setInteractive({ useHandCursor: true });
craftBtn.on('pointerover', () => {
craftBtn.setFillStyle(0x5abd6f);
btnText.setScale(1.1);
});
craftBtn.on('pointerout', () => {
craftBtn.setFillStyle(0x4a9d5f);
btnText.setScale(1.0);
});
craftBtn.on('pointerdown', () => this.craftSelectedRecipe());
}
this.detailsContent.push(craftBtn, btnText);
// Add all to container
this.detailsContent.forEach(item => this.container.add(item));
}
craftSelectedRecipe() {
if (!this.selectedRecipe) return;
const success = this.craftingSystem.craftItem(this.selectedRecipe.id);
if (success) {
// Refresh UI
this.refreshRecipeList();
this.showRecipeDetails();
console.log(`✅ Crafting started: ${this.selectedRecipe.name}`);
}
}
setupEventListeners() {
// Listen for inventory changes to update recipe availability
this.scene.events.on('inventory-changed', () => {
if (this.isOpen) {
this.refreshRecipeList();
if (this.selectedRecipe) {
this.showRecipeDetails();
}
}
});
// Listen for craft completion
this.scene.events.on('craft-complete', (data) => {
if (this.isOpen) {
this.refreshRecipeList();
if (this.selectedRecipe) {
this.showRecipeDetails();
}
}
});
}
show() {
this.isOpen = true;
this.container.setVisible(true);
this.selectCategory(this.currentCategory);
console.log('🛠️ Crafting UI opened');
}
hide() {
this.isOpen = false;
this.container.setVisible(false);
console.log('🛠️ Crafting UI closed');
}
toggle() {
if (this.isOpen) {
this.hide();
} else {
this.show();
}
}
destroy() {
if (this.container) {
this.container.destroy();
}
}
}

265
src/ui/WeatherUI.js Normal file
View File

@@ -0,0 +1,265 @@
/**
* 🌦️ WEATHER CONTROL UI PANEL
* Separate UI for weather system controls
*/
class WeatherUI {
constructor(scene) {
this.scene = scene;
this.container = null;
this.isVisible = false;
this.createPanel();
this.hide(); // Start hidden
// Toggle with W key
this.scene.input.keyboard.on('keydown-W', () => {
this.toggle();
});
}
createPanel() {
const width = 300;
const height = 400;
const x = 20;
const y = 100;
// Main container
this.container = this.scene.add.container(x, y);
this.container.setDepth(900000); // High depth (below debug UI)
this.container.setScrollFactor(0);
// Background
const bg = this.scene.add.rectangle(0, 0, width, height, 0x222222, 0.9);
bg.setOrigin(0, 0);
bg.setStrokeStyle(2, 0x44aaff);
this.container.add(bg);
// Title
const title = this.scene.add.text(width / 2, 20, '🌦️ WEATHER CONTROL', {
fontSize: '20px',
fontFamily: 'Arial',
fontStyle: 'bold',
fill: '#44aaff'
});
title.setOrigin(0.5, 0);
this.container.add(title);
// Current Weather Display
const currentLabel = this.scene.add.text(20, 60, 'Current:', {
fontSize: '16px',
fill: '#ffffff'
});
this.container.add(currentLabel);
this.currentWeatherText = this.scene.add.text(width - 20, 60, 'Clear ☀️', {
fontSize: '16px',
fontStyle: 'bold',
fill: '#ffdd00'
});
this.currentWeatherText.setOrigin(1, 0);
this.container.add(this.currentWeatherText);
// Intensity Display
const intensityLabel = this.scene.add.text(20, 90, 'Intensity:', {
fontSize: '16px',
fill: '#ffffff'
});
this.container.add(intensityLabel);
this.intensityText = this.scene.add.text(width - 20, 90, '100%', {
fontSize: '16px',
fontStyle: 'bold',
fill: '#00ff88'
});
this.intensityText.setOrigin(1, 0);
this.container.add(this.intensityText);
// Intensity Slider Visual
const sliderBg = this.scene.add.rectangle(20, 120, width - 40, 10, 0x444444);
sliderBg.setOrigin(0, 0);
this.container.add(sliderBg);
this.intensityBar = this.scene.add.rectangle(20, 120, (width - 40) * 1.0, 10, 0x00ff88);
this.intensityBar.setOrigin(0, 0);
this.container.add(this.intensityBar);
// Auto Cycle Status
const autoLabel = this.scene.add.text(20, 150, 'Auto Cycle:', {
fontSize: '16px',
fill: '#ffffff'
});
this.container.add(autoLabel);
this.autoText = this.scene.add.text(width - 20, 150, 'OFF', {
fontSize: '16px',
fontStyle: 'bold',
fill: '#ff4444'
});
this.autoText.setOrigin(1, 0);
this.container.add(this.autoText);
// Weather Buttons
const buttonY = 190;
const buttonData = [
{ label: '☀️ Clear', weather: 'clear', color: 0xffdd00 },
{ label: '🌧️ Rain', weather: 'rain', color: 0x44aaff },
{ label: '❄️ Snow', weather: 'snow', color: 0xccccff },
{ label: '⚡ Storm', weather: 'storm', color: 0xff4444 },
{ label: '🌫️ Fog', weather: 'fog', color: 0x888888 }
];
buttonData.forEach((data, index) => {
const btn = this.createButton(
width / 2,
buttonY + (index * 35),
width - 40,
30,
data.label,
data.color,
() => this.scene.setWeather(data.weather)
);
this.container.add(btn);
});
// Controls Help
const helpY = buttonY + (buttonData.length * 35) + 10;
const help = this.scene.add.text(width / 2, helpY,
'CONTROLS:\n' +
'W = Toggle Panel\n' +
'Shift+A = Auto Cycle\n' +
'+/- = Intensity', {
fontSize: '12px',
fill: '#888888',
align: 'center'
});
help.setOrigin(0.5, 0);
this.container.add(help);
}
createButton(x, y, width, height, text, color, onClick) {
const container = this.scene.add.container(x, y);
const bg = this.scene.add.rectangle(0, 0, width, height, color, 0.8);
bg.setStrokeStyle(2, 0xffffff, 0.5);
bg.setInteractive({ useHandCursor: true });
const label = this.scene.add.text(0, 0, text, {
fontSize: '14px',
fontFamily: 'Arial',
fontStyle: 'bold',
fill: '#ffffff'
});
label.setOrigin(0.5, 0.5);
bg.on('pointerover', () => {
bg.setAlpha(1.0);
bg.setStrokeStyle(2, 0xffffff, 1.0);
});
bg.on('pointerout', () => {
bg.setAlpha(0.8);
bg.setStrokeStyle(2, 0xffffff, 0.5);
});
bg.on('pointerdown', () => {
this.scene.tweens.add({
targets: container,
scaleX: 0.95,
scaleY: 0.95,
duration: 100,
yoyo: true
});
onClick();
});
container.add(bg);
container.add(label);
return container;
}
update() {
if (!this.isVisible) return;
// Update current weather display
const weatherIcons = {
clear: '☀️',
rain: '🌧️',
snow: '❄️',
storm: '⚡',
fog: '🌫️'
};
const weatherColors = {
clear: '#ffdd00',
rain: '#44aaff',
snow: '#ccccff',
storm: '#ff4444',
fog: '#888888'
};
const currentWeather = this.scene.currentWeather || 'clear';
const icon = weatherIcons[currentWeather] || '☀️';
const color = weatherColors[currentWeather] || '#ffdd00';
this.currentWeatherText.setText(`${currentWeather.charAt(0).toUpperCase() + currentWeather.slice(1)} ${icon}`);
this.currentWeatherText.setColor(color);
// Update intensity
const intensity = this.scene.weatherIntensity || 1.0;
this.intensityText.setText(`${Math.round(intensity * 100)}%`);
this.intensityBar.setScale((intensity / 2.0), 1); // Max 2.0 = 100% width
// Update auto cycle status
const autoEnabled = this.scene.autoWeatherEnabled || false;
this.autoText.setText(autoEnabled ? 'ON' : 'OFF');
this.autoText.setColor(autoEnabled ? '#00ff88' : '#ff4444');
}
toggle() {
if (this.isVisible) {
this.hide();
} else {
this.show();
}
}
show() {
this.isVisible = true;
this.container.setVisible(true);
// Slide in animation
this.container.setAlpha(0);
this.container.x = -320;
this.scene.tweens.add({
targets: this.container,
x: 20,
alpha: 1,
duration: 300,
ease: 'Back.easeOut'
});
}
hide() {
this.isVisible = false;
// Slide out animation
this.scene.tweens.add({
targets: this.container,
x: -320,
alpha: 0,
duration: 300,
ease: 'Back.easeIn',
onComplete: () => {
this.container.setVisible(false);
}
});
}
destroy() {
if (this.container) {
this.container.destroy();
}
}
}