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:
344
src/systems/CraftingSystem.js
Normal file
344
src/systems/CraftingSystem.js
Normal 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');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user