<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Combo Meal - Daily Culinary Puzzle</title> <!-- p5.js Library --> <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script> <!-- Supabase Library --> <script src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2"></script> <style> body { margin: 0; padding: 0; display: flex; justify-content: center; align-items: center; min-height: 100vh; background-color: #f5f2eb; font-family: Arial, sans-serif; } #game-container { position: relative; width: 100%; max-width: 1200px; margin: 0 auto; padding: 20px; } canvas { display: block; margin: 0 auto; } /* Loading screen styles */ #loading-screen { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: #f5f2eb; display: flex; justify-content: center; align-items: center; z-index: 1000; } .loading-spinner { border: 4px solid #f3f3f3; border-top: 4px solid #3498db; border-radius: 50%; width: 40px; height: 40px; animation: spin 1s linear infinite; margin-right: 10px; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } </style> </head> <body> <!-- Loading Screen --> <div id="loading-screen"> <div class="loading-spinner"></div> <p>Loading game...</p> </div> <div id="game-container"> <!-- Game will be rendered here by p5.js --> </div> <!-- Database Configuration --> <script> // Development mode flag - set to false for production const DEV_MODE = false; window.DEV_MODE = DEV_MODE; // Global variables for recipe data window.recipeData = { isLoaded: false, loadingMessage: "Loading today's recipe...", currentRecipe: null, currentCombinations: [], currentIngredients: [], intermediate_combinations: [], final_combination: null, ingredients: [], recipeUrl: "https://www.bonappetit.com/recipe/chicken-parm" }; // Supabase configuration const SUPABASE_URL = 'https://lfcvqhbvwxnwqxnqvlzs.supabase.co'; const SUPABASE_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImxmY3ZxaGJ2d3hud3F4bnF2bHpzIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MDk1OTI0NzcsImV4cCI6MjAyNTE2ODQ3N30.Nh83ebqzv7yZlCCEQxh_Gd1y9kGG0aNGCTqPQ6KKPW4'; // Initialize Supabase client with error handling let supabase; try { supabase = window.supabase.createClient(SUPABASE_URL, SUPABASE_KEY, { auth: { storage: window.localStorage, autoRefreshToken: true, persistSession: true, detectSessionInUrl: false }, realtime: { params: { eventsPerSecond: 2 } }, db: { schema: 'public' } }); console.log("Supabase client initialized successfully"); } catch (error) { console.error("Failed to initialize Supabase client:", error); supabase = null; window.recipeData.loadingMessage = "Failed to initialize database client"; } // Function to get the current date in EST/EDT timezone function getCurrentDateEST() { const date = new Date(); const estDate = new Date(date.toLocaleString('en-US', { timeZone: 'America/New_York' })); return estDate.toISOString().split('T')[0]; } // Function to update the loading message function updateLoadingMessage(message) { window.recipeData.loadingMessage = message; console.log("Loading message updated:", message); } // Rest of database.js code here // Development mode flag - set to true to skip Supabase and use mock data // IMPORTANT: Set this to false when deploying to production to use real Supabase data const DEV_MODE = true; // Set to true to use mock data during development window.DEV_MODE = DEV_MODE; // Make it accessible globally // Global variables for recipe data window.recipeData = { isLoaded: false, loadingMessage: "Loading today's recipe...", currentRecipe: null, currentCombinations: [], currentIngredients: [], intermediate_combinations: [], final_combination: null, ingredients: [], recipeUrl: "https://www.bonappetit.com/recipe/chicken-parm" }; // Supabase configuration const SUPABASE_URL = 'https://lfcvqhbvwxnwqxnqvlzs.supabase.co'; const SUPABASE_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImxmY3ZxaGJ2d3hud3F4bnF2bHpzIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MDk1OTI0NzcsImV4cCI6MjAyNTE2ODQ3N30.Nh83ebqzv7yZlCCEQxh_Gd1y9kGG0aNGCTqPQ6KKPW4'; // Function to get the current date in EST/EDT timezone function getCurrentDateEST() { const date = new Date(); const estDate = new Date(date.toLocaleString('en-US', { timeZone: 'America/New_York' })); return estDate.toISOString().split('T')[0]; } // Mock data for testing when Supabase is unavailable const MOCK_RECIPE = { rec_id: "mock-recipe-id", name: "Chicken Parm", date: "2023-03-04", recipe_url: "https://www.bonappetit.com/recipe/chicken-parm", description: "Classic Chicken Parmesan" }; const MOCK_COMBINATIONS = [ { combo_id: "mock-combo-1", rec_id: "mock-recipe-id", name: "Breaded Chicken Cutlet", is_final: false, is_egg: false }, { combo_id: "mock-combo-2", rec_id: "mock-recipe-id", name: "Tomato Sauce", is_final: false, is_egg: false }, { combo_id: "mock-combo-3", rec_id: "mock-recipe-id", name: "Mixed Cheeses", is_final: false, is_egg: false }, { combo_id: "mock-combo-4", rec_id: "mock-recipe-id", name: "Chicken Parm", is_final: true, is_egg: false } ]; const MOCK_INGREDIENTS = [ // Breaded Chicken Cutlet ingredients { ing_id: 1, combo_id: "mock-combo-1", name: "Chicken Cutlet", is_base: true }, { ing_id: 2, combo_id: "mock-combo-1", name: "Flour", is_base: true }, { ing_id: 3, combo_id: "mock-combo-1", name: "Eggs", is_base: true }, { ing_id: 4, combo_id: "mock-combo-1", name: "Panko Bread Crumbs", is_base: true }, // Tomato Sauce ingredients { ing_id: 5, combo_id: "mock-combo-2", name: "Garlic", is_base: true }, { ing_id: 6, combo_id: "mock-combo-2", name: "Red Chile Flakes", is_base: true }, { ing_id: 7, combo_id: "mock-combo-2", name: "Plum Tomatoes", is_base: true }, { ing_id: 8, combo_id: "mock-combo-2", name: "Basil", is_base: true }, // Mixed Cheeses ingredients { ing_id: 9, combo_id: "mock-combo-3", name: "Parmesan", is_base: true }, { ing_id: 10, combo_id: "mock-combo-3", name: "Mozzarella", is_base: true }, // Chicken Parm ingredients (combinations) { ing_id: 11, combo_id: "mock-combo-4", name: "Breaded Chicken Cutlet", is_base: false }, { ing_id: 12, combo_id: "mock-combo-4", name: "Tomato Sauce", is_base: false }, { ing_id: 13, combo_id: "mock-combo-4", name: "Mixed Cheeses", is_base: false } ]; // Function to load mock data when Supabase is unavailable function loadMockData() { if (DEV_MODE) { console.log("Loading mock data (development mode)"); updateLoadingMessage("Loading mock recipe data (dev mode)..."); } else { console.log("Loading mock data (fallback)"); updateLoadingMessage("Loading local recipe data..."); } window.recipeData.currentRecipe = MOCK_RECIPE; window.recipeData.currentCombinations = MOCK_COMBINATIONS; window.recipeData.currentIngredients = MOCK_INGREDIENTS; // Process the mock data updateLoadingMessage("Processing recipe data..."); processRecipeData(); window.recipeData.isLoaded = true; return true; } // Process the fetched data into the format needed by the game function processRecipeData() { const { currentRecipe, currentCombinations, currentIngredients } = window.recipeData; if (!currentRecipe || !currentCombinations || !currentIngredients) { console.error("Cannot process recipe data: data not loaded"); return; } // Clear existing game data window.recipeData.intermediate_combinations = []; // Find the final combination const finalCombo = currentCombinations.find(c => c.is_final); if (!finalCombo) { console.error("No final combination found in the data"); return; } // Get ingredients for the final combination const finalComboIngredients = currentIngredients .filter(i => i.combo_id === finalCombo.combo_id) .map(i => i.name); // Set the final combination window.recipeData.final_combination = { name: finalCombo.name, required: finalComboIngredients }; // Process intermediate combinations const intermediateCombos = currentCombinations.filter(c => !c.is_final); window.recipeData.intermediate_combinations = intermediateCombos.map(combo => { const comboIngredients = currentIngredients .filter(i => i.combo_id === combo.combo_id) .map(i => i.name); return { name: combo.name, required: comboIngredients }; }); // Extract all individual ingredients window.recipeData.ingredients = [...new Set(window.recipeData.intermediate_combinations.flatMap(c => c.required))]; // Set recipe URL if available if (currentRecipe.recipe_url) { window.recipeData.recipeUrl = currentRecipe.recipe_url; } console.log("Processed game data:"); console.log("- Final combination:", window.recipeData.final_combination); console.log("- Intermediate combinations:", window.recipeData.intermediate_combinations); console.log("- Base ingredients:", window.recipeData.ingredients); } // Fetch recipe data from Supabase based on current date async function fetchTodaysRecipe() { try { // Check if Supabase client is available if (!supabase) { console.error("Supabase client not initialized"); updateLoadingMessage("Using local recipe data"); return false; } const currentDate = getCurrentDateEST(); console.log("Fetching recipe for date:", currentDate); updateLoadingMessage("Connecting to database..."); // Add a timeout to the Supabase request const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Request timed out')), 10000) // Increased timeout to 10 seconds ); // Get recipe for today's date const recipePromise = supabase .from('recipes') .select('*') .eq('date', currentDate) .order('day_number', { ascending: true }) .limit(1) .single(); console.log("Sending request to Supabase..."); // Race the Supabase request against the timeout const { data: recipeData, error: recipeError } = await Promise.race([ recipePromise, timeoutPromise ]).catch(err => { console.error("Error during Supabase request:", err); return { data: null, error: err }; }); if (recipeError) { console.error("Recipe fetch error:", recipeError); updateLoadingMessage("Using local recipe data"); return false; } if (!recipeData) { console.error("No recipe data returned"); updateLoadingMessage("Using local recipe data"); return false; } window.recipeData.currentRecipe = recipeData; console.log("Recipe found:", window.recipeData.currentRecipe); updateLoadingMessage("Recipe found, loading combinations..."); // Get all combinations for this recipe const { data: combinationsData, error: combosError } = await supabase .from('combinations') .select('*') .eq('rec_id', window.recipeData.currentRecipe.rec_id) .order('is_final', { ascending: false }) .catch(err => { // Silently catch errors and return an empty result return { data: null, error: err }; }); if (combosError || !combinationsData) { // Silently fail and use mock data updateLoadingMessage("Using local recipe data"); return false; } window.recipeData.currentCombinations = combinationsData; console.log("Combinations found:", window.recipeData.currentCombinations); updateLoadingMessage("Combinations found, loading ingredients..."); // Get all ingredients for these combinations const { data: ingredientsData, error: ingredientsError } = await supabase .from('ingredients') .select('*') .in('combo_id', window.recipeData.currentCombinations.map(c => c.combo_id)) .catch(err => { // Silently catch errors and return an empty result return { data: null, error: err }; }); if (ingredientsError || !ingredientsData) { // Silently fail and use mock data updateLoadingMessage("Using local recipe data"); return false; } window.recipeData.currentIngredients = ingredientsData; console.log("Ingredients found:", window.recipeData.currentIngredients); updateLoadingMessage("Processing recipe data..."); // Process the data into the format needed by the game processRecipeData(); window.recipeData.isLoaded = true; return true; } catch (error) { // Silently fail and use mock data updateLoadingMessage("Using local recipe data"); return false; } } // Initialize data loading async function initializeGameData() { try { // If in development mode, skip Supabase and use mock data if (DEV_MODE) { console.log("Development mode active, using mock data"); updateLoadingMessage("Development mode: using local recipe data"); return loadMockData(); } // Check if we're running locally (file:// protocol or localhost) const isLocalEnvironment = window.location.protocol === 'file:' || window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'; if (isLocalEnvironment) { console.log("Running in local environment, using mock data"); updateLoadingMessage("Local environment detected, using local recipe data"); return loadMockData(); } const success = await fetchTodaysRecipe(); if (success) { console.log("Successfully loaded recipe data from Supabase"); updateLoadingMessage("Recipe loaded successfully!"); return true; } else { console.log("Failed to load recipe data from Supabase. Using mock data."); updateLoadingMessage("Using local recipe data"); return loadMockData(); } } catch (error) { console.log("Error initializing game data, using mock data"); updateLoadingMessage("Using local recipe data"); return loadMockData(); } } // Hide loading screen when game is ready window.addEventListener('load', function() { // Initialize game data initializeGameData().then(() => { document.getElementById('loading-screen').style.display = 'none'; }); }); </script> <!-- Game Logic --> <script> // Game code from sketch.js here // Define intermediate combinations let intermediate_combinations = [ { name: "Fried Chicken Cutlet", required: ["Chicken Cutlet", "Flour", "Eggs", "Panko Bread Crumbs"] }, { name: "Tomato Sauce", required: ["Garlic", "Red Chile Flakes", "Plum Tomatoes", "Basil"] }, { name: "Mixed Cheeses", required: ["Parmesan", "Mozzarella"] } ]; // Define the final combination let final_combination = { name: "Chicken Parm", required: ["Fried Chicken Cutlet", "Tomato Sauce", "Mixed Cheeses"] }; // Extract all individual ingredients let ingredients = [...new Set(intermediate_combinations.flatMap(c => c.required))]; // Global variables let vessels = []; let draggedVessel = null; let offsetX, offsetY; let gameWon = false; let turnCounter = 0; let moveHistory = []; // Array to store move history with colors let animations = []; // Array to store active animations let titleFont; let recipeUrl = "https://www.bonappetit.com/recipe/chicken-parm"; let hintButton; let hintVessel = null; let showingHint = false; let gameStarted = false; // New variable to track game state let startButton; // New button for start screen let hintButtonY; let dataLoading = true; // Flag to track if data is still loading // loadingMessage is now defined in database.js // Add these variables at the top with the other global variables let demoIngredients = [ { name: "Bread", x: 0, y: 0, color: 'white', combined: false }, { name: "Grapes", x: 0, y: 0, color: 'white', combined: false }, { name: "Peanuts", x: 0, y: 0, color: 'white', combined: false }, { name: "Sugar", x: 0, y: 0, color: 'white', combined: false }, { name: "Salt", x: 0, y: 0, color: 'white', combined: false } ]; let demoSteps = [ { ingredients: ["Peanuts", "Salt"], result: "Peanut Butter", color: 'yellow', completed: false, timer: 0 }, { ingredients: ["Grapes", "Sugar"], result: "Jelly", color: 'yellow', completed: false, timer: 0 }, { ingredients: ["Bread", "Jelly"], result: "Bread & Jelly", color: 'yellow', completed: false, timer: 0 }, { ingredients: ["Peanut Butter", "Bread & Jelly"], result: "PB&J", color: 'green', completed: false, timer: 0 } ]; let demoAnimations = []; let currentDemoStep = 0; let demoStepDelay = 90; // Faster animation between demo steps let demoTimer = 0; // Animation class for combining ingredients class CombineAnimation { constructor(x, y, color, targetX, targetY) { this.x = x; this.y = y; this.targetX = targetX; this.targetY = targetY; this.color = color; this.size = 30; this.alpha = 255; this.progress = 0; this.duration = 30; // frames this.sparkles = []; // Create sparkles for (let i = 0; i < 15; i++) { this.sparkles.push({ x: random(-20, 20), y: random(-20, 20), size: random(3, 8), speed: random(0.5, 2), angle: random(TWO_PI) }); } } update() { this.progress += 1 / this.duration; if (this.progress >= 1) { return true; // Animation complete } // Update sparkles for (let sparkle of this.sparkles) { sparkle.x += cos(sparkle.angle) * sparkle.speed; sparkle.y += sin(sparkle.angle) * sparkle.speed; sparkle.size *= 0.95; } return false; } draw() { // Easing function for smooth animation let t = this.progress; let easedT = t < 0.5 ? 4 * t * t * t : 1 - pow(-2 * t + 2, 3) / 2; // Calculate current position let currentX = lerp(this.x, this.targetX, easedT); let currentY = lerp(this.y, this.targetY, easedT); // Draw main particle noStroke(); fill(this.color); ellipse(currentX, currentY, this.size * (1 - this.progress * 0.5)); // Draw sparkles for (let sparkle of this.sparkles) { fill(this.color); ellipse(currentX + sparkle.x, currentY + sparkle.y, sparkle.size); } } } // Button class for UI elements class Button { constructor(x, y, w, h, label, action, color = '#4285F4', textColor = 'white') { this.x = x; this.y = y; this.w = w; this.h = h; this.label = label; this.action = action; this.color = color; this.textColor = textColor; this.hovered = false; } draw() { // Draw button rectMode(CENTER); if (this.hovered) { fill(lerpColor(color(this.color), color(255), 0.2)); } else { fill(this.color); } stroke(0, 50); strokeWeight(3); // Increased line width for cartoony look rect(this.x, this.y, this.w, this.h, 8); // Draw label fill(this.textColor); noStroke(); textAlign(CENTER, CENTER); textSize(16); text(this.label, this.x, this.y); } isInside(x, y) { return x > this.x - this.w/2 && x < this.x + this.w/2 && y > this.y - this.h/2 && y < this.y + this.h/2; } checkHover(x, y) { this.hovered = this.isInside(x, y); } handleClick() { if (this.hovered) { this.action(); return true; } return false; } } // Hint Vessel class - extends Vessel with hint-specific functionality class HintVessel { constructor(combo) { this.combo = combo; this.name = combo.name; this.required = [...combo.required]; this.collected = []; this.x = width / 2; this.y = hintButtonY; // Use the fixed Y position this.w = 200; this.h = 100; this.color = '#FF5252'; // Red color this.scale = 1; this.targetScale = 1; } update() { // Scale animation this.scale = lerp(this.scale, this.targetScale, 0.2); } draw() { push(); translate(this.x, this.y); scale(this.scale); // Draw pot handles (small circles) BEHIND the main shape fill('#888888'); stroke('black'); strokeWeight(3); // Position handles slightly lower and half-overlapping with the main shape // Move handles a bit past the edge of the pot circle(-this.w * 0.4, -this.h * 0.15, this.h * 0.2); circle(this.w * 0.4, -this.h * 0.15, this.h * 0.2); // Draw vessel (pot body) fill(this.color); stroke('black'); strokeWeight(3); // Draw pot body (3:2 rectangle with rounded corners ONLY at the bottom) rectMode(CENTER); rect(0, 0, this.w * 0.8, this.h * 0.6, 0, 0, 10, 10); // Draw combo name fill('white'); noStroke(); textAlign(CENTER, CENTER); textSize(14); text(this.name, 0, -this.h * 0.1); // Draw progress indicator textSize(16); text(`${this.collected.length}/${this.required.length}`, 0, this.h * 0.1); pop(); } isInside(x, y) { return x > this.x - this.w / 2 && x < this.x + this.w / 2 && y > this.y - this.h / 2 && y < this.y + this.h / 2; } addIngredient(ingredient) { if (this.required.includes(ingredient) && !this.collected.includes(ingredient)) { this.collected.push(ingredient); this.pulse(); return true; } return false; } isComplete() { return this.collected.length === this.required.length && this.required.every(ing => this.collected.includes(ing)); } pulse() { this.targetScale = 1.2; setTimeout(() => { this.targetScale = 1; }, 300); } // Convert to a regular vessel when complete but keep it red toVessel() { let v = new Vessel([], [], this.name, '#FF5252', this.x, this.y, 200, 100); v.isAdvanced = true; // Mark as advanced for proper rendering v.pulse(); return v; } } class Vessel { constructor(ingredients, complete_combinations, name, color, x, y, w, h) { this.ingredients = ingredients; this.complete_combinations = complete_combinations; this.name = name; this.color = color; this.x = x; this.y = y; this.w = w; this.h = h; this.originalX = x; this.originalY = y; this.isAdvanced = color !== 'white'; // Yellow or green vessels are advanced this.scale = 1; // For animation this.targetScale = 1; } getDisplayText() { if (this.name != null) return this.name; else if (this.ingredients.length > 0) return this.ingredients.join(" + "); else return this.complete_combinations.join(" + "); } isInside(x, y) { return x > this.x - this.w / 2 && x < this.x + this.w / 2 && y > this.y - this.h / 2 && y < this.y + this.h / 2; } snapBack() { this.x = this.originalX; this.y = this.originalY; } update() { // Scale animation only (removed floating animation) this.scale = lerp(this.scale, this.targetScale, 0.2); } draw() { push(); translate(this.x, this.y); scale(this.scale); if (this.isAdvanced) { // Advanced vessel (pot or pan based on color) if (this.color === '#FF5252') { // Red vessel (pot with two handles) // Draw handles BEHIND the main shape fill('#888888'); stroke('black'); strokeWeight(3); // Position handles slightly lower and half-overlapping with the main shape // Move handles a bit past the edge of the pot circle(-this.w * 0.4, -this.h * 0.15, this.h * 0.2); circle(this.w * 0.4, -this.h * 0.15, this.h * 0.2); } else if (this.color === 'green') { // Green vessel (pan with long handle) // Draw handle BEHIND the main shape fill('#888888'); stroke('black'); strokeWeight(3); rectMode(CENTER); // Draw handle as thin horizontal rectangle rect(this.w * 0.6, 0, this.w * 0.5, this.h * 0.15, 5); } else if (this.color === 'yellow') { // Yellow vessel (pot with two handles like the red vessel) // Draw handles BEHIND the main shape fill('#888888'); stroke('black'); strokeWeight(3); // Position handles slightly lower and half-overlapping with the main shape // Move handles a bit past the edge of the pot circle(-this.w * 0.4, -this.h * 0.15, this.h * 0.2); circle(this.w * 0.4, -this.h * 0.15, this.h * 0.2); } // Draw vessel body fill(this.color); stroke('black'); strokeWeight(3); // Draw vessel body (3:2 rectangle with rounded corners ONLY at the bottom) rectMode(CENTER); rect(0, 0, this.w * 0.8, this.h * 0.6, 0, 0, 10, 10); } else { // Basic ingredient vessel (rectangle with extremely rounded bottom corners) fill(this.color); stroke('black'); strokeWeight(3); // Draw rounded rectangle rectMode(CENTER); rect(0, -this.h * 0.1, this.w * 0.8, this.h * 0.6, 5, 5, 30, 30); } // Draw text inside the vessel fill('black'); noStroke(); textAlign(CENTER, CENTER); textSize(12); // Calculate text position to be inside the vessel let displayText = this.getDisplayText(); let lines = splitTextIntoLines(displayText, this.w * 0.7); for (let i = 0; i < lines.length; i++) { let yOffset = (i - (lines.length - 1) / 2) * 15; // Position text slightly higher in the vessel text(lines[i], 0, yOffset - this.h * 0.1); } pop(); } pulse() { this.targetScale = 1.2; setTimeout(() => { this.targetScale = 1; }, 300); } } // Helper function to split text into lines that fit within a width function splitTextIntoLines(text, maxWidth) { let words = text.split(' '); let lines = []; let currentLine = words[0]; for (let i = 1; i < words.length; i++) { let testLine = currentLine + ' ' + words[i]; let testWidth = textWidth(testLine); if (testWidth > maxWidth) { lines.push(currentLine); currentLine = words[i]; } else { currentLine = testLine; } } lines.push(currentLine); return lines; } function preload() { // Load fonts or other assets titleFont = loadFont('https://cdnjs.cloudflare.com/ajax/libs/topcoat/0.8.0/font/SourceSansPro-Bold.otf'); } function setup() { createCanvas(windowWidth, windowHeight); // Fullscreen canvas for mobile textFont('Arial'); // Initialize game data from Supabase initializeGameData().then(success => { dataLoading = false; if (success) { console.log("Successfully loaded recipe data from Supabase"); } else { console.log("Using default recipe data"); } // Set up the game with the loaded or default recipe data setupGameElements(); }).catch(error => { console.error("Error initializing game data:", error); dataLoading = false; // Continue with default recipe data setupGameElements(); }); } // New function to set up game elements after data is loaded function setupGameElements() { // Use the data from window.recipeData if available if (window.recipeData && window.recipeData.isLoaded) { intermediate_combinations = window.recipeData.intermediate_combinations; final_combination = window.recipeData.final_combination; ingredients = window.recipeData.ingredients; recipeUrl = window.recipeData.recipeUrl; // Check if we're in development mode if (window.DEV_MODE) { console.log("Using mock recipe data (development mode)"); } else { console.log("Using recipe data from Supabase"); } } else { console.log("Using default recipe data (fallback)"); } let original_w = 100; let original_h = 80; let margin = 10; // Reduced margin for compact UI // Clear any existing vessels vessels = []; // Create vessels for each ingredient ingredients.forEach((ing) => { let v = new Vessel([ing], [], null, 'white', 0, 0, original_w, original_h); vessels.push(v); }); // Randomize the order of vessels shuffleArray(vessels); // Initial arrangement of vessels arrangeVessels(); // Calculate the lowest vessel position let lowestY = 0; vessels.forEach(v => { lowestY = Math.max(lowestY, v.y + v.h/2); }); // Set fixed hint button position 40 pixels below the lowest vessel hintButtonY = lowestY + 40; // Create share and recipe buttons shareButton = new Button(width * 0.35, height * 0.65, 120, 40, "Share Score", shareScore); recipeButton = new Button(width * 0.65, height * 0.65, 120, 40, "View Recipe", viewRecipe); // Create hint button with white background and grey outline, using the fixed Y position hintButton = new Button(width * 0.5, hintButtonY, 120, 40, "Hint", showHint, 'white', '#FF5252'); // Create start button startButton = new Button(width * 0.5, height * 0.7, 180, 60, "Start Cooking!", startGame, '#4CAF50', 'white'); // Position demo ingredients arrangeDemoIngredients(); } // Fisher-Yates shuffle algorithm to randomize vessel order function shuffleArray(array) { for (let i = array.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [array[i], array[j]] = [array[j], array[i]]; } return array; } function arrangeVessels() { let basic_w = 100; let advanced_w = 200; // Double width for advanced vessels let basic_h = 80; let advanced_h = 100; // Slightly taller for advanced vessels let margin = 15; // Space between vessels let vertical_margin = 8; // Vertical spacing between rows // First, separate vessels into advanced and basic let advancedVessels = vessels.filter(v => v.isAdvanced); let basicVessels = vessels.filter(v => !v.isAdvanced); // Calculate total slots needed let totalSlots = advancedVessels.length * 2 + basicVessels.length; let slotsPerRow = 3; // Maximum slots per row let rows = Math.ceil(totalSlots / slotsPerRow); // Calculate maximum height needed for vessels let totalHeight = rows * advanced_h + (rows - 1) * vertical_margin; // Ensure we leave space at the bottom for the hint button let startY = Math.min(180, 180 + (400 - totalHeight) / 2); // Create an array to hold our row arrangements let rowArrangements = []; let currentRow = []; let currentSlots = 0; // First, try to pair advanced vessels with basic vessels when possible while (advancedVessels.length > 0 || basicVessels.length > 0) { // Reset/create new row if current row is full if (currentSlots >= slotsPerRow) { rowArrangements.push(currentRow); currentRow = []; currentSlots = 0; } // If we can fit an advanced vessel (2 slots) and have one available if (currentSlots + 2 <= slotsPerRow && advancedVessels.length > 0) { // Randomly decide whether to add the advanced vessel now or wait if (Math.random() < 0.5 || basicVessels.length === 0) { currentRow.push(advancedVessels.shift()); currentSlots += 2; // If we have exactly one slot left and a basic vessel, add it if (currentSlots === 2 && basicVessels.length > 0) { currentRow.push(basicVessels.shift()); currentSlots += 1; } } else { // Add basic vessels first while (currentSlots + 1 <= slotsPerRow && basicVessels.length > 0 && (currentSlots + 3 <= slotsPerRow || advancedVessels.length === 0)) { currentRow.push(basicVessels.shift()); currentSlots += 1; } } } // If we can't fit an advanced vessel but can fit a basic one else if (currentSlots + 1 <= slotsPerRow && basicVessels.length > 0) { currentRow.push(basicVessels.shift()); currentSlots += 1; } // If we can't fit either type but still have vessels, start a new row else if (currentRow.length > 0) { rowArrangements.push(currentRow); currentRow = []; currentSlots = 0; } } // Add the last row if it has any vessels if (currentRow.length > 0) { rowArrangements.push(currentRow); } // Position all vessels based on row arrangements rowArrangements.forEach((row, rowIndex) => { // Calculate total width of this row let rowWidth = row.reduce((width, v) => { return width + (v.isAdvanced ? advanced_w : basic_w); }, 0) + (row.length - 1) * margin; // Calculate starting x position to center the row let startX = (width - rowWidth) / 2; let currentX = startX; // Position each vessel in the row row.forEach((v) => { // Update vessel dimensions if (v.isAdvanced) { v.w = advanced_w; v.h = advanced_h; } else { v.w = basic_w; v.h = basic_h; } // Set vessel position v.x = currentX + v.w / 2; v.y = startY + rowIndex * (advanced_h + vertical_margin); v.originalX = v.x; v.originalY = v.y; // Move x position for next vessel currentX += v.w + margin; }); }); } function draw() { background(245, 242, 235); // Cream background // Show loading screen if data is still loading if (window.recipeData && !window.recipeData.isLoaded) { drawLoadingScreen(); return; } // Draw title drawTitle(); if (!gameStarted) { // Draw start screen with animated demo drawStartScreen(); updateDemo(); } else if (gameWon) { // Draw win screen drawWinScreen(); } else { // Draw game screen // Update all vessels vessels.forEach(v => { v.update(); }); // Sort vessels by type to ensure advanced vessels are drawn first (behind basic vessels) let sortedVessels = [...vessels].sort((a, b) => { if (a.isAdvanced && !b.isAdvanced) return -1; if (!a.isAdvanced && b.isAdvanced) return 1; return 0; }); // Draw all vessels in sorted order sortedVessels.forEach(v => { v.draw(); }); // Draw hint button or hint vessel if (showingHint && hintVessel) { hintVessel.update(); hintVessel.draw(); } else { hintButton.draw(); } // Draw animations for (let i = animations.length - 1; i >= 0; i--) { animations[i].draw(); if (animations[i].update()) { animations.splice(i, 1); } } // Draw turn counter textAlign(LEFT, BOTTOM); textSize(16); fill('#333'); text("Turns: " + turnCounter, 20, height - 20); // Draw move history drawMoveHistory(); } // Update cursor if hovering over a vessel or button updateCursor(); } function drawTitle() { // Draw game title textAlign(CENTER, CENTER); fill('#333'); textSize(40); textFont(titleFont); text("COMBO MEAL", width/2, 60); // Draw byline textSize(18); textFont('Arial'); fill('#666'); text("Combine the ingredients to discover the dish!", width/2, 100); } function drawStartScreen() { // Draw demo ingredients and combinations drawDemoIngredients(); // Draw demo animations for (let i = demoAnimations.length - 1; i >= 0; i--) { demoAnimations[i].draw(); if (demoAnimations[i].update()) { demoAnimations.splice(i, 1); } } // Draw instructions textAlign(CENTER); textSize(18); fill('#333'); text("Combine ingredients into culinary combos to discover the dish", width/2, height * 0.55); text("in the fewest turns. New recipe daily!", width/2, height * 0.55 + 30); // Draw start button startButton.draw(); startButton.checkHover(mouseX, mouseY); } function arrangeDemoIngredients() { // Position the demo ingredients in a more compact layout const centerY = height * 0.35; const topRowY = centerY - 50; const bottomRowY = centerY; // First row: 3 ingredients demoIngredients[0].x = width * 0.25; // Bread demoIngredients[0].y = topRowY; demoIngredients[1].x = width * 0.5; // Grapes demoIngredients[1].y = topRowY; demoIngredients[2].x = width * 0.75; // Peanuts demoIngredients[2].y = topRowY; // Second row: 2 ingredients demoIngredients[3].x = width * 0.35; // Sugar demoIngredients[3].y = bottomRowY; demoIngredients[4].x = width * 0.65; // Salt demoIngredients[4].y = bottomRowY; // Position for intermediate results - keep everything more compact demoSteps[0].x = width * 0.75; // Peanut Butter (stays on right) demoSteps[0].y = centerY + 50; demoSteps[1].x = width * 0.4; // Jelly (stays on left) demoSteps[1].y = centerY + 50; demoSteps[2].x = width * 0.4; // Bread & Jelly (replaces Jelly position) demoSteps[2].y = centerY + 50; demoSteps[3].x = width * 0.5; // Final PB&J (centered) demoSteps[3].y = centerY + 80; } function drawDemoIngredients() { // Draw each demo ingredient for (let ing of demoIngredients) { if (!ing.combined) { drawDemoVessel(ing.x, ing.y, ing.name, ing.color, false); } } // Draw completed combinations for (let step of demoSteps) { if (step.completed) { drawDemoVessel(step.x, step.y, step.result, step.color, true); } } } function drawDemoVessel(x, y, name, vesselColor, isAdvanced) { push(); translate(x, y); if (!isAdvanced) { // Basic ingredient vessel fill(vesselColor); stroke('black'); strokeWeight(3); // Draw rounded rectangle rectMode(CENTER); rect(0, -10, 80, 50, 5, 5, 30, 30); // Draw text fill('black'); noStroke(); textAlign(CENTER, CENTER); textSize(12); text(name, 0, -10); } else { // Advanced vessel if (vesselColor === 'green') { // Green vessel (pan with long handle) fill('#888888'); stroke('black'); strokeWeight(3); rectMode(CENTER); rect(80, 0, 80, 15, 5); // Draw vessel body - make it larger for the final result fill(vesselColor); stroke('black'); strokeWeight(3); // Draw rectangle with rounded corners ONLY at the bottom rectMode(CENTER); rect(0, 0, 140, 70, 0, 0, 10, 10); } else { // Yellow vessel (pot with two handles) fill('#888888'); stroke('black'); strokeWeight(3); circle(-60, -5, 20); circle(60, -5, 20); // Draw vessel body fill(vesselColor); stroke('black'); strokeWeight(3); // Draw rectangle with rounded corners ONLY at the bottom rectMode(CENTER); rect(0, 0, 120, 60, 0, 0, 10, 10); } // Draw text fill('black'); noStroke(); textAlign(CENTER, CENTER); textSize(14); text(name, 0, -10); } pop(); } function updateDemo() { demoTimer++; // Process the current demo step if (currentDemoStep < demoSteps.length) { let step = demoSteps[currentDemoStep]; if (!step.completed) { step.timer++; // When it's time to perform this step if (step.timer >= demoStepDelay) { // Special handling for the Bread & Jelly step if (step.result === "Bread & Jelly") { // Find Bread and animate it moving to Jelly let bread = demoIngredients.find(i => i.name === "Bread"); let jellyStep = demoSteps.find(s => s.result === "Jelly"); if (bread && !bread.combined && jellyStep && jellyStep.completed) { bread.combined = true; // Create animation from Bread to Jelly position createDemoAnimation(bread.x, bread.y, bread.color, jellyStep.x, jellyStep.y); // Mark this step as completed step.completed = true; // Move to the next step currentDemoStep++; } } // Special handling for the final PB&J step else if (step.result === "PB&J") { // Find Peanut Butter and animate it moving to Bread & Jelly let pbStep = demoSteps.find(s => s.result === "Peanut Butter"); let bjStep = demoSteps.find(s => s.result === "Bread & Jelly"); if (pbStep && pbStep.completed && bjStep && bjStep.completed) { // Create animation from Peanut Butter to Bread & Jelly position createDemoAnimation(pbStep.x, pbStep.y, pbStep.color, bjStep.x, bjStep.y); // Mark this step as completed step.completed = true; // Move to the next step currentDemoStep++; // If we've completed all steps, reset the demo after a delay if (currentDemoStep >= demoSteps.length) { setTimeout(resetDemo, 3000); } } } // Normal handling for other steps else { // Mark the ingredients as combined for (let ingName of step.ingredients) { // Check if it's a basic ingredient let basicIng = demoIngredients.find(i => i.name === ingName && !i.combined); if (basicIng) { basicIng.combined = true; // Create animation from this ingredient to the result position createDemoAnimation(basicIng.x, basicIng.y, basicIng.color, step.x, step.y); } } // Mark this step as completed step.completed = true; // Move to the next step currentDemoStep++; } } } } } function createDemoAnimation(startX, startY, color, targetX, targetY) { for (let i = 0; i < 5; i++) { demoAnimations.push(new CombineAnimation(startX, startY, color, targetX, targetY)); } } function resetDemo() { // Reset all demo ingredients and steps for (let ing of demoIngredients) { ing.combined = false; } for (let step of demoSteps) { step.completed = false; step.timer = 0; } currentDemoStep = 0; demoTimer = 0; // Clear any remaining animations demoAnimations = []; } function drawWinScreen() { // Draw win message textAlign(CENTER, CENTER); textSize(40); textFont(titleFont); fill('#333'); text("YOU MADE IT!", width/2, height/2 - 120); // Draw recipe name textSize(32); fill('#4CAF50'); text(final_combination.name, width/2, height/2 - 60); // Draw turn count with larger, more prominent display textSize(28); fill('#666'); text("Completed in", width/2, height/2); // Draw turn counter in a circle let turnCircleSize = 80; fill('#4285F4'); stroke('#333'); strokeWeight(3); circle(width/2, height/2 + 60, turnCircleSize); // Draw turn number fill('white'); noStroke(); textSize(36); text(turnCounter, width/2, height/2 + 60); textSize(18); text("turns", width/2, height/2 + 90); // Draw move history with larger circles drawWinMoveHistory(); // Draw buttons shareButton.draw(); recipeButton.draw(); // Check button hover shareButton.checkHover(mouseX, mouseY); recipeButton.checkHover(mouseX, mouseY); } // Enhanced move history display for win screen function drawWinMoveHistory() { const circleSize = 25; const margin = 8; const maxPerRow = 15; const startX = width/2 - (Math.min(moveHistory.length, maxPerRow) * (circleSize + margin) - margin) / 2; const startY = height/2 + 140; for (let i = 0; i < moveHistory.length; i++) { const row = Math.floor(i / maxPerRow); const col = i % maxPerRow; const x = startX + col * (circleSize + margin); const y = startY + row * (circleSize + margin); fill(moveHistory[i]); stroke('black'); strokeWeight(2); circle(x, y, circleSize); } } // Keep the regular move history for during gameplay function drawMoveHistory() { const circleSize = 15; const margin = 5; const startX = 90; const startY = height - 20; for (let i = 0; i < moveHistory.length; i++) { fill(moveHistory[i]); stroke('black'); strokeWeight(1); circle(startX + i * (circleSize + margin), startY - circleSize/2, circleSize); } } function updateCursor() { let overInteractive = false; if (!gameStarted) { // Check start button if (startButton.isInside(mouseX, mouseY)) { overInteractive = true; } } else if (gameWon) { // Check buttons if (shareButton.isInside(mouseX, mouseY) || recipeButton.isInside(mouseX, mouseY)) { overInteractive = true; } } else { // Check vessels for (let v of vessels) { if (v.isInside(mouseX, mouseY)) { overInteractive = true; break; } } // Check hint vessel if (showingHint && hintVessel && hintVessel.isInside(mouseX, mouseY)) { overInteractive = true; } // Check buttons if (!showingHint && hintButton.isInside(mouseX, mouseY)) { overInteractive = true; } } // Set cursor cursor(overInteractive ? HAND : ARROW); } function mousePressed() { if (!gameStarted) { // Check if start button was clicked if (startButton.isInside(mouseX, mouseY)) { startButton.handleClick(); return; } } else if (gameWon) { // Check if buttons were clicked if (shareButton.handleClick() || recipeButton.handleClick()) { return; } } else { // Check if hint button was clicked if (!showingHint && hintButton.isInside(mouseX, mouseY)) { hintButton.handleClick(); return; } // Check if a vessel was clicked for (let v of vessels) { if (v.isInside(mouseX, mouseY)) { draggedVessel = v; offsetX = mouseX - v.x; offsetY = mouseY - v.y; v.targetScale = 1.1; // Slight scale up when dragging triggerHapticFeedback('success'); // Haptic feedback on successful drag break; } } } } function mouseDragged() { if (draggedVessel) { draggedVessel.x = mouseX - offsetX; draggedVessel.y = mouseY - offsetY; } } function mouseReleased() { if (draggedVessel) { draggedVessel.targetScale = 1; // Reset scale let overVessel = null; let overHintVessel = false; // Check if dragged over another vessel for (let v of vessels) { if (v !== draggedVessel && v.isInside(mouseX, mouseY)) { overVessel = v; break; } } // Check if dragged over hint vessel if (showingHint && hintVessel && hintVessel.isInside(mouseX, mouseY)) { overHintVessel = true; } if (overVessel) { // Regular vessel combination // Increment turn counter turnCounter++; let new_v = combineVessels(draggedVessel, overVessel); if (new_v) { // Create animation particles createCombineAnimation(draggedVessel.x, draggedVessel.y, draggedVessel.color, new_v.x, new_v.y); createCombineAnimation(overVessel.x, overVessel.y, overVessel.color, new_v.x, new_v.y); // Remove old vessels and add new one vessels = vessels.filter(v => v !== draggedVessel && v !== overVessel); vessels.push(new_v); arrangeVessels(); // Re-center after combination // Pulse the new vessel new_v.pulse(); // Store the current move history length to detect if checkForMatchingVessels adds moves let originalMoveHistoryLength = moveHistory.length; // Check if the new vessel matches the current hint if (showingHint && hintVessel) { // Check if this vessel matches the hint checkForMatchingVessels(); } // Only add to move history if it wasn't already added by checkForMatchingVessels if (moveHistory.length === originalMoveHistoryLength) { // Add successful move to history with the color of the new vessel moveHistory.push(new_v.color); } if (vessels.length === 1 && vessels[0].name === final_combination.name) { gameWon = true; triggerHapticFeedback('completion'); // Haptic feedback on game completion } } else { draggedVessel.snapBack(); // Add unsuccessful move to history (black) moveHistory.push('black'); triggerHapticFeedback('error'); // Haptic feedback on unsuccessful move } } else if (overHintVessel) { // Trying to add to the hint vessel turnCounter++; let canAddToHint = false; // Check if it's a single ingredient if (draggedVessel.ingredients.length === 1) { let ingredientName = draggedVessel.ingredients[0]; canAddToHint = hintVessel.addIngredient(ingredientName); } // Check if it's a partial combination that matches one of the required ingredients else if (draggedVessel.name && hintVessel.required.includes(draggedVessel.name)) { canAddToHint = hintVessel.addIngredient(draggedVessel.name); } // Check if it's a yellow vessel with multiple ingredients that are all part of the hint else if (draggedVessel.ingredients.length > 0 && draggedVessel.ingredients.every(ing => hintVessel.required.includes(ing))) { // Check if we can add all ingredients to the hint canAddToHint = true; for (let ing of draggedVessel.ingredients) { if (hintVessel.collected.includes(ing)) { canAddToHint = false; break; } } // If we can add all ingredients, do so if (canAddToHint) { for (let ing of draggedVessel.ingredients) { hintVessel.addIngredient(ing); } } } if (canAddToHint) { // Create animation createCombineAnimation(draggedVessel.x, draggedVessel.y, draggedVessel.color, hintVessel.x, hintVessel.y); // Remove the vessel vessels = vessels.filter(v => v !== draggedVessel); arrangeVessels(); // Add red move to history (not the original vessel color) moveHistory.push('#FF5252'); // Check if hint is complete if (hintVessel.isComplete()) { // Convert hint to regular vessel let newVessel = hintVessel.toVessel(); vessels.push(newVessel); arrangeVessels(); // Reset hint hintVessel = null; showingHint = false; // Check win condition if (vessels.length === 1 && vessels[0].name === final_combination.name) { gameWon = true; triggerHapticFeedback('completion'); // Haptic feedback on game completion } } } else { draggedVessel.snapBack(); // Add unsuccessful move to history (black) moveHistory.push('black'); triggerHapticFeedback('error'); // Haptic feedback on unsuccessful move } } else { draggedVessel.snapBack(); } draggedVessel = null; } } function createCombineAnimation(startX, startY, color, targetX, targetY) { for (let i = 0; i < 5; i++) { animations.push(new CombineAnimation(startX, startY, color, targetX, targetY)); } } function combineVessels(v1, v2) { // Check if hint is active before creating any new vessels let hintActive = showingHint && hintVessel; if (v1.ingredients.length > 0 && v2.ingredients.length > 0 && v1.complete_combinations.length === 0 && v2.complete_combinations.length === 0) { let U = [...new Set([...v1.ingredients, ...v2.ingredients])]; let C_candidates = intermediate_combinations.filter(C => U.every(ing => C.required.includes(ing))); if (C_candidates.length > 0) { let C = C_candidates[0]; // If hint is active, check if these ingredients are part of the hint if (hintActive) { // Check if all ingredients are required for the hint let allIngredientsInHint = U.every(ing => hintVessel.required.includes(ing)); // Check if any of these ingredients are already collected in the hint let anyAlreadyCollected = U.some(ing => hintVessel.collected.includes(ing)); // If all ingredients are part of the hint and none are already collected, // we should add them to the hint vessel instead of creating a new vessel if (allIngredientsInHint && !anyAlreadyCollected) { // We'll handle this in mouseReleased by returning a yellow vessel // that will be detected by checkForMatchingVessels let new_v = new Vessel(U, [], null, 'yellow', (v1.x + v2.x) / 2, (v1.y + v2.y) / 2, 200, 100); return new_v; } } // Create a new vessel (yellow or green) let new_v = new Vessel(U, [], null, 'yellow', (v1.x + v2.x) / 2, (v1.y + v2.y) / 2, 200, 100); if (U.length === C.required.length && C.required.every(ing => U.includes(ing))) { // Only turn green if not part of an active hint if (!hintActive || !C.name === hintVessel.name) { new_v.name = C.name; new_v.color = 'green'; new_v.ingredients = []; // Clear ingredients since this is now a complete combination } } return new_v; } } else if ((v1.name || v1.complete_combinations.length > 0) && (v2.name || v2.complete_combinations.length > 0)) { // Handle combining completed combinations (green vessels) let set1 = v1.complete_combinations.length > 0 ? v1.complete_combinations : (v1.name ? [v1.name] : []); let set2 = v2.complete_combinations.length > 0 ? v2.complete_combinations : (v2.name ? [v2.name] : []); let U = [...new Set([...set1, ...set2])]; // Check if the combined set contains valid components for the final combination if (U.some(name => final_combination.required.includes(name))) { let new_v = new Vessel([], U, null, 'yellow', (v1.x + v2.x) / 2, (v1.y + v2.y) / 2, 200, 100); // Check if we have all required components for the final combination if (final_combination.required.every(name => U.includes(name))) { new_v.name = final_combination.name; new_v.color = 'green'; new_v.complete_combinations = []; // Clear since this is the final combination } return new_v; } } return null; } function shareScore() { // Create share text let shareText = `I made ${final_combination.name} in ${turnCounter} turns in Combo Meal! Can you beat my score?`; // Try to use the Web Share API if available if (navigator.share) { navigator.share({ title: 'Combo Meal Score', text: shareText, url: window.location.href }) .catch(error => console.log('Error sharing:', error)); } else { // Fallback: copy to clipboard navigator.clipboard.writeText(shareText) .then(() => { alert('Score copied to clipboard!'); }) .catch(err => { console.error('Failed to copy: ', err); alert(shareText); }); } } function viewRecipe() { window.open(recipeUrl, '_blank'); } function mouseMoved() { // Update button hover states if (!gameStarted) { startButton.checkHover(mouseX, mouseY); } else if (gameWon) { shareButton.checkHover(mouseX, mouseY); recipeButton.checkHover(mouseX, mouseY); } else if (!showingHint) { hintButton.checkHover(mouseX, mouseY); } } // Function to show a hint function showHint() { if (!showingHint && !gameWon) { // Find combinations that haven't been completed yet let completedCombos = vessels .filter(v => v.name !== null) .map(v => v.name); // Find partial combinations that exist in vessels let partialCombos = []; for (let v of vessels) { if (v.ingredients.length > 0 && v.name === null) { // This is a partial combination (yellow vessel without a name) for (let combo of intermediate_combinations) { // Check if this partial combination is working toward a specific combo if (v.ingredients.every(ing => combo.required.includes(ing))) { partialCombos.push(combo.name); } } } } // First check intermediate combinations that aren't partial or completed let availableCombos = intermediate_combinations.filter(combo => !completedCombos.includes(combo.name) && !partialCombos.includes(combo.name)); // If no non-partial combos are available, then allow partial combos if (availableCombos.length === 0) { availableCombos = intermediate_combinations.filter(combo => !completedCombos.includes(combo.name)); } // If all intermediate combinations are done, check final combination if (availableCombos.length === 0 && !completedCombos.includes(final_combination.name)) { availableCombos = [final_combination]; } // If there are available combinations, show a hint if (availableCombos.length > 0) { // Choose a random combination to hint let combo = availableCombos[Math.floor(Math.random() * availableCombos.length)]; hintVessel = new HintVessel(combo); showingHint = true; // Check if any existing yellow vessels match ingredients needed for this hint checkForMatchingVessels(); } } } // Function to check if any yellow vessels match the current hint function checkForMatchingVessels() { if (!hintVessel) return; // Look for yellow vessels that match required ingredients for the hint for (let i = vessels.length - 1; i >= 0; i--) { let v = vessels[i]; // Check if it's a yellow vessel with ingredients that match the hint if (v.color === 'yellow' && v.ingredients.length > 0) { let matchesHint = false; // Check if all ingredients in this vessel are required for the hint if (v.ingredients.every(ing => hintVessel.required.includes(ing))) { // Check if we can add all ingredients to the hint let canAddAll = true; for (let ing of v.ingredients) { if (hintVessel.collected.includes(ing)) { canAddAll = false; break; } } if (canAddAll) { matchesHint = true; // Add all ingredients to the hint vessel for (let ing of v.ingredients) { hintVessel.addIngredient(ing); } // Create animation createCombineAnimation(v.x, v.y, v.color, hintVessel.x, hintVessel.y); // Remove the vessel vessels.splice(i, 1); // Add red moves to history - one for each ingredient (or at least two if it was a combination) // This ensures we count the proper number of turns when adding multiple ingredients at once let numIngredientsAdded = Math.max(2, v.ingredients.length); for (let j = 0; j < numIngredientsAdded; j++) { moveHistory.push('#FF5252'); } // Increment turn counter - add one more turn since the first turn was already counted // when the vessel was created in mouseReleased turnCounter += (numIngredientsAdded - 1); } } } } // Re-arrange vessels after potential removals arrangeVessels(); // Check if hint is complete if (hintVessel && hintVessel.isComplete()) { // Convert hint to regular vessel let newVessel = hintVessel.toVessel(); vessels.push(newVessel); arrangeVessels(); // Reset hint hintVessel = null; showingHint = false; // Check win condition if (vessels.length === 1 && vessels[0].name === final_combination.name) { gameWon = true; triggerHapticFeedback('completion'); // Haptic feedback on game completion } } } function startGame() { gameStarted = true; } function triggerHapticFeedback(type) { if (navigator.vibrate) { switch(type) { case 'success': navigator.vibrate([50, 30, 50]); // Short double vibration break; case 'error': navigator.vibrate(100); // Single short vibration break; case 'completion': navigator.vibrate([100, 50, 100, 50, 200]); // Celebratory pattern break; } } } // Function to draw loading screen function drawLoadingScreen() { // Center text textAlign(CENTER, CENTER); // Draw loading message fill(50); textSize(24); const message = window.recipeData ? window.recipeData.loadingMessage : "Loading today's recipe..."; text(message, width/2, height/2 - 40); // Draw animated loading indicator const time = millis() * 0.001; const dotCount = 3; let dots = ""; for (let i = 0; i < dotCount; i++) { if (i < Math.floor((time * 2) % (dotCount + 1))) { dots += "."; } else { dots += " "; } } text(dots, width/2, height/2); // Draw small text textSize(16); text("Fetching today's culinary puzzle", width/2, height/2 + 40); } </script> </body> </html>