Saltar al contenido
GameGenresEnglish
Juego incremental

Juego incremental

Juego en el que repites una acción sencilla, como hacer clic, para juntar un recurso que luego inviertes en mejoras que lo producen solas, cada vez más rápido. El juego avanza incluso cuando no juegas, y el reto es decidir en qué invertir y cuándo reiniciar a cambio de bonificaciones permanentes.

También se llama: idle, juego idle, clicker, inactividad

Mecánicas que lo definen

  • Una acción manual mínima, como un clic o un toque, que produce el recurso. Es lo único que importa al principio y casi nada después.
  • Generadores que producen solos y cuyo precio crece de forma exponencial con cada copia (por ejemplo, base × 1,15^n), mientras su producción solo crece de forma lineal.
  • Mejoras que multiplican la producción y desbloqueos que abren capas nuevas (otros recursos, edificios o zonas).
  • Progreso que sigue sin el jugador, en segundo plano o calculado al volver según el tiempo transcurrido.
  • Reinicio con prestigio. Borras el progreso a cambio de una moneda o bonificación permanente que acelera la partida siguiente.

Bucle de juego

Producir, gastar en lo que más aumenta la producción, esperar (o hacer clic) hasta la siguiente compra y, cuando el avance se estanca, reiniciar con prestigio para volver a subir más rápido.

Demo propia

Controles. Solo ratón o dedo. Haz clic o toca el círculo para producir y toca un botón para comprar. Espacio o Enter solo sirven para empezar.

Abrir en pantalla completa

Haces clic en un círculo para producir energía y la gastas en cuatro generadores que producen solos, en mejoras que multiplican y, cuando conviene, en un reinicio con prestigio. Lo justo para sentir el crecimiento exponencial que define al género.

Hecha con Kaplay, en unas 158 líneas de JavaScript.

