Clique aqui para jogar a versão em tela cheia

Mina de ouro ociosa (Idle)

Jogos "Idle" ou "clicker" tornaram-se imensamente populares por volta de 2013, então estamos um pouco atrasados para a festa, mas ainda podemos nos divertir criando um.

A ideia desses jogos é dar ao jogador uma recompensa por uma ação simples (como um clique). Algumas pessoas gostam desses jogos, outras preferem não perder seu tempo precioso. Não estou julgando.
No nosso caso, o jogador operará uma mina de ouro e sua recompensa por clicar será um novo trabalhador em estilo voxel. Essa "recompensa" custará a você $5.
Cada trabalhador cava incansavelmente as pepitas de ouro e as transporta para baús, onde um elevador automatizado as recolhe e as leva para outro baú na superfície.
De lá, outra equipe transporta as pepitas para o armazém e o jogador recebe $1 por cada uma (aparentemente o mercado de ouro não está muito aquecido agora).
Por apenas $20, você pode abrir um novo poço.
O jogo, assim como a ganância humana, nunca termina, mas enjoa bem rápido: você abre todos os poços e os enche de trabalhadores. Nesse ponto, você pode apenas observar ociosamente sua riqueza crescer ou conferir um dos tutoriais abaixo.

Aqui está como funciona (código completo abaixo):

[1-7] Configura o canvas HTML5 e sua fonte.
[8-27] Imagens
[29-30] Dimensões do poço
[32] $5 para contratar um novo trabalhador
[33] máx de 3 trabalhadores por poço
[34] $20 para abrir um novo poço (que pechincha!)
[35] 10 poços no máximo - é o que cabe no nosso canvas
[36-37] dimensões das imagens
[38] cada sequência de animação tem 20 quadros (frames)
[39] a proporção de estiramento horizontal e vertical do canvas para caber na tela do dispositivo
[40] score é o seu dinheiro atual
[41] inicia a matriz de poços
[42] o quadro de animação atual (começa com 0)

[43-81] o objeto do elevador:
[44] velocidade inicial
[45-59] Quando o elevador chega a um poço:
[47-52] se estiver no subsolo, move o ouro (se houver) do baú para o elevador, esvazia o baú do poço, inicia o contador de carregamento
[54-58] caso contrário, move o ouro do elevador para o baú do poço
[60-67] Move o elevador:
[61] altera a coordenada y
[63-64] se o elevador chegou a um poço, executa a função correspondente
[65-66] se atingiu o topo ou o fundo, inverte a velocidade
[68-80] Desenha e atualiza o elevador:
[69] Desenha o bitmap
[70] Mostra o conteúdo do baú
[71-75] Se o contador de carregamento estiver ligado, desenha a seta na direção atual
[76] e diminui o contador
[77-78] caso contrário, move o elevador.

[83-100] O poço
[84] começa sem trabalhadores
[85] e com um baú vazio
[86] fechado por padrão (abriremos mais tarde)
[87-93] Desenha o poço:
[88-92] Se houver espaço para mais trabalhadores, mostra o botão "contratar trabalhador" (ativo ou inativo, dependendo se você tem dinheiro suficiente)
[94-98] Abrindo um novo poço:
[95] muda para "aberto"
[96] aumenta o contador de poços abertos
[97] contrata automaticamente o primeiro trabalhador
[98] e paga por ele.

[102-154] Trabalhadores
[103] começam aleatoriamente em algum lugar perto do elevador
[104] virados para a direita
[107-153] Lógica do trabalhador:
[110-117] Se ele estiver cavando:
[111] Aumenta o contador de escavação
[112] usa a 5ª linha da folha de sprites (spritesheet):


[113] Se ele estiver cavando por 100 quadros, termina de cavar e vai para o elevador.
[118-130] Se ele estiver indo para a direita:
[119] usa a 1ª linha (superior) da spritesheet
[120] move para a direita
[121] se ele estiver perto do lado direito, começa a cavar ou (se estiver na superfície) deposita a carga no armazém e volta para o elevador
[131-146] Se ele estiver indo para a esquerda:
[132] Usa a 2ª linha da spritesheet
[133] move para a esquerda
[134-144] se ele estiver perto do baú:
[135] vira de volta
[136-7] adiciona o ouro ao baú se estiver no subsolo
[138-143] se houver ouro no baú, pega-o
[147-148] Se estiver na superfície, desce duas linhas na spritesheet (para inverter a direção para a qual ele está virado). Na superfície, ele carrega a pepita indo para a esquerda e está de mãos vazias indo para a direita. No subsolo, é o oposto.
[149-150] Se estiver na superfície e de mãos vazias, usa a 1ª linha.
[152] desenha o quadro da spritesheet.
[156-169] Desenha a tela:
[157] desenha a imagem de fundo para os poços abertos
[158] mostra a pontuação
[159-162] desenha os poços abertos
[165-168] desenha o botão "novo poço" ativo/inativo

