
Cocina
Preparas platos siguiendo recetas: cortas, mezclas, cocinas y sirves a tiempo. El reto suele ser la precisión de cada paso o atender muchos pedidos a la vez sin que se queme nada.
También se llama: juego de cocina, simulación de cocina
Mecánicas que lo definen
- Recetas como secuencias de pasos, en las que cada ingrediente pasa por estaciones (cortar, cocinar, emplatar) en un orden fijo.
- Estaciones que trabajan con el tiempo. Lo que pones al fuego se cocina solo y, si te descuidas, se quema.
- Pedidos con temporizador que llegan en cola. Entregar pronto da más puntos y dejar que caduquen penaliza.
- Manos limitadas. Llevas una cosa a la vez, así que el reto está en el orden y la ruta más que en un solo gesto.
- En la variante de minijuegos, cada paso de la receta es un gesto preciso (cortar, batir, voltear) que el juego califica.
Bucle de juego
Leer el pedido, decidir qué preparar primero, poner los ingredientes en marcha en las estaciones, vigilar lo que se cocina, emplatar y entregar antes de que se acabe el tiempo.
Demo propia
Controles. Todo se juega con el ratón o el dedo. Haz clic en un cajón para tomar un ingrediente y luego en una estación para dejarlo; vuelve a hacer clic en la estación para recogerlo cuando esté listo. Clic en el plato para emplatar, en la basura para tirar lo que llevas (o vaciar el plato) y en Servir para entregar.
Abrir en pantalla completaUna cocina con cajones de ingredientes, una tabla para picar, dos fogones, un plato, una basura y una ventanilla para servir. Llegan pedidos con temporizador, y cada uno exige pasar los ingredientes por las estaciones en el orden correcto, vigilar que nada se queme y entregar a tiempo.
Hecha con Kaplay, en unas 170 líneas de JavaScript.
Cómo está hecha
Configuración y ritmo
Las constantes son las perillas del diseño. CHOP_TIME y COOK_TIME marcan cuánto tarda cada paso, BURN_TIME cuánto margen tienes antes de quemar, y ORDER_TIME y ORDER_EVERY cuánta presión hay. Con estos números se ajusta casi toda la dificultad.
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 expireIngredientes, recetas y estados
Cada ingrediente tiene un estado (raw, chopped, cooked, burnt) y cada receta es una lista de pares «ingrediente:estado». ACCEPTS dice qué acepta cada estación y en qué estado, y de ahí sale el orden obligatorio: la sopa necesita el tomate picado antes de que el fogón lo acepte. drawItem dibuja cada estado de forma distinta para que se lea de un vistazo.
// 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) } }); }Estaciones
Cada lugar de la cocina (cajones, tabla, fogones, plato, basura, ventanilla) es una caja con area() que responde al clic, y una sola función crea todas. Cada estación guarda lo que tiene encima y se dibuja a sí misma.
// 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]);El tiempo cocina (y quema)
Una vez cargada, la estación trabaja sola. La tabla termina y espera, pero el fogón sigue, porque cocina en COOK_TIME y quema en BURN_TIME. La barra verde muestra el avance y, cuando la comida está lista, se vuelve roja y parpadea al final. Esa ventana entre cocido y quemado es la que obliga a vigilar varias cosas a la vez.
// 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 }); }Tomar y dejar
Todo el juego son dos clics, tomar algo y dejarlo en algún sitio. Como solo llevas una cosa a la vez, el reto está en el orden. Si intentas algo que no corresponde, el juego explica por qué (por ejemplo, «primero pícalo en la tabla»), y la comida quemada solo puede ir a la basura.
// 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(); }Pedidos con temporizador
Cada pedido es un objeto con su receta y su tiempo restante. Llega uno nuevo cada ORDER_EVERY segundos, con un máximo de tres en espera. Al servir se busca un pedido cuya receta coincida exactamente con el plato, y entregar pronto suma más puntos. Si el tiempo de un pedido llega a cero se pierde, y tres pedidos perdidos terminan el turno.
// 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); }Fichas y marcador
Las fichas muestran qué pide cada cliente con los mismos iconos que los ingredientes, además de una barra de tiempo que se vuelve roja al final. Debajo se ve qué llevas en la mano y un mensaje con la última explicación.
// 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)); });Escenas
Tres escenas (title, game y end). Se gana sirviendo GOAL platos y se pierde dejando que caduquen MAX_LOST pedidos.
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");
Qué simplificamos
- Solo hay cuatro ingredientes, tres recetas y dos tipos de estación. Picar es automático, mientras que en muchos juegos del género hay que mantener pulsado o repetir un gesto.
- El cocinero no camina, porque un clic lo lleva a cualquier estación. En juegos como Overcooked moverse por la cocina es una parte central del reto.
- No hay dinero, mejoras ni varios días de servicio.
Para modificarla
- Haz que picar requiera mantener pulsado el clic sobre la tabla. ¿Cómo cambia la sensación de prisa?
- Añade una receta de tres ingredientes y un tercer tipo de estación (por ejemplo, un horno más lento).
- Añade un segundo cocinero controlado con el teclado y conviértelo en un juego cooperativo.
Código completo
// 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");
// #endregionRetos de diseño
- Dosificar el ritmo de los pedidos para que haya presión sin caos imposible. La dificultad viene de cuántas tareas en paralelo exige el juego.
- Que el estado de cada ingrediente y estación (crudo, picado, cocido, a punto de quemarse) se lea de un vistazo.
- Diseñar recetas que obliguen a planificar, por ejemplo un paso largo que conviene empezar primero, en vez de repetir siempre lo mismo.
- En cooperativo, armar cocinas que obliguen a coordinarse y repartirse el trabajo.
Historia
- 1982OrigenBurgerTime
BurgerTime ya tenía un cocinero que arma hamburguesas haciendo caer los ingredientes sobre el pan, aunque se juega como un juego de plataformas. No encontramos una historia del género que fije un primer juego de recetas, así que este origen es provisional.
BurgerTime (Wikipedia) - 1993
Motoko-chan no Wonder Kitchen, para Super Famicom, tiene una pantalla de cocina en la que preparas comidas con los ingredientes disponibles.
Motoko-chan no Wonder Kitchen (Wikipedia) - 2006Cooking Mama
Cooking Mama convierte cada paso de una receta en un minijuego.
Cooking Mama (video game) (Wikipedia) - 2007Papa's Pizzeria
Papa's Pizzeria, un juego Flash para el navegador, abre una larga serie de Flipline Studios en la que el trabajo se reparte entre estaciones para tomar pedidos, preparar, cocinar y servir.
Papa Louie (Wikipedia) - 2012Cook, Serve, Delicious!
Cook, Serve, Delicious! reparte los pedidos entre varias estaciones a la vez y convierte cada receta en una secuencia de teclas; los platos se califican según los pasos que te saltaste o hiciste mal.
Cook, Serve, Delicious! (Wikipedia) - 2016Overcooked
Overcooked hace de la cocina un caos cooperativo: varios chefs preparan pedidos contra el reloj.
Overcooked (Wikipedia) - 2019Cooking Simulator
Cooking Simulator, del estudio polaco Big Cheese, apuesta por el realismo, con un motor de física y más de 80 recetas.
Cooking Simulator (Wikipedia) - 2022PlateUp!
PlateUp! mezcla la gestión de un restaurante cooperativo con elementos roguelike, como partidas de 15 días con mejoras al azar.
PlateUp! (Wikipedia) - 2023Venba
Venba usa la cocina para contar una historia, y cada receta perdida que hay que reconstruir por ensayo y error es un rompecabezas sobre una familia.
Venba (video game) (Wikipedia)
Juegos de referencia
1982
BurgerTime
Eres un cocinero que recorre plataformas y escaleras; al pasar sobre cada ingrediente gigante (pan, carne, tomate, lechuga, queso) lo haces caer un piso hasta que las hamburguesas quedan armadas, mientras esquivas a los alimentos que te persiguen.
2006
Cooking Mama
Cada paso de una receta (picar verduras, cortar, voltear en la sartén, emplatar) es un minijuego de pocos segundos que haces con el lápiz en la pantalla táctil, y al final el plato recibe una medalla según tu precisión.
2007
Papa's Pizzeria
Atiendes una pizzería repartiendo el trabajo entre estaciones (tomar el pedido, preparar, hornear y servir) y cobras propinas según lo bien que cumples cada pedido. Es la primera de la serie de juegos de gestión del tiempo de Flipline.
2012
Cook, Serve, Delicious!
Llevas un restaurante con hasta ocho estaciones a la vez. Cada pedido se prepara pulsando la secuencia de teclas de su receta, y la nota baja si te saltas pasos, pones ingredientes de más o lo cocinas mal.
2016
Overcooked
Hasta cuatro cocineros preparan, cocinan, sirven y lavan en cocinas que cambian de forma en cada nivel. Los pedidos caducan en poco tiempo, así que todo depende de repartirse el trabajo y hablar.
2017
Get That Order!
Prototipo hecho para la Ludum Dare 40. Recoges ingredientes con el ratón, los cocinas en la sartén (si no los retiras a tiempo se queman) y sirves cada pedido antes de que se acumulen más.
2019
Cooking Simulator
Cocinas en primera persona en una cocina de restaurante simulada con física, donde cada ingrediente y utensilio se manipula a mano, con más de 80 recetas.
2022
PlateUp!
De uno a cuatro jugadores atienden un restaurante (tomar pedidos, cocinar, servir, lavar platos) durante partidas de 15 días, y entre día y día eligen mejoras y dificultades al azar.
2023
Little Chef: Prototype
Juego de cocina en 2D con física, que se juega solo con el ratón y que invita a experimentar con los ingredientes y descubrir recetas. Es el prototipo gratuito del que salió Little Chef Cozy Cooking.
2023
Venba
Juego narrativo en el que reconstruyes por ensayo y error recetas familiares perdidas. Cada receta es un rompecabezas, y entre una y otra eliges diálogos que cuentan la historia de una familia migrante.
Ejercicio
Juega la demo y anota en qué momento empieza el caos. Después cambia ORDER_EVERY y BURN_TIME hasta encontrar el punto en que es exigente pero justo. Por último, diseña una receta nueva que obligue a usar las dos estaciones en orden.