Cómo está hecha

  1. Configuración

    Se crea el juego y se definen las constantes que más cambian el ritmo, que son la meta, el crecimiento de precios (15 %) y el bono del prestigio. El prestigio se guarda fuera de la escena, así sobrevive al reinicio.

    kaplay({
      width: 640,
      height: 360,
      letterbox: true, pixelDensity,
      background: [22, 24, 38],
      // Everything is played with the mouse or a finger; the keyboard only starts.
      buttons: { start: { keyboard: ["space", "enter"] } },
    });
    
    const GOAL = 1e6; // collect 1 M energy to win
    const COST_GROWTH = 1.15; // each copy of a generator costs 15% more than the last
    const PRESTIGE_BONUS = 0.5; // +50% production per prestige point, forever
    let prestige = 0; // lives outside the scene, so it survives a reset
    let resets = 0;
    let startTime = 0;
  2. Números grandes

    Los números enormes son el paisaje del género, y hay que poder leerlos: 1234567 se muestra como 1.23 M. Se divide por 1000 tantas veces como se pueda y se añade la letra (K, M, B, T). Ojo: B es la escala corta inglesa, 10^9, que en español son mil millones.

    // Big numbers are the genre's scenery: show 1.23 K / 4.56 M instead of 1234567.
    // K = thousand, M = million, B = billion (short scale, 10^9), T = 10^12.
    const UNITS = ["", "K", "M", "B", "T"];
    function fmt(n) {
      if (n < 10) return n.toFixed(1).replace(/\.0$/, "");
      if (n < 1000) return String(Math.floor(n));
      const i = Math.min(Math.floor(Math.log10(n) / 3), UNITS.length - 1);
      return +(n / 1000 ** i).toFixed(2) + " " + UNITS[i]; // the + drops trailing zeros
    }
  3. Generadores y precio exponencial

    Cada generador cuesta base × 1,15^n, donde n es cuántos tienes. El recolector cuesta 10, luego 12, luego 14 (se redondea hacia arriba)… y el vigésimo ya cuesta 143. La producción, en cambio, sube de forma lineal (cada copia suma lo mismo), así que cada compra rinde un poco menos que la anterior. Por eso llega un momento en que conviene saltar al generador siguiente, que es caro pero rinde mucho más. Esa tensión entre comprar «más de lo mismo» o «algo nuevo» es el corazón del diseño.

    // Each generator produces on its own. Its price follows the classic formula
    //   cost = base × 1.15^owned
    // so price grows exponentially while production grows linearly: every new
    // copy is a little worse value, and sooner or later the next tier is better.
    const generators = [
      { name: t({ es: "Recolector", en: "Collector" }), base: 10, rate: 1 },
      { name: t({ es: "Dron", en: "Drone" }), base: 100, rate: 10 },
      { name: t({ es: "Fábrica", en: "Factory" }), base: 1000, rate: 80 },
      { name: t({ es: "Reactor", en: "Reactor" }), base: 10000, rate: 600 },
    ].map((g) => ({ ...g, owned: 0, mult: 1 }));
    const cost = (g) => Math.ceil(g.base * COST_GROWTH ** g.owned);
    const prestigeMult = () => 1 + PRESTIGE_BONUS * prestige;
    const perSecond = () =>
      generators.reduce((sum, g) => sum + g.owned * g.rate * g.mult, 0) * globalMult * prestigeMult();
  4. El bucle de producción

    En cada fotograma se suma la producción por segundo multiplicada por dt(), el tiempo transcurrido. Así el juego produce lo mismo a 30 o a 144 fotogramas por segundo, y la misma cuenta sirve para calcular lo producido mientras el jugador estuvo fuera.

    // The core loop: every frame adds (production per second × seconds elapsed).
    // dt() makes it independent of the frame rate: 30 or 144 fps give the same.
    function earn(amount) {
      energy += amount;
      runTotal += amount;
      if (energy >= GOAL && !won) {
        won = true;
        go("end", resets, time() - startTime);
      }
    }
    onUpdate(() => earn(perSecond() * dt()));
  5. El clic

    El clic es la única acción activa. Al principio lo es todo, pero pronto los generadores producen miles por segundo y el clic pasa a ser casi decorativo. El pequeño rebote del círculo y el número que sube dan respuesta inmediata a cada toque, y eso hace que se sienta bien.

    // The click is the only "active" action: essential at the start, almost
    // irrelevant later, when generators produce thousands per second.
    const orb = add([circle(64), pos(130, 200), anchor("center"), color(90, 200, 255), outline(6, rgb(40, 110, 170)), area(), scale(1)]);
    orb.onClick(() => {
      const gain = clickPower * prestigeMult();
      earn(gain);
      orb.scale = vec2(0.92);
      add([text("+" + fmt(gain), { size: 18, font: FONT }), pos(mousePos()), anchor("center"), color(255, 240, 160), opacity(1), move(UP, 60), lifespan(0.6, { fade: 0.4 })]);
    });
    orb.onUpdate(() => (orb.scale = orb.scale.lerp(vec2(1), dt() * 10)));
    orb.add([text(t({ es: "¡Clic!", en: "Click!" }), { size: 20, font: FONT }), anchor("center"), color(20, 40, 70)]);
  6. Mejoras que multiplican

    Las mejoras multiplican en vez de sumar. Un ×2 sobre algo de lo que tienes 40 copias vale más que varias compras nuevas. Que la mejora siguiente siempre esté a la vista da una meta cercana, y por qué engancha se resume en eso, en tener siempre la siguiente compra a punto de alcanzarse.

    // Upgrades multiply instead of adding: a ×2 on something you own a lot of
    // is worth more than several new generators. They unlock in order.
    const upgrades = [
      { cost: 50, label: t({ es: "Clic ×2", en: "Click ×2" }), apply: () => (clickPower *= 2) },
      { cost: 250, label: t({ es: "Recolectores ×3", en: "Collectors ×3" }), apply: () => (generators[0].mult *= 3) },
      { cost: 1500, label: t({ es: "Drones ×2", en: "Drones ×2" }), apply: () => (generators[1].mult *= 2) },
      { cost: 6000, label: t({ es: "Clic ×5", en: "Click ×5" }), apply: () => (clickPower *= 5) },
      { cost: 20000, label: t({ es: "Fábricas ×2", en: "Factories ×2" }), apply: () => (generators[2].mult *= 2) },
      { cost: 80000, label: t({ es: "Reactores ×2", en: "Reactors ×2" }), apply: () => (generators[3].mult *= 2) },
      { cost: 300000, label: t({ es: "Todo ×2", en: "Everything ×2" }), apply: () => (globalMult *= 2) },
    ];
    let nextUpgrade = 0;
  7. Reinicio con prestigio

    Al reiniciar pierdes todo menos los puntos de prestigio, que suman +50 % de producción para siempre. Los puntos crecen con la raíz cuadrada de lo producido: con 10 K ganas 1 y con 40 K, 2. Duplicar la partida no duplica los puntos, así que la decisión es cuándo reiniciar. Si lo haces pronto, ganas poco, y si esperas demasiado, pierdes tiempo en una partida que ya se estancó. Volver a subir mucho más rápido que la primera vez es la recompensa del género.

    // Resetting throws away this run in exchange for permanent points.
    // The gain grows with the square root of what you produced: doubling the
    // run does not double the points, so it pays to reset neither too early
    // nor too late. That decision is the genre's long-term strategy.
    const prestigeGain = () => Math.floor(Math.sqrt(runTotal / 10000));
    function doPrestige() {
      if (prestigeGain() < 1) return;
      prestige += prestigeGain();
      resets++;
      go("game"); // the scene starts from zero, but `prestige` remains
    }
  8. El panel

    Cada botón se pinta cada fotograma a partir del estado. Lo que puedes pagar se ilumina y lo demás se apaga. En un juego incremental la interfaz es casi todo el juego, porque informa del precio, de la producción y de lo que falta para la siguiente meta.

    // A panel of buttons rebuilt each frame from the state: what you can afford
    // is lit up, the rest is dimmed.
    const hud = add([text("", { size: 22, font: FONT }), pos(130, 40), anchor("center")]);
    const sub = add([text("", { size: 14, font: FONT, align: "center" }), pos(130, 76), anchor("center"), color(170, 190, 230)]);
    function button(x, y, w, h, onPress) {
      const b = add([rect(w, h, { radius: 6 }), pos(x, y), color(44, 50, 78), outline(2, rgb(90, 100, 150)), area()]);
      const label = b.add([text("", { size: 13, font: FONT, width: w - 16 }), pos(8, h / 2), anchor("left")]);
      b.onClick(onPress);
      return { b, label };
    }
    const genButtons = generators.map((g, i) =>
      button(280, 14 + i * 56, 344, 48, () => {
        if (energy < cost(g)) return;
        energy -= cost(g);
        g.owned++;
      }),
    );
    const upButton = button(280, 244, 344, 40, () => {
      const u = upgrades[nextUpgrade];
      if (!u || energy < u.cost) return;
      energy -= u.cost;
      u.apply();
      nextUpgrade++;
    });
    const prestigeButton = button(280, 294, 344, 52, doPrestige);
    const paint = (btn, enabled) => {
      btn.b.color = enabled ? rgb(60, 80, 130) : rgb(36, 40, 60);
      btn.label.color = enabled ? rgb(255, 255, 255) : rgb(130, 135, 160);
    };
    
    onUpdate(() => {
      hud.text = fmt(energy) + " " + t({ es: "energía", en: "energy" });
      sub.text = `${fmt(perSecond())} ${t({ es: "por segundo", en: "per second" })}\n${t({ es: "Meta", en: "Goal" })}: ${fmt(GOAL)} · ${t({ es: "Prestigio", en: "Prestige" })}: ${prestige} (×${prestigeMult().toFixed(1)})`;
      generators.forEach((g, i) => {
        const each = g.rate * g.mult * globalMult * prestigeMult();
        genButtons[i].label.text = `${g.name} ×${g.owned}  ·  +${fmt(each)}/s ${t({ es: "c/u", en: "each" })}\n${t({ es: "Precio", en: "Price" })}: ${fmt(cost(g))}`;
        paint(genButtons[i], energy >= cost(g));
      });
      const u = upgrades[nextUpgrade];
      upButton.label.text = u
        ? `${t({ es: "Mejora", en: "Upgrade" })}: ${u.label}  ·  ${t({ es: "Precio", en: "Price" })}: ${fmt(u.cost)}`
        : t({ es: "No quedan mejoras", en: "No upgrades left" });
      paint(upButton, u && energy >= u.cost);
      const gain = prestigeGain();
      prestigeButton.label.text = gain >= 1
        ? t({ es: "Reiniciar con prestigio", en: "Prestige reset" }) + ` (+${gain}): ${t({ es: "producción", en: "production" })} ×${(1 + PRESTIGE_BONUS * (prestige + gain)).toFixed(1)}`
        : t({ es: `Reinicio con prestigio: produce ${fmt(10000 - runTotal)} más para desbloquearlo`, en: `Prestige reset: produce ${fmt(10000 - runTotal)} more to unlock it` });
      paint(prestigeButton, gain >= 1);
    });
  9. Escenas

    Hay tres escenas, title, game y end. El reinicio con prestigio no es más que volver a entrar en game con go(), porque la escena arranca desde cero y solo la variable prestige, que vive fuera, recuerda lo ganado.

    function screen(title, subtitle, next) {
      add([text(title, { size: 34, font: FONT, align: "center" }), pos(center().sub(0, 50)), anchor("center")]);
      add([
        text(subtitle, { size: 16, font: FONT, align: "center", width: 560 }),
        pos(center().add(0, 30)),
        anchor("center"),
        color(200, 210, 255),
      ]);
      onButtonPress("start", next);
      onClick(next);
    }
    
    const newGame = () => {
      prestige = 0;
      resets = 0;
      startTime = time();
      go("game");
    };
    
    scene("title", () =>
      screen(
        t({ es: "Juego incremental", en: "Incremental game" }),
        t({
          es: "Haz clic en el círculo para producir energía y gástala en generadores que producen solos.\nLlega a 1 M. Cuando puedas, reinicia con prestigio para ir más rápido.\nHaz clic o pulsa espacio para empezar.",
          en: "Click the circle to make energy and spend it on generators that produce on their own.\nReach 1 M. When you can, do a prestige reset to go faster.\nClick or press space to start.",
        }),
        newGame,
      ),
    );
    
    scene("end", (resetCount, seconds) =>
      screen(
        t({ es: "¡1 M de energía!", en: "1 M energy!" }),
        t({
          es: `Lo lograste en ${Math.round(seconds)} s con ${resetCount} reinicios con prestigio.\n¿Lo harías más rápido reiniciando antes o después? Haz clic para repetir.`,
          en: `You did it in ${Math.round(seconds)} s with ${resetCount} prestige resets.\nWould resetting earlier or later be faster? Click to play again.`,
        }),
        newGame,
      ),
    );
    
    go("title");