[171-183] Loop principal de animação
[173-177] atualiza cada trabalhador
[179-181] Aumenta o contador de quadros e redefine se necessário.
[182] Aguarda o próximo quadro de animação.
[185-203] Valores iniciais para as variáveis.
[205-214] Caso a janela seja redimensionada (ex: o dispositivo móvel seja rotacionado), altera as dimensões de estilo do canvas para preencher a tela inteira e calcula os aspectos x/y atualizados.

[216-230] Manipulação de cliques
[217-218] recalcula as coordenadas levando os aspectos em conta
[219] Qual botão de poço foi clicado?
[221-224] se um botão "contratar trabalhador" foi clicado, adiciona um trabalhador e paga por ele
[225-227] caso contrário, abre um novo poço

[232-242] Quando a janela carrega pela primeira vez:
[233-238] cria os poços e define o número máximo de trabalhadores
[235-236] a superfície pode ter 5 vezes mais trabalhadores que os poços.


<html>
<body>
<canvas id='canvaswidth='600height='600style='position:absoluteleft:0top:0'></canvas>
<script>
const canvas = document.getElementById('canvas');
const context = canvas.getContext('2d');
context.font = 'bold 16px sans-serif';
const backgroundImg = new Image();
backgroundImg.src = 'background.png';
const liftImg = new Image();
liftImg.src = 'lift.png';
const arrowLeftImg = new Image();
arrowLeftImg.src = 'arrow.png';
const arrowRightImg = new Image();
arrowRightImg.src = 'arrow_right.png';
const spritesheetImg = new Image();
spritesheetImg.src = 'spritesheet.png';
const closeImg = new Image();
closeImg.src = 'close.png';
const workerActiveButtonsImg = new Image();
workerActiveButtonsImg.src = 'worker_active.png';
const workerInactiveButtonsImg = new Image();
workerInactiveButtonsImg.src = 'worker_inactive.png';
const shaftActiveButtonsImg = new Image();
shaftActiveButtonsImg.src = 'shaft_active.png';
const shaftInactiveButtonsImg = new Image();
shaftInactiveButtonsImg.src = 'shaft_inactive.png';
 
const shaftHeight = 60,
  shaftWidth = 450,
  liftWidth = 40,
  workerCost = 5,
  max_workers = 3,
  shaftCost = 20,
  max_shafts = 10,
  buttonWidth = 150,
  spriteSize = 60,
  max_frames = 19;
let aspectXaspectY;
let scoreshaftsOpen;
let shafts = [];
let frame = 0;
let lift = {
  speed: .5,
  arrivedAtShaftfunction(shaftNumber) {
    let shaft = shafts[shaftNumber];
    if (shaftNumber > 0) {
      if (shaft.chest > 0) {
        this.chest = this.chest + shaft.chest;
        shaft.chest = 0;
        this.loadingCounter = 100;
      }
    } else {
      if (this.chest > 0)
        this.loadingCounter = 100;
      shaft.chest = shaft.chest + this.chest;
      this.chest = 0;
    }
  },
  movefunction() {
    this.y = this.y + this.speed;
    let currentShaft = this.y / shaftHeight;
    if (currentShaft == Math.floor(currentShaft))
      this.arrivedAtShaft(currentShaft);
    if (this.y >= (shaftsOpen - 1) * shaftHeight || this.y <= 0)
      this.speed = -this.speed;
  },
  updatefunction() {
    context.drawImage(liftImg0this.y);
    context.fillText(this.chest10this.y + 25);
    if (this.loadingCounter) {
      if (this.y > 0)
        context.drawImage(arrowLeftImgliftWidth * .5this.y);
      else
        context.drawImage(arrowRightImgliftWidth * .5this.y);
      this.loadingCounter--;
    } else {
      this.move();
    }
  },
};
 
function Shaft() {
  this.workers = [];
  this.chest = 0;
  this.closed = true;
  this.draw = function(shaftNumber) {
    if (shafts[shaftNumber].workers.length < shafts[shaftNumber].max_workers)
      if (score >= workerCost)
        context.drawImage(workerActiveButtonsImgshaftWidthshaftHeight * shaftNumber);
      else
        context.drawImage(workerInactiveButtonsImgshaftWidthshaftHeight * shaftNumber);
  };
  this.open = function() {
    this.closed = false;
    shaftsOpen++;
    this.workers.push(new Worker());
    score = score - shaftCost;
  };
}
 
