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='canvas' width='600' height='600' style='position:absolute; left:0; top: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 aspectX, aspectY; |
| let score, shaftsOpen; |
| let shafts = []; |
| let frame = 0; |
| let lift = { |
| speed: .5, |
| arrivedAtShaft: function(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; |
| } |
| }, |
| move: function() { |
| 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; |
| }, |
| update: function() { |
| context.drawImage(liftImg, 0, this.y); |
| context.fillText(this.chest, 10, this.y + 25); |
| if (this.loadingCounter) { |
| if (this.y > 0) |
| context.drawImage(arrowLeftImg, liftWidth * .5, this.y); |
| else |
| context.drawImage(arrowRightImg, liftWidth * .5, this.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(workerActiveButtonsImg, shaftWidth, shaftHeight * shaftNumber); |
| else |
| context.drawImage(workerInactiveButtonsImg, shaftWidth, shaftHeight * 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(spritesheetImg, frame * spriteSize, row * spriteSize, spriteSize, spriteSize, this.x, shaftNumber * shaftHeight, spriteSize, spriteSize); |
| }; |
| } |
| |
| function drawScreen() { |
| context.drawImage(backgroundImg, 0, 0, shaftWidth + buttonWidth, shaftHeight * shaftsOpen, 0, 0, shaftWidth + buttonWidth, shaftHeight * shaftsOpen); |
| context.fillText(score, 300, 15); |
| shafts.forEach((shaft, shaftNumber) => { |
| if (!shaft.closed) { |
| context.fillText(shaft.chest, liftWidth + 15, shaftNumber * shaftHeight + 25); |
| shaft.draw(shaftNumber); |
| } |
| }); |
| if (score >= shaftCost) |
| context.drawImage(shaftActiveButtonsImg, shaftWidth, shaftHeight * (shaftsOpen)); |
| if (score < shaftCost) |
| context.drawImage(shaftInactiveButtonsImg, shaftWidth, shaftHeight * (shaftsOpen)); |
| } |
| |
| function animate() { |
| drawScreen(); |
| shafts.forEach((shaft, shaftNumber) => { |
| 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 = shaftsOpen; i < max_shafts; i++) { |
| shafts[i].closed = true; |
| shafts[i].workers = []; |
| context.drawImage(closeImg, 0, i * 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 = 0; i < max_shafts; i++) { |
| 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> |