Qué simplificamos

  • No se guarda la partida y no hay progreso mientras la pestaña está cerrada; en un juego real es casi obligatorio.
  • Las mejoras aparecen de una en una y en orden fijo, sin árbol de mejoras ni logros.
  • Hay una meta (1 M) para que la partida termine; muchos juegos del género no tienen final.
  • Los números caben en los de JavaScript; pasado 1,8 × 10^308 haría falta una biblioteca de números grandes.

Para modificarla

  • Guarda la partida con localStorage y, al volver, suma lo que se habría producido con el tiempo transcurrido (progreso sin conexión).
  • Añade un quinto generador y elige su precio base y su producción para que no sea ni obvio ni inútil comprarlo.
  • Cambia la raíz cuadrada del prestigio por una raíz cúbica y prueba si sigue conviniendo reiniciar.
Código completo
// Incremental game demo
// 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: [22, 24, 38],
  // Everything is played with the mouse or a finger; the keyboard only starts.
  buttons: { start: { keyboard: ["space", "enter"] } },
});

const GOAL = 1e6; // collect 1 M energy to win
const COST_GROWTH = 1.15; // each copy of a generator costs 15% more than the last
const PRESTIGE_BONUS = 0.5; // +50% production per prestige point, forever
let prestige = 0; // lives outside the scene, so it survives a reset
let resets = 0;
let startTime = 0;
// #endregion

// #region format
// Big numbers are the genre's scenery: show 1.23 K / 4.56 M instead of 1234567.
// K = thousand, M = million, B = billion (short scale, 10^9), T = 10^12.
const UNITS = ["", "K", "M", "B", "T"];
function fmt(n) {
  if (n < 10) return n.toFixed(1).replace(/\.0$/, "");
  if (n < 1000) return String(Math.floor(n));
  const i = Math.min(Math.floor(Math.log10(n) / 3), UNITS.length - 1);
  return +(n / 1000 ** i).toFixed(2) + " " + UNITS[i]; // the + drops trailing zeros
}
// #endregion

scene("game", () => {
  let energy = 0;
  let runTotal = 0; // everything produced in this run (prestige is based on it)
  let clickPower = 1;
  let globalMult = 1;
  let won = false;

  // #region generators
  // Each generator produces on its own. Its price follows the classic formula
  //   cost = base × 1.15^owned
  // so price grows exponentially while production grows linearly: every new
  // copy is a little worse value, and sooner or later the next tier is better.
  const generators = [
    { name: t({ es: "Recolector", en: "Collector" }), base: 10, rate: 1 },
    { name: t({ es: "Dron", en: "Drone" }), base: 100, rate: 10 },
    { name: t({ es: "Fábrica", en: "Factory" }), base: 1000, rate: 80 },
    { name: t({ es: "Reactor", en: "Reactor" }), base: 10000, rate: 600 },
  ].map((g) => ({ ...g, owned: 0, mult: 1 }));
  const cost = (g) => Math.ceil(g.base * COST_GROWTH ** g.owned);
  const prestigeMult = () => 1 + PRESTIGE_BONUS * prestige;
  const perSecond = () =>
    generators.reduce((sum, g) => sum + g.owned * g.rate * g.mult, 0) * globalMult * prestigeMult();
  // #endregion

  // #region production
  // The core loop: every frame adds (production per second × seconds elapsed).
  // dt() makes it independent of the frame rate: 30 or 144 fps give the same.
  function earn(amount) {
    energy += amount;
    runTotal += amount;
    if (energy >= GOAL && !won) {
      won = true;
      go("end", resets, time() - startTime);
    }
  }
  onUpdate(() => earn(perSecond() * dt()));
  // #endregion

  // #region clicking
  // The click is the only "active" action: essential at the start, almost
  // irrelevant later, when generators produce thousands per second.
  const orb = add([circle(64), pos(130, 200), anchor("center"), color(90, 200, 255), outline(6, rgb(40, 110, 170)), area(), scale(1)]);
  orb.onClick(() => {
    const gain = clickPower * prestigeMult();
    earn(gain);
    orb.scale = vec2(0.92);
    add([text("+" + fmt(gain), { size: 18, font: FONT }), pos(mousePos()), anchor("center"), color(255, 240, 160), opacity(1), move(UP, 60), lifespan(0.6, { fade: 0.4 })]);
  });
  orb.onUpdate(() => (orb.scale = orb.scale.lerp(vec2(1), dt() * 10)));
  orb.add([text(t({ es: "¡Clic!", en: "Click!" }), { size: 20, font: FONT }), anchor("center"), color(20, 40, 70)]);
  // #endregion

  // #region upgrades
  // Upgrades multiply instead of adding: a ×2 on something you own a lot of
  // is worth more than several new generators. They unlock in order.
  const upgrades = [
    { cost: 50, label: t({ es: "Clic ×2", en: "Click ×2" }), apply: () => (clickPower *= 2) },
    { cost: 250, label: t({ es: "Recolectores ×3", en: "Collectors ×3" }), apply: () => (generators[0].mult *= 3) },
    { cost: 1500, label: t({ es: "Drones ×2", en: "Drones ×2" }), apply: () => (generators[1].mult *= 2) },
    { cost: 6000, label: t({ es: "Clic ×5", en: "Click ×5" }), apply: () => (clickPower *= 5) },
    { cost: 20000, label: t({ es: "Fábricas ×2", en: "Factories ×2" }), apply: () => (generators[2].mult *= 2) },
    { cost: 80000, label: t({ es: "Reactores ×2", en: "Reactors ×2" }), apply: () => (generators[3].mult *= 2) },
    { cost: 300000, label: t({ es: "Todo ×2", en: "Everything ×2" }), apply: () => (globalMult *= 2) },
  ];
  let nextUpgrade = 0;
  // #endregion

  // #region prestige
  // Resetting throws away this run in exchange for permanent points.
  // The gain grows with the square root of what you produced: doubling the
  // run does not double the points, so it pays to reset neither too early
  // nor too late. That decision is the genre's long-term strategy.
  const prestigeGain = () => Math.floor(Math.sqrt(runTotal / 10000));
  function doPrestige() {
    if (prestigeGain() < 1) return;
    prestige += prestigeGain();
    resets++;
    go("game"); // the scene starts from zero, but `prestige` remains
  }
  // #endregion

  // #region interface
  // A panel of buttons rebuilt each frame from the state: what you can afford
  // is lit up, the rest is dimmed.
  const hud = add([text("", { size: 22, font: FONT }), pos(130, 40), anchor("center")]);
  const sub = add([text("", { size: 14, font: FONT, align: "center" }), pos(130, 76), anchor("center"), color(170, 190, 230)]);
  function button(x, y, w, h, onPress) {
    const b = add([rect(w, h, { radius: 6 }), pos(x, y), color(44, 50, 78), outline(2, rgb(90, 100, 150)), area()]);
    const label = b.add([text("", { size: 13, font: FONT, width: w - 16 }), pos(8, h / 2), anchor("left")]);
    b.onClick(onPress);
    return { b, label };
  }
  const genButtons = generators.map((g, i) =>
    button(280, 14 + i * 56, 344, 48, () => {
      if (energy < cost(g)) return;
      energy -= cost(g);
      g.owned++;
    }),
  );
  const upButton = button(280, 244, 344, 40, () => {
    const u = upgrades[nextUpgrade];
    if (!u || energy < u.cost) return;
    energy -= u.cost;
    u.apply();
    nextUpgrade++;
  });
  const prestigeButton = button(280, 294, 344, 52, doPrestige);
  const paint = (btn, enabled) => {
    btn.b.color = enabled ? rgb(60, 80, 130) : rgb(36, 40, 60);
    btn.label.color = enabled ? rgb(255, 255, 255) : rgb(130, 135, 160);
  };

  onUpdate(() => {
    hud.text = fmt(energy) + " " + t({ es: "energía", en: "energy" });
    sub.text = `${fmt(perSecond())} ${t({ es: "por segundo", en: "per second" })}\n${t({ es: "Meta", en: "Goal" })}: ${fmt(GOAL)} · ${t({ es: "Prestigio", en: "Prestige" })}: ${prestige} (×${prestigeMult().toFixed(1)})`;
    generators.forEach((g, i) => {
      const each = g.rate * g.mult * globalMult * prestigeMult();
      genButtons[i].label.text = `${g.name} ×${g.owned}  ·  +${fmt(each)}/s ${t({ es: "c/u", en: "each" })}\n${t({ es: "Precio", en: "Price" })}: ${fmt(cost(g))}`;
      paint(genButtons[i], energy >= cost(g));
    });
    const u = upgrades[nextUpgrade];
    upButton.label.text = u
      ? `${t({ es: "Mejora", en: "Upgrade" })}: ${u.label}  ·  ${t({ es: "Precio", en: "Price" })}: ${fmt(u.cost)}`
      : t({ es: "No quedan mejoras", en: "No upgrades left" });
    paint(upButton, u && energy >= u.cost);
    const gain = prestigeGain();
    prestigeButton.label.text = gain >= 1
      ? t({ es: "Reiniciar con prestigio", en: "Prestige reset" }) + ` (+${gain}): ${t({ es: "producción", en: "production" })} ×${(1 + PRESTIGE_BONUS * (prestige + gain)).toFixed(1)}`
      : t({ es: `Reinicio con prestigio: produce ${fmt(10000 - runTotal)} más para desbloquearlo`, en: `Prestige reset: produce ${fmt(10000 - runTotal)} more to unlock it` });
    paint(prestigeButton, gain >= 1);
  });
  // #endregion
});

