Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/ImLukzy/ChefDash/llms.txt

Use this file to discover all available pages before exploring further.

RecipesView is the beating heart of ChefDash — the screen where every round is actually played. Once a level starts, the player stares down a target burger recipe displayed as a row of emoji slots, a ticking arcade timer, and a shuffling grid of ingredient buttons. Their only job is to tap each ingredient in the exact correct order, one by one, before the clock hits zero. Every correct order earns coins and raises the combo multiplier; every mistake costs precious seconds and resets the combo. The round ends the moment the timer expires, at which point the final star rating is calculated and control passes to VictoryView.

How the Round Starts

RecipesView becomes active when ContentView detects gameState.activeTab == "kitchen_round". This value is set immediately after gameState.startNewRound() is called from the pre-game modal on the map screen.
// ContentView hides the CustomTabBar and shows RecipesView
// whenever the active tab is "kitchen_round"
gameState.activeTab = "kitchen_round"
The CustomTabBar is hidden while activeTab == "kitchen_round". ContentView checks activeTab != "kitchen_round" before rendering the tab bar, keeping the gameplay canvas clean and distraction-free. The same hiding logic applies to "level_complete".
On .onAppear, RecipesView performs three setup steps:
  1. Resets timeRemaining to 15.0 seconds as the initial baseline.
  2. Reads gameState.currentLevel.availableIngredients and stores a shuffled copy in randomizedIngredients — the local @State array that drives the ingredient grid.
  3. Registers the onRecipeSuccessBonusTime closure (see Horno Industrial upgrade).
If the Horno Industrial ("oven") upgrade was selected for the round, an additional +5.0 seconds is added to timeRemaining at this point.
// RecipesView.onAppear
timeRemaining = 15.0
randomizedIngredients = gameState.currentLevel.availableIngredients.shuffled()

if gameState.selectedUpgradesForRound.contains("oven") {
    timeRemaining += 5.0
}

Core Gameplay Loop

1

Generate the next order

gameState.generateNextOrder() is called — either at round start (via startNewRound) or immediately after the previous order is completed. It picks a random recipe from currentLevel.possibleRecipes and writes it to targetRecipeEmojis. It also derives a display name (currentRecipeName) from the recipe’s contents: "Mega Bacon Burger" for recipes containing 🥓, "Fresh Veggie Burger" for 🥬, and "Classic Cheeseburger" otherwise.
func generateNextOrder() {
    currentBurgerStack.removeAll()
    let level = currentLevel

    if let randomRecipe = level.possibleRecipes.randomElement() {
        targetRecipeEmojis = randomRecipe

        if randomRecipe.contains("🥓") { currentRecipeName = "Mega Bacon Burger" }
        else if randomRecipe.contains("🥬") { currentRecipeName = "Fresh Veggie Burger" }
        else { currentRecipeName = "Classic Cheeseburger" }
    }
}
2

Player reads the target recipe

The HUD displays currentRecipeName in the arcade orange accent color, and below it renders each emoji in targetRecipeEmojis as a row of frosted slots — one slot per required ingredient, in order. The player must replicate this sequence exactly.
3

Player taps an ingredient button

The ingredient grid (LazyVGrid) renders every emoji in randomizedIngredients. Each button tap calls gameState.addIngredient(_ emoji: String) and — regardless of correctness — immediately shuffles the grid via a spring animation, making each press feel snappy and keeping the layout unpredictable.
// Inside the ingredient Button action in RecipesView
gameState.addIngredient(ingrediente)

withAnimation(.spring(response: 0.25, dampingFraction: 0.6)) {
    randomizedIngredients.shuffle()
}
4

addIngredient validates positionally

GameState.addIngredient appends the tapped emoji to currentBurgerStack, then walks every index so far and compares currentBurgerStack[i] against targetRecipeEmojis[i]. The first mismatch — or a stack that has grown beyond the target length — immediately calls triggerError(). If all positions match and the stack length equals the target, triggerSuccess() is called.
func addIngredient(_ emoji: String) {
    currentBurgerStack.append(emoji)

    if currentBurgerStack.count > targetRecipeEmojis.count {
        triggerError()
        return
    }

    for i in 0..<currentBurgerStack.count {
        if currentBurgerStack[i] != targetRecipeEmojis[i] {
            triggerError()
            return
        }
    }

    if currentBurgerStack == targetRecipeEmojis {
        triggerSuccess()
    }
}
5

On success — coins, combo, bonus time

triggerSuccess() increments ordersCompletedInSession, awards (25 + bonusCoins) * comboMultiplier coins, and bumps comboMultiplier by 1 (capped at 5). It sets showPerfectMessage = true, triggers the onRecipeSuccessBonusTime closure (granting +1.5s to the timer on every successful order), then clears the flag and calls generateNextOrder() after 0.8 seconds.
private func triggerSuccess() {
    ordersCompletedInSession += 1
    showPerfectMessage = true

    let baseCoinsPerOrder = 25
    let hasSauce = shopInventory.first(where: { $0.id == "sauce" })?.quantityOwned ?? 0 > 0
    let bonusCoins = hasSauce ? 10 : 0
    coins += (baseCoinsPerOrder + bonusCoins) * comboMultiplier

    if comboMultiplier < 5 { comboMultiplier += 1 }

    onRecipeSuccessBonusTime?()

    DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) {
        self.showPerfectMessage = false
        self.generateNextOrder()
    }
}
6

