
Cooking
You prepare dishes by following recipes: chopping, mixing, cooking and serving on time. The challenge is usually getting each step right or juggling many orders at once without burning anything.
Also known as: cooking game, cooking sim
Defining mechanics
- Recipes as sequences of steps, in which each ingredient goes through stations (chopping, cooking, plating) in a fixed order.
- Stations that work over time. What you put on the heat cooks by itself and, if you are careless, burns.
- Timed orders that arrive in a queue. Delivering early scores more and letting them expire is penalised.
- Limited hands. You carry one thing at a time, so the challenge lies in the order and the route more than in any single action.
- In the minigame variant, each step of the recipe is a precise gesture (chopping, whisking, flipping) that the game grades.
Gameplay loop
Read the order, decide what to prepare first, get the ingredients going at the stations, keep an eye on what is cooking, plate up and deliver before time runs out.
Our demo
Controls. Everything is played with the mouse or a finger. Click a crate to pick up an ingredient, then a station to put it down; click the station again to collect it when it is ready. Click the plate to plate up, the bin to throw away what you carry (or empty the plate) and Serve to deliver.
Open full screenA kitchen with ingredient crates, a chopping board, two stoves, a plate, a bin and a serving hatch. Timed orders come in, and each one requires taking the ingredients through the stations in the right order, making sure nothing burns and delivering on time.
Made with Kaplay, in about 170 lines of JavaScript.
How it's made
Setup and pace
The constants are the design knobs. CHOP_TIME and COOK_TIME set how long each step takes, BURN_TIME how much margin you have before burning, and ORDER_TIME and ORDER_EVERY how much pressure there is. Almost all the difficulty is tuned with these numbers.
kaplay({ width: 640, height: 360, letterbox: true, pixelDensity, background: [38, 32, 36], // Played with the mouse or a finger: click a crate, then a station. buttons: { start: { keyboard: ["space", "enter"] } }, }); const CHOP_TIME = 1.2; // seconds on the board const COOK_TIME = 3; // seconds on the stove until cooked… const BURN_TIME = 7.5; // …and until burnt const ORDER_TIME = 45; // seconds a customer waits const ORDER_EVERY = 10; // a new order every few seconds (at most 3 waiting) const GOAL = 5; // dishes to serve to win const MAX_LOST = 3; // orders you can let expireIngredients, recipes and states
Each ingredient has a state (raw, chopped, cooked, burnt) and each recipe is a list of «ingredient:state» pairs. ACCEPTS says what each station takes and in which state, and that is where the mandatory order comes from: the soup needs the tomato chopped before the stove will take it. drawItem draws each state differently so it can be read at a glance.
// Ingredients, and how they look after each step. const FOOD = { bread: { name: t({ es: "Pan", en: "Bread" }), cooked: t({ es: "tostado", en: "toasted" }), color: [214, 170, 100] }, tomato: { name: t({ es: "Tomate", en: "Tomato" }), cooked: t({ es: "cocido", en: "cooked" }), color: [222, 70, 60] }, cheese: { name: t({ es: "Queso", en: "Cheese" }), cooked: "", color: [250, 210, 80] }, cucumber: { name: t({ es: "Pepino", en: "Cucumber" }), cooked: "", color: [110, 190, 90] }, }; const RECIPES = [ { name: t({ es: "Ensalada", en: "Salad" }), needs: ["cucumber:chopped", "tomato:chopped"] }, { name: t({ es: "Pan con queso", en: "Cheese toast" }), needs: ["bread:cooked", "cheese:chopped"] }, { name: t({ es: "Sopa de tomate", en: "Tomato soup" }), needs: ["tomato:cooked"] }, ]; // Each station accepts an ingredient only in the state it can work on. // That is what forces the order: the tomato for the soup is chopped first. const ACCEPTS = { board: { tomato: "raw", cheese: "raw", cucumber: "raw" }, stove: { bread: "raw", tomato: "chopped" }, }; const keyOf = (items) => items.map((i) => `${i.kind}:${i.state}`).sort().join(","); function itemName(item) { const f = FOOD[item.kind]; const state = { raw: "", chopped: t({ es: "picado", en: "chopped" }), cooked: f.cooked, burnt: t({ es: "quemado", en: "burnt" }) }[item.state]; return t({ es: `${f.name} ${state}`, en: `${state} ${f.name.toLowerCase()}` }).trim(); } function drawItem(item, p) { const c = item.state === "burnt" ? rgb(50, 40, 40) : rgb(...FOOD[item.kind].color); if (item.state === "chopped" || (item.state === "cooked" && item.kind === "tomato")) for (const dx of [-10, 0, 10]) drawRect({ pos: p.add(dx - 4, dx ? -2 : -7), width: 8, height: 8, color: c }); else drawCircle({ pos: p, radius: 12, color: c }); if (item.state === "cooked") drawCircle({ pos: p, radius: 17, fill: false, outline: { width: 3, color: rgb(255, 150, 40) } }); }Stations
Every place in the kitchen (crates, board, stoves, plate, bin, hatch) is a box with area() that responds to clicks, and a single function creates them all. Each station keeps track of what is on it and draws itself.
// Every place in the kitchen is a clickable box that reacts to what you carry. function station(type, x, y, w, h, label, col, extra = {}) { const s = add([rect(w, h, { radius: 6 }), pos(x, y), color(...col), area(), "station", { type, item: null, time: 0, ...extra, draw() { if (this.item) drawItem(this.item, vec2(w / 2, h / 2 + 6)); if (type === "plate") this.items.forEach((it, i) => drawItem(it, vec2(30 + i * 48, h / 2 + 8))); if (type === "crate") drawItem({ kind: this.kind, state: "raw" }, vec2(w - 22, h / 2)); if (this.item && (type === "board" || type === "stove")) drawProgress(this, w, h); } }]); s.add([text(label, { size: 13, font: FONT }), pos(8, 6)]); s.onClick(() => use(s)); return s; } Object.keys(FOOD).forEach((kind, i) => station("crate", 12, 96 + i * 64, 100, 56, FOOD[kind].name, [70, 58, 50], { kind })); station("board", 130, 96, 110, 84, t({ es: "Tabla (picar)", en: "Board (chop)" }), [150, 120, 80]); station("stove", 256, 96, 110, 84, t({ es: "Fogón 1", en: "Stove 1" }), [70, 70, 84]); station("stove", 382, 96, 110, 84, t({ es: "Fogón 2", en: "Stove 2" }), [70, 70, 84]); const plate = station("plate", 130, 226, 160, 90, t({ es: "Plato", en: "Plate" }), [200, 200, 210], { items: [] }).items; station("trash", 306, 246, 80, 70, t({ es: "Basura", en: "Bin" }), [60, 60, 60]); station("serve", 402, 226, 226, 90, t({ es: "Servir (entregar el plato)", en: "Serve (hand in the plate)" }), [60, 110, 80]);Time cooks (and burns)
Once loaded, a station works by itself. The board finishes and waits, but the stove keeps going, because it cooks at COOK_TIME and burns at BURN_TIME. The green bar shows progress and, when the food is ready, it turns red and blinks near the end. That window between cooked and burnt is what forces you to watch several things at once.
// Stations work on their own once loaded. The board finishes and waits; // the stove keeps going: cooked at COOK_TIME, burnt at BURN_TIME. const ready = (s) => (s.type === "board" ? s.item.state === "chopped" : s.item.state !== ACCEPTS.stove[s.item.kind]); onUpdate("station", (s) => { if (!s.item || (s.type !== "board" && s.type !== "stove")) return; s.time += dt(); if (s.type === "board" && s.time >= CHOP_TIME) s.item.state = "chopped"; if (s.type === "stove" && s.time >= COOK_TIME && s.item.state !== "burnt") s.item.state = "cooked"; if (s.type === "stove" && s.time >= BURN_TIME) s.item.state = "burnt"; }); function drawProgress(s, w, h) { const limit = s.type === "board" ? CHOP_TIME : s.time < COOK_TIME ? COOK_TIME : BURN_TIME; const k = Math.min(1, s.time / limit); const danger = s.type === "stove" && s.time >= COOK_TIME && s.item.state !== "burnt"; // Once cooked, the bar refills in red: the time left before it burns. It blinks near the end. const blink = danger && s.time > BURN_TIME - 2 && Math.floor(time() * 6) % 2; drawRect({ pos: vec2(8, h - 12), width: (w - 16) * k, height: 6, color: danger ? rgb(240, 70, 50) : rgb(120, 220, 120), opacity: blink ? 0.3 : 1 }); }Pick up and put down
The whole game is two clicks, picking something up and putting it down somewhere. Since you only carry one thing at a time, the challenge lies in the order. If you try something that does not fit, the game explains why (for example, «chop it on the board first»), and burnt food can only go in the bin.
// The whole game is two clicks: pick something up, put it somewhere. function use(s) { if (s.type === "crate") return hand ? notify(t({ es: "Ya llevas algo en la mano.", en: "Your hands are full." })) : (hand = { kind: s.kind, state: "raw" }); if (s.type === "board" || s.type === "stove") { if (!hand && s.item) return ready(s) ? ((hand = s.item), (s.item = null)) : notify(t({ es: "Todavía no está listo.", en: "Not ready yet." })); if (!hand || s.item) return; const need = ACCEPTS[s.type][hand.kind]; if (need === hand.state) return (s.item = hand), (s.time = 0), (hand = null); if (s.type === "stove" && hand.kind === "tomato" && hand.state === "raw") return notify(t({ es: "Primero pícalo en la tabla: cada estación tiene su turno.", en: "Chop it on the board first: each station has its turn." })); return notify(t({ es: `${itemName(hand)}: aquí no.`, en: `${itemName(hand)}: not here.` })); } if (s.type === "plate" && hand) { if (hand.state === "burnt") return notify(t({ es: "Quemado no se sirve: tíralo a la basura.", en: "Burnt food can't be served: bin it." })); if (plate.length >= 3) return notify(t({ es: "El plato está lleno.", en: "The plate is full." })); plate.push(hand); hand = null; } if (s.type === "trash") hand ? (hand = null) : (plate.length = 0); if (s.type === "serve") serve(); }Timed orders
Each order is an object with its recipe and its remaining time. A new one arrives every ORDER_EVERY seconds, with at most three waiting. When serving, the game looks for an order whose recipe exactly matches the plate, and early delivery scores more. If an order's time reaches zero it is lost, and three lost orders end the shift.
// Orders arrive with a timer. Serving early scores more; a lost order counts against you. // Each order is a game object: its ticket is drawn in arrival order. function spawnOrder() { if (get("order").length < 3) add([pos(0, 8), z(5), "order", { recipe: choose(RECIPES), left: ORDER_TIME, draw: drawTicket }]); } loop(ORDER_EVERY, spawnOrder); // the first one comes right away onUpdate(() => get("order").forEach((o, i) => (o.pos.x = 12 + i * 152))); onUpdate("order", (o) => { o.left -= dt(); if (o.left <= 0) { destroy(o); lost++; notify(t({ es: `Se fue un cliente sin su ${o.recipe.name.toLowerCase()}.`, en: `A customer left without their ${o.recipe.name.toLowerCase()}.` })); if (lost >= MAX_LOST) go("end", false, served, score); } }); function serve() { const order = get("order").find((o) => [...o.recipe.needs].sort().join(",") === keyOf(plate)); if (!plate.length) return notify(t({ es: "El plato está vacío.", en: "The plate is empty." })); if (!order) return notify(t({ es: "Nadie pidió ese plato: revisa los pedidos.", en: "Nobody ordered that: check the orders." })); destroy(order); plate.length = 0; served++; score += 10 + Math.ceil(order.left); notify(t({ es: `Pedido servido: ${order.recipe.name.toLowerCase()}.`, en: `Order served: ${order.recipe.name.toLowerCase()}.` })); if (served >= GOAL) go("end", true, served, score); }Tickets and counters
The tickets show what each customer wants using the same icons as the ingredients, plus a time bar that turns red near the end. Below you can see what you are holding and a message with the latest explanation.
// Order tickets, counters, a message line and what you carry, drawn every frame. function drawTicket() { drawRect({ width: 144, height: 74, radius: 4, color: rgb(250, 244, 225) }); drawText({ text: this.recipe.name, size: 14, font: FONT, pos: vec2(8, 6), color: rgb(40, 30, 30) }); this.recipe.needs.forEach((n, j) => { const [kind, state] = n.split(":"); drawItem({ kind, state }, vec2(24 + j * 44, 42)); }); const k = this.left / ORDER_TIME; drawRect({ pos: vec2(6, 64), width: 132 * k, height: 5, color: k > 0.35 ? rgb(90, 170, 90) : rgb(220, 70, 50) }); } onDraw(() => { drawText({ text: t({ es: `Servidos ${served}/${GOAL}\nPerdidos ${lost}/${MAX_LOST}\nPuntos ${score}`, en: `Served ${served}/${GOAL}\nLost ${lost}/${MAX_LOST}\nScore ${score}` }), size: 14, font: FONT, pos: vec2(520, 12), color: rgb(240, 230, 210) }); drawText({ text: t({ es: "En la mano: ", en: "Holding: " }) + (hand ? itemName(hand) : "—"), size: 14, font: FONT, pos: vec2(130, 194), color: rgb(255, 230, 160) }); drawText({ text: notice, size: 13, font: FONT, pos: vec2(130, 326), width: 500, color: rgb(255, 190, 140) }); if (hand) drawItem(hand, mousePos().add(16, 16)); });Scenes
Three scenes (title, game and end). You win by serving GOAL dishes and lose by letting MAX_LOST orders expire.
function screen(title, subtitle, next) { add([text(title, { size: 32, font: FONT, align: "center" }), pos(center().sub(0, 70)), anchor("center")]); add([text(subtitle, { size: 15, font: FONT, align: "center", width: 580 }), pos(center().add(0, 30)), anchor("center"), color(240, 225, 210)]); onButtonPress("start", next); onClick(next); } scene("title", () => screen( t({ es: "Cocina a contrarreloj", en: "Kitchen rush" }), t({ es: `Haz clic en un cajón para tomar un ingrediente y luego en una estación para dejarlo.\nLa tabla pica; el fogón cocina y, si te descuidas, quema. Arma el plato y sírvelo.\nCada pedido tiene su tiempo. Sirve ${GOAL} antes de perder ${MAX_LOST}.\nClic o espacio para empezar.`, en: `Click a crate to pick up an ingredient, then a station to put it down.\nThe board chops; the stove cooks and, if you are careless, burns. Build the plate and serve it.\nEach order has a timer. Serve ${GOAL} before you lose ${MAX_LOST}.\nClick or press space to start.`, }), () => go("game"), ), ); scene("end", (won, served, score) => screen( won ? t({ es: "¡Servicio completo!", en: "Service complete!" }) : t({ es: "Cerró la cocina", en: "The kitchen closed" }), t({ es: `Platos servidos: ${served}. Puntos: ${score}. Clic para otro turno.`, en: `Dishes served: ${served}. Score: ${score}. Click for another shift.` }), () => go("game"), ), ); go("title");
What we simplified
- There are only four ingredients, three recipes and two kinds of station. Chopping is automatic, whereas in many games of the genre you have to hold a button or repeat a gesture.
- The cook does not walk, because a click takes them to any station. In games such as Overcooked moving around the kitchen is a central part of the challenge.
- There is no money, upgrades or multiple days of service.
To modify it
- Make chopping require holding the click on the board. How does it change the feeling of urgency?
- Add a three-ingredient recipe and a third kind of station (for example, a slower oven).
- Add a second cook controlled with the keyboard and turn it into a co-op game.
Full code
// Cooking game demo: kitchen rush
// Engine: Kaplay 3001 (https://kaplayjs.com). MIT licence.
import kaplay from "../../vendor/kaplay.mjs";
import { t, FONT, pixelDensity } from "../common.js";
// #region setup
kaplay({
width: 640,
height: 360,
letterbox: true, pixelDensity,
background: [38, 32, 36],
// Played with the mouse or a finger: click a crate, then a station.
buttons: { start: { keyboard: ["space", "enter"] } },
});
const CHOP_TIME = 1.2; // seconds on the board
const COOK_TIME = 3; // seconds on the stove until cooked…
const BURN_TIME = 7.5; // …and until burnt
const ORDER_TIME = 45; // seconds a customer waits
const ORDER_EVERY = 10; // a new order every few seconds (at most 3 waiting)
const GOAL = 5; // dishes to serve to win
const MAX_LOST = 3; // orders you can let expire
// #endregion
// #region recipes
// Ingredients, and how they look after each step.
const FOOD = {
bread: { name: t({ es: "Pan", en: "Bread" }), cooked: t({ es: "tostado", en: "toasted" }), color: [214, 170, 100] },
tomato: { name: t({ es: "Tomate", en: "Tomato" }), cooked: t({ es: "cocido", en: "cooked" }), color: [222, 70, 60] },
cheese: { name: t({ es: "Queso", en: "Cheese" }), cooked: "", color: [250, 210, 80] },
cucumber: { name: t({ es: "Pepino", en: "Cucumber" }), cooked: "", color: [110, 190, 90] },
};
const RECIPES = [
{ name: t({ es: "Ensalada", en: "Salad" }), needs: ["cucumber:chopped", "tomato:chopped"] },
{ name: t({ es: "Pan con queso", en: "Cheese toast" }), needs: ["bread:cooked", "cheese:chopped"] },
{ name: t({ es: "Sopa de tomate", en: "Tomato soup" }), needs: ["tomato:cooked"] },
];
// Each station accepts an ingredient only in the state it can work on.
// That is what forces the order: the tomato for the soup is chopped first.
const ACCEPTS = {
board: { tomato: "raw", cheese: "raw", cucumber: "raw" },
stove: { bread: "raw", tomato: "chopped" },
};
const keyOf = (items) => items.map((i) => `${i.kind}:${i.state}`).sort().join(",");
function itemName(item) {
const f = FOOD[item.kind];
const state = { raw: "", chopped: t({ es: "picado", en: "chopped" }), cooked: f.cooked, burnt: t({ es: "quemado", en: "burnt" }) }[item.state];
return t({ es: `${f.name} ${state}`, en: `${state} ${f.name.toLowerCase()}` }).trim();
}
function drawItem(item, p) {
const c = item.state === "burnt" ? rgb(50, 40, 40) : rgb(...FOOD[item.kind].color);
if (item.state === "chopped" || (item.state === "cooked" && item.kind === "tomato"))
for (const dx of [-10, 0, 10]) drawRect({ pos: p.add(dx - 4, dx ? -2 : -7), width: 8, height: 8, color: c });
else drawCircle({ pos: p, radius: 12, color: c });
if (item.state === "cooked") drawCircle({ pos: p, radius: 17, fill: false, outline: { width: 3, color: rgb(255, 150, 40) } });
}
// #endregion
scene("game", () => {
let hand = null; // what the cook is carrying (one thing at a time)
let [served, lost, score] = [0, 0, 0];
let notice = "";
const notify = (s) => (notice = s);
// #region stations
// Every place in the kitchen is a clickable box that reacts to what you carry.
function station(type, x, y, w, h, label, col, extra = {}) {
const s = add([rect(w, h, { radius: 6 }), pos(x, y), color(...col), area(), "station", { type, item: null, time: 0, ...extra,
draw() {
if (this.item) drawItem(this.item, vec2(w / 2, h / 2 + 6));
if (type === "plate") this.items.forEach((it, i) => drawItem(it, vec2(30 + i * 48, h / 2 + 8)));
if (type === "crate") drawItem({ kind: this.kind, state: "raw" }, vec2(w - 22, h / 2));
if (this.item && (type === "board" || type === "stove")) drawProgress(this, w, h);
} }]);
s.add([text(label, { size: 13, font: FONT }), pos(8, 6)]);
s.onClick(() => use(s));
return s;
}
Object.keys(FOOD).forEach((kind, i) => station("crate", 12, 96 + i * 64, 100, 56, FOOD[kind].name, [70, 58, 50], { kind }));
station("board", 130, 96, 110, 84, t({ es: "Tabla (picar)", en: "Board (chop)" }), [150, 120, 80]);
station("stove", 256, 96, 110, 84, t({ es: "Fogón 1", en: "Stove 1" }), [70, 70, 84]);
station("stove", 382, 96, 110, 84, t({ es: "Fogón 2", en: "Stove 2" }), [70, 70, 84]);
const plate = station("plate", 130, 226, 160, 90, t({ es: "Plato", en: "Plate" }), [200, 200, 210], { items: [] }).items;
station("trash", 306, 246, 80, 70, t({ es: "Basura", en: "Bin" }), [60, 60, 60]);
station("serve", 402, 226, 226, 90, t({ es: "Servir (entregar el plato)", en: "Serve (hand in the plate)" }), [60, 110, 80]);
// #endregion
// #region cooking
// Stations work on their own once loaded. The board finishes and waits;
// the stove keeps going: cooked at COOK_TIME, burnt at BURN_TIME.
const ready = (s) => (s.type === "board" ? s.item.state === "chopped" : s.item.state !== ACCEPTS.stove[s.item.kind]);
onUpdate("station", (s) => {
if (!s.item || (s.type !== "board" && s.type !== "stove")) return;
s.time += dt();
if (s.type === "board" && s.time >= CHOP_TIME) s.item.state = "chopped";
if (s.type === "stove" && s.time >= COOK_TIME && s.item.state !== "burnt") s.item.state = "cooked";
if (s.type === "stove" && s.time >= BURN_TIME) s.item.state = "burnt";
});
function drawProgress(s, w, h) {
const limit = s.type === "board" ? CHOP_TIME : s.time < COOK_TIME ? COOK_TIME : BURN_TIME;
const k = Math.min(1, s.time / limit);
const danger = s.type === "stove" && s.time >= COOK_TIME && s.item.state !== "burnt";
// Once cooked, the bar refills in red: the time left before it burns. It blinks near the end.
const blink = danger && s.time > BURN_TIME - 2 && Math.floor(time() * 6) % 2;
drawRect({ pos: vec2(8, h - 12), width: (w - 16) * k, height: 6, color: danger ? rgb(240, 70, 50) : rgb(120, 220, 120), opacity: blink ? 0.3 : 1 });
}
// #endregion
// #region actions
// The whole game is two clicks: pick something up, put it somewhere.
function use(s) {
if (s.type === "crate") return hand ? notify(t({ es: "Ya llevas algo en la mano.", en: "Your hands are full." })) : (hand = { kind: s.kind, state: "raw" });
if (s.type === "board" || s.type === "stove") {
if (!hand && s.item) return ready(s) ? ((hand = s.item), (s.item = null)) : notify(t({ es: "Todavía no está listo.", en: "Not ready yet." }));
if (!hand || s.item) return;
const need = ACCEPTS[s.type][hand.kind];
if (need === hand.state) return (s.item = hand), (s.time = 0), (hand = null);
if (s.type === "stove" && hand.kind === "tomato" && hand.state === "raw")
return notify(t({ es: "Primero pícalo en la tabla: cada estación tiene su turno.", en: "Chop it on the board first: each station has its turn." }));
return notify(t({ es: `${itemName(hand)}: aquí no.`, en: `${itemName(hand)}: not here.` }));
}
if (s.type === "plate" && hand) {
if (hand.state === "burnt") return notify(t({ es: "Quemado no se sirve: tíralo a la basura.", en: "Burnt food can't be served: bin it." }));
if (plate.length >= 3) return notify(t({ es: "El plato está lleno.", en: "The plate is full." }));
plate.push(hand);
hand = null;
}
if (s.type === "trash") hand ? (hand = null) : (plate.length = 0);
if (s.type === "serve") serve();
}
// #endregion
// #region orders
// Orders arrive with a timer. Serving early scores more; a lost order counts against you.
// Each order is a game object: its ticket is drawn in arrival order.
function spawnOrder() {
if (get("order").length < 3) add([pos(0, 8), z(5), "order", { recipe: choose(RECIPES), left: ORDER_TIME, draw: drawTicket }]);
}
loop(ORDER_EVERY, spawnOrder); // the first one comes right away
onUpdate(() => get("order").forEach((o, i) => (o.pos.x = 12 + i * 152)));
onUpdate("order", (o) => {
o.left -= dt();
if (o.left <= 0) {
destroy(o);
lost++;
notify(t({ es: `Se fue un cliente sin su ${o.recipe.name.toLowerCase()}.`, en: `A customer left without their ${o.recipe.name.toLowerCase()}.` }));
if (lost >= MAX_LOST) go("end", false, served, score);
}
});
function serve() {
const order = get("order").find((o) => [...o.recipe.needs].sort().join(",") === keyOf(plate));
if (!plate.length) return notify(t({ es: "El plato está vacío.", en: "The plate is empty." }));
if (!order) return notify(t({ es: "Nadie pidió ese plato: revisa los pedidos.", en: "Nobody ordered that: check the orders." }));
destroy(order);
plate.length = 0;
served++;
score += 10 + Math.ceil(order.left);
notify(t({ es: `Pedido servido: ${order.recipe.name.toLowerCase()}.`, en: `Order served: ${order.recipe.name.toLowerCase()}.` }));
if (served >= GOAL) go("end", true, served, score);
}
// #endregion
// #region hud
// Order tickets, counters, a message line and what you carry, drawn every frame.
function drawTicket() {
drawRect({ width: 144, height: 74, radius: 4, color: rgb(250, 244, 225) });
drawText({ text: this.recipe.name, size: 14, font: FONT, pos: vec2(8, 6), color: rgb(40, 30, 30) });
this.recipe.needs.forEach((n, j) => {
const [kind, state] = n.split(":");
drawItem({ kind, state }, vec2(24 + j * 44, 42));
});
const k = this.left / ORDER_TIME;
drawRect({ pos: vec2(6, 64), width: 132 * k, height: 5, color: k > 0.35 ? rgb(90, 170, 90) : rgb(220, 70, 50) });
}
onDraw(() => {
drawText({ text: t({ es: `Servidos ${served}/${GOAL}\nPerdidos ${lost}/${MAX_LOST}\nPuntos ${score}`, en: `Served ${served}/${GOAL}\nLost ${lost}/${MAX_LOST}\nScore ${score}` }),
size: 14, font: FONT, pos: vec2(520, 12), color: rgb(240, 230, 210) });
drawText({ text: t({ es: "En la mano: ", en: "Holding: " }) + (hand ? itemName(hand) : "—"), size: 14, font: FONT, pos: vec2(130, 194), color: rgb(255, 230, 160) });
drawText({ text: notice, size: 13, font: FONT, pos: vec2(130, 326), width: 500, color: rgb(255, 190, 140) });
if (hand) drawItem(hand, mousePos().add(16, 16));
});
// #endregion
});
// #region scenes
function screen(title, subtitle, next) {
add([text(title, { size: 32, font: FONT, align: "center" }), pos(center().sub(0, 70)), anchor("center")]);
add([text(subtitle, { size: 15, font: FONT, align: "center", width: 580 }), pos(center().add(0, 30)), anchor("center"), color(240, 225, 210)]);
onButtonPress("start", next);
onClick(next);
}
scene("title", () =>
screen(
t({ es: "Cocina a contrarreloj", en: "Kitchen rush" }),
t({
es: `Haz clic en un cajón para tomar un ingrediente y luego en una estación para dejarlo.\nLa tabla pica; el fogón cocina y, si te descuidas, quema. Arma el plato y sírvelo.\nCada pedido tiene su tiempo. Sirve ${GOAL} antes de perder ${MAX_LOST}.\nClic o espacio para empezar.`,
en: `Click a crate to pick up an ingredient, then a station to put it down.\nThe board chops; the stove cooks and, if you are careless, burns. Build the plate and serve it.\nEach order has a timer. Serve ${GOAL} before you lose ${MAX_LOST}.\nClick or press space to start.`,
}),
() => go("game"),
),
);
scene("end", (won, served, score) =>
screen(
won ? t({ es: "¡Servicio completo!", en: "Service complete!" }) : t({ es: "Cerró la cocina", en: "The kitchen closed" }),
t({ es: `Platos servidos: ${served}. Puntos: ${score}. Clic para otro turno.`, en: `Dishes served: ${served}. Score: ${score}. Click for another shift.` }),
() => go("game"),
),
);
go("title");
// #endregionDesign challenges
- Pacing the orders so there is pressure without impossible chaos. The difficulty comes from how many parallel tasks the game demands.
- Making the state of each ingredient and station (raw, chopped, cooked, about to burn) readable at a glance.
- Designing recipes that force planning, for example a long step that is best started first, instead of always repeating the same thing.
- In co-op, building kitchens that force players to coordinate and share out the work.
History
- 1982OriginBurgerTime
BurgerTime already had a chef who builds burgers by dropping the ingredients onto the buns, although it plays as a platformer. We found no history of the genre that names a first recipe game, so this origin is provisional.
BurgerTime (Wikipedia) - 1993
Motoko-chan no Wonder Kitchen, for the Super Famicom, has a kitchen screen where you prepare meals with the ingredients provided.
Motoko-chan no Wonder Kitchen (Wikipedia) - 2006Cooking Mama
Cooking Mama turns each step of a recipe into a minigame.
Cooking Mama (video game) (Wikipedia) - 2007Papa's Pizzeria
Papa's Pizzeria, a Flash browser game, starts a long Flipline Studios series in which the work is split across stations for taking orders, preparing, cooking and serving.
Papa Louie (Wikipedia) - 2012Cook, Serve, Delicious!
Cook, Serve, Delicious! spreads orders across several stations at once and turns each recipe into a sequence of key presses; dishes are graded by the steps you skipped or got wrong.
Cook, Serve, Delicious! (Wikipedia) - 2016Overcooked
Overcooked makes cooking a cooperative frenzy: several chefs prepare orders against the clock.
Overcooked (Wikipedia) - 2019Cooking Simulator
Cooking Simulator, by Polish studio Big Cheese, goes for realism, with a physics engine and over 80 recipes.
Cooking Simulator (Wikipedia) - 2022PlateUp!
PlateUp! mixes running a co-op restaurant with roguelike elements, such as 15-day runs with random upgrades.
PlateUp! (Wikipedia) - 2023Venba
Venba uses cooking to tell a story, and each lost recipe that has to be rebuilt by trial and error is a puzzle about a family.
Venba (video game) (Wikipedia)
Reference games
1982
BurgerTime
You are a chef running across platforms and ladders; walking over each giant ingredient (bun, patty, tomato, lettuce, cheese) drops it one floor until the burgers are assembled, while you dodge the food items chasing you.
2006
Cooking Mama
Each step of a recipe (chopping vegetables, slicing, flipping in the pan, plating) is a minigame of a few seconds that you play with the stylus on the touch screen, and at the end the dish gets a medal based on your precision.
2007
Papa's Pizzeria
You run a pizzeria by splitting the work across stations (taking the order, preparing, baking and serving) and earn tips based on how well you fill each order. It is the first of Flipline's series of time management games.
2012
Cook, Serve, Delicious!
You run a restaurant with up to eight stations at once. Each order is prepared by pressing its recipe's key sequence, and the grade drops if you skip steps, add the wrong ingredients or cook it badly.
2016
Overcooked
Up to four chefs prepare, cook, serve and wash up in kitchens whose layout changes every level. Orders expire quickly, so everything depends on sharing out the work and talking.
2017
Get That Order!
A prototype made for Ludum Dare 40. You pick up ingredients with the mouse, cook them in the frying pan (they burn if you do not take them off in time) and serve each order before more pile up.
2019
Cooking Simulator
You cook in first person in a physics-simulated restaurant kitchen, where every ingredient and utensil is handled by hand, with over 80 recipes.
2022
PlateUp!
One to four players run a restaurant (taking orders, cooking, serving, washing dishes) over 15-day runs, choosing random upgrades and difficulty modifiers between days.
2023
Little Chef: Prototype
A 2D physics-based cooking game, played with the mouse alone, that invites you to experiment with ingredients and discover recipes. It is the free prototype that led to Little Chef Cozy Cooking.
2023
Venba
A narrative game in which you rebuild lost family recipes by trial and error. Each recipe is a puzzle, and between them you choose dialogue that tells the story of a migrant family.
Exercise
Play the demo and note when the chaos begins. Then change ORDER_EVERY and BURN_TIME until you find the point where it is demanding but fair. Finally, design a new recipe that forces you to use both stations in order.