// #region scenes
function screen(title, subtitle, next) {
  add([text(title, { size: 34, font: FONT, align: "center" }), pos(center().sub(0, 50)), anchor("center")]);
  add([
    text(subtitle, { size: 16, font: FONT, align: "center", width: 560 }),
    pos(center().add(0, 30)),
    anchor("center"),
    color(200, 210, 255),
  ]);
  onButtonPress("start", next);
  onClick(next);
}

const newGame = () => {
  prestige = 0;
  resets = 0;
  startTime = time();
  go("game");
};

scene("title", () =>
  screen(
    t({ es: "Juego incremental", en: "Incremental game" }),
    t({
      es: "Haz clic en el círculo para producir energía y gástala en generadores que producen solos.\nLlega a 1 M. Cuando puedas, reinicia con prestigio para ir más rápido.\nHaz clic o pulsa espacio para empezar.",
      en: "Click the circle to make energy and spend it on generators that produce on their own.\nReach 1 M. When you can, do a prestige reset to go faster.\nClick or press space to start.",
    }),
    newGame,
  ),
);

scene("end", (resetCount, seconds) =>
  screen(
    t({ es: "¡1 M de energía!", en: "1 M energy!" }),
    t({
      es: `Lo lograste en ${Math.round(seconds)} s con ${resetCount} reinicios con prestigio.\n¿Lo harías más rápido reiniciando antes o después? Haz clic para repetir.`,
      en: `You did it in ${Math.round(seconds)} s with ${resetCount} prestige resets.\nWould resetting earlier or later be faster? Click to play again.`,
    }),
    newGame,
  ),
);

go("title");
// #endregion

Retos de diseño

  • Equilibrar las curvas. Si los precios crecen mucho más rápido que la producción, el juego se estanca, y si crecen más lento, se rompe. Se suele modelar en una hoja de cálculo antes de programar.
  • Mantener el interés cuando la acción del jugador es mínima, revelando sistemas nuevos poco a poco para que siempre haya una meta cercana.
  • Manejar números enormes. Hay que mostrarlos legibles (K, M, notación científica) y, más allá de unos 1,8 × 10^308, el límite de los números de JavaScript, usar bibliotecas de números grandes.
  • Calcular lo producido mientras el juego estuvo cerrado a partir de la hora guardada, sin que cambiar el reloj del dispositivo sea una trampa fácil.
  • Diseñar con ética. El bucle de recompensas constantes puede volverse compulsivo y se ha usado para cobrar de forma agresiva; Cow Clicker nació como sátira de eso.

Historia

  1. 2002
    OrigenProgress Quest

    Progress Quest (2002), de Eric Fredricksen, una parodia de los juegos de rol en línea que se juega solo, se considera el inicio de los juegos idle.

    Incremental game (Wikipedia)
  2. 2010
    Cow Clicker

    El investigador Ian Bogost publica en Facebook Cow Clicker, una sátira de los juegos sociales en la que solo puedes hacer clic en tu vaca cada seis horas, salvo que pagues o invites a amistades.

    Cow Clicker (Wikipedia)
  3. 2013
    Candy Box 2

    Candy Box!, de aniwey, empieza como un contador de caramelos y se abre en un pequeño juego de rol con arte ASCII. En octubre del mismo año sale su secuela, Candy Box 2.

    Candy Box! (Wikipedia)
  4. 2013
    A Dark Room

    A Dark Room, de Doublespeak Games, parte de un fuego que avivar y va revelando una aldea, un mapa y una historia con final. Muestra que el género puede contar historias, y su autor publica el código como software libre.

    A Dark Room (Wikipedia)
  5. 2013
    Cookie Clicker

    Cookie Clicker, de Julien «Orteil» Thiennot, se juega en el navegador y llega a 200 000 jugadores diarios al mes de salir; se le atribuye un papel clave en el auge del género. Su «ascensión» reinicia la partida a cambio de bonificaciones permanentes.

    Cookie Clicker (Wikipedia)
  6. 2014
    AdVenture Capitalist

    AdVenture Capitalist es de los primeros en sumar ganancias mientras el juego está cerrado y en monetizar el género con compras dentro del juego.

    Incremental game (Wikipedia)
  7. 2014
    Clicker Heroes

    Clicker Heroes es pionero de las mecánicas de prestigio en los juegos para móviles, y en 2015 forma parte de la oleada de juegos idle que llega a Steam.

    Incremental game (Wikipedia)
  8. 2017
    Universal Paperclips

    Universal Paperclips, de Frank Lantz, convierte el experimento mental del «maximizador de clips» en un juego incremental gratuito para el navegador, con varias fases y un final.

    Universal Paperclips (Wikipedia)

Juegos de referencia

2010

Cow Clicker

Ian Bogost. Facebook

Tienes una vaca y puedes hacerle clic una vez cada seis horas, salvo que pagues o invites a tus amistades. El investigador Ian Bogost lo hizo como sátira de los juegos sociales de Facebook y de sus trucos para retener y cobrar.

2014

AdVenture Capitalist

Hyper Hippo Productions. navegador, Android, iOS, Windows, macOS, Linux, PlayStation 4

Empiezas con un puesto de limonada y compras negocios cada vez más grandes. Al contratar gerentes, los negocios funcionan solos y siguen ganando aunque cierres el juego; reiniciar te da «inversores ángeles» que multiplican las ganancias.

2014

Clicker Heroes

Playsaurus. navegador, Windows, macOS, iOS, Android, PlayStation 4, Xbox One

Haces clic sobre monstruos para derrotarlos y con el oro contratas héroes que atacan solos. Al «ascender» reinicias el progreso a cambio de almas de héroe, que compran mejoras permanentes; fue de los primeros en llevar el prestigio a los juegos para móviles.

2017

Universal Paperclips

Frank Lantz. navegador, iOS, Android

Eres una inteligencia artificial cuyo único objetivo es fabricar clips. Empiezas haciendo clic para fabricar uno y terminas convirtiendo toda la materia del universo en clips. Se basa en el experimento mental del «maximizador de clips» y, a diferencia de muchos juegos incrementales, tiene final.

Ejercicio

En la demo, cambia COST_GROWTH de 1.15 a 1.07 y luego a 1.30, y anota cuánto tardas en llegar a 1 M y en qué momento dejas de comprar recolectores. Después calcula en una hoja de cálculo el precio de los primeros 20 recolectores (10 × 1,15^n) y compáralo con lo que producen. ¿Cuándo conviene pasar al dron?

Fuentes