function Worker() {
  this.x = Math.floor(Math.random() * liftWidth);
  this.mode = 'goRight';
  this.counter = 0;
  this.load = 0;
  this.update = function(shaftNumber) {
    let row;
    switch (this.mode) {
      case 'dig':
        this.counter++;
        row = 4;
        if (this.counter > 100) {
          this.counter = 0;
          this.mode = 'goLeft';
        }
        break;
      case 'goRight':
        row = 0;
        this.x++;
        if (this.x >= shaftWidth - spriteSize * 1.5 + Math.random() * spriteSize) {
          if (shaftNumber > 0)
            this.mode = 'dig';
          else {
            score = score + this.load;
            this.load = 0;
            this.mode = 'goLeft';
          }
        }
        break;
      case 'goLeft':
        row = 1;
        this.x--;
        if (this.x <= 2 * liftWidth) {
          this.mode = 'goRight';
          if (shaftNumber > 0) {
            shafts[shaftNumber].chest = shafts[shaftNumber].chest + 1;
          } else {
            if (shafts[0].chest > 0) {
              this.load = 1;
              shafts[0].chest = shafts[0].chest - 1;
            }
          }
        }
        break;
    }
    if (shaftNumber == 0) {
      row = row + 2;
      if (this.load == 0 && this.mode == 'goRight')
        row = 0;
    }
    context.drawImage(spritesheetImgframe * spriteSizerow * spriteSizespriteSizespriteSizethis.xshaftNumber * shaftHeightspriteSizespriteSize);
  };
}
 
function drawScreen() {
  context.drawImage(backgroundImg00shaftWidth + buttonWidthshaftHeight * shaftsOpen00shaftWidth + buttonWidthshaftHeight * shaftsOpen);
  context.fillText(score30015);
  shafts.forEach((shaftshaftNumber) => {
    if (!shaft.closed) {
      context.fillText(shaft.chestliftWidth + 15shaftNumber * shaftHeight + 25);
      shaft.draw(shaftNumber);
    }
  });
  if (score >= shaftCost)
    context.drawImage(shaftActiveButtonsImgshaftWidthshaftHeight * (shaftsOpen));
  if (score < shaftCost)
    context.drawImage(shaftInactiveButtonsImgshaftWidthshaftHeight * (shaftsOpen));
}
 
function animate() {
  drawScreen();
  shafts.forEach((shaftshaftNumber) => {
    shaft.workers.forEach((worker) => {
      worker.update(shaftNumber);
    });
  });
  lift.update();
  frame++;
  if (frame > max_frames)
    frame = 0;
  window.requestAnimationFrame(animate);
}
 
function reset() {
  shaftsOpen = 0;
  score = 70;
  lift.y = 0;
  lift.chest = 0;
  lift.loadingCounter = 0;
  shafts.forEach((shaft) => {
    shaft.closed = true;
    shaft.workers = [];
  });
  shafts[0].open();
  shafts[1].open();
  for (let i = shaftsOpeni < max_shaftsi++) {
    shafts[i].closed = true;
    shafts[i].workers = [];
    context.drawImage(closeImg0i * shaftHeight);
  }
  animate();
}
 
function resize() {
  let picSizeX = window.innerWidth;
  let picSizeY = window.innerHeight;
  canvas.style.width = window.innerWidth;
  canvas.style.height = window.innerHeight;
  aspectX = picSizeX / 600;
  aspectY = picSizeY / 600;
}
 
window.onresize = resize;
 
window.onpointerdown = function(event) {
  let x = event.offsetX / aspectX;
  let y = event.offsetY / aspectY;
  let shaftNumber = Math.floor(y / shaftHeight);
  let shaft = shafts[shaftNumber];
  if (x > shaftWidth)
    if (shaftNumber < shaftsOpen && score >= workerCost && shaft.workers.length < shaft.max_workers) {
      shaft.workers.push(new Worker());
      score = score - workerCost;
    } else {
      if (shaftNumber == shaftsOpen && score >= shaftCost) {
        shaft.open();
      }
    }
};
 
window.onload = function() {
  for (let i = 0i < max_shaftsi++) {
    shafts[i] = new Shaft();
    if (i == 0)
      shafts[i].max_workers = max_workers * 5;
    else
      shafts[i].max_workers = max_workers;
  }
  reset();
  resize();
};
</script>
</body>
</html>