On error — penalty, reset, clear

triggerError() sets showErrorMessage = true and resets comboMultiplier to 1. After 0.8 seconds the flag clears and currentBurgerStack is emptied, giving the player a clean plate to try again on the same order. Note that errors in RecipesView also subtract 3.0 seconds from timeRemaining before calling addIngredient — this deduction happens in the view, not in GameState.
// RecipesView — before calling addIngredient
if ingrediente != gameState.targetRecipeEmojis[nextIndexNeeded] {
    withAnimation { timeRemaining = max(0.0, timeRemaining - 3.0) }
}

Timer Mechanics

RecipesView owns the timer entirely via a Timer.publish(every: 0.1, ...) Combine publisher connected in .onReceive. Each tick decrements timeRemaining by 0.1. When timeRemaining drops to or below 0.1 the publisher is cancelled, the star rating is calculated inline, gameState.finishRound(starsEarned:) is called, and the screen transitions to "level_complete".
.onReceive(gameTimer) { _ in
    if timeRemaining > 0.1 {
        timeRemaining -= 0.1
    } else {
        timeRemaining = 0.0
        gameTimer.upstream.connect().cancel()

        let target = gameState.currentLevel.targetOrders
        let completadas = gameState.ordersCompletedInSession

        let estrellasFinales = completadas >= target ? 3
            : (completadas >= max(1, target / 2) ? 2
            : (completadas > 0 ? 1 : 0))

        gameState.finishRound(starsEarned: estrellasFinales)

        withAnimation(.spring()) {
            gameState.activeTab = "level_complete"
        }
    }
}
The timer clock in the HUD turns red when timeRemaining <= 5.0, providing a last-seconds visual warning.
The baseTime field on the Level model defines each level’s intended time budget (e.g., 40 s for Burger Station, 45 s for Green Diner, 50 s for Bacon & Grill — decreasing toward 30 s for the hardest procedural levels). RecipesView currently hard-codes its initial timeRemaining to 15.0 regardless of baseTime, then adds the Horno Industrial bonus on top if applicable.

The Horno Industrial Upgrade

The Horno Industrial shop item (id: "oven") grants an extra +5.0 seconds at round start. This is the only time-bonus gated by the upgrade:
MomentAmountCondition
Round start (onAppear)+5.0 sOnly when selectedUpgradesForRound.contains("oven")
Separately, RecipesView always registers an onRecipeSuccessBonusTime closure that adds +1.5 seconds to the timer on every successful order — regardless of whether the Horno Industrial upgrade was active. This closure is set unconditionally in .onAppear:
gameState.onRecipeSuccessBonusTime = {
    withAnimation {
        timeRemaining += 1.5
        randomizedIngredients.shuffle()
    }
}
GameState.triggerSuccess() calls onRecipeSuccessBonusTime?() — it does not mutate timeRemaining directly. This design keeps time management entirely inside RecipesView, where the local @State var timeRemaining lives. Calling the closure from GameState rather than passing a binding avoids threading issues with the Combine timer.

Visual Feedback Overlays

Both feedback states are driven by @Published flags on GameState that auto-clear after 0.8 seconds.

✨ Perfect Order

showPerfectMessage = true renders a green capsule overlay reading ”✨ +1.5s ✨” centered over the burger assembly plate. The +1.5s label reflects the bonus time added to timeRemaining on every successful order via the onRecipeSuccessBonusTime closure.

💥 Wrong Ingredient

showErrorMessage = true renders a red capsule overlay reading ”💥 -3.0s” over the same plate. The -3.0s penalty is subtracted from timeRemaining in the view at the moment the wrong ingredient is tapped.

Combo Multiplier

When comboMultiplier > 1, a "🔥 x{N}" badge appears in the header HUD between the exit button and the timer. The multiplier starts at 1 (or 2 with the Cuchillo Afilado upgrade) and increments by 1 on each correct order, capped at 5. Any wrong ingredient resets it to 1.
// Coin award formula inside triggerSuccess()
coins += (baseCoinsPerOrder + bonusCoins) * comboMultiplier
// e.g. at x5 combo with Salsa Secreta: (25 + 10) * 5 = 175 coins per order

Clearing the Plate

A “LIMPIAR PLATO” button at the bottom of the ingredient grid clears currentBurgerStack directly and reshuffles the ingredient grid. This is a convenience reset that does not trigger triggerError(), so it carries no time penalty and does not break the combo.

Build docs developers (and LLMs) love