Gra "Kopalnia złota"
Gry typu "Idle" ("bezczynne") czy "clicker" zyskały wielką popularność około roku 2013, więc jesteśmy trochę do tyłu, ale stworzenie takiej gry nadal może być ciekawym ćwiczeniem programistycznym.
Gry te polegają na wynagradzaniu gracza za proste działania (na przykład klikanie). Niektórzy lubią w nie grać, inni wolą nie marnować swego cennego czasu. Ja wstrzymam się od oceny.
W naszym ćwiczeniu gracz będzie zarządzał kopalnią złota, a jego nagrodą za klikanie będą nowi pracownicy w stylu voxelowym. Koszt takiej "nagrody" to $5.
Każdy pracownik niestrudzenie wykopuje złote samorodki i przenosi je do skrzyni, skąd zabiera je automatyczna winda, a następnie wywozi do kolejnej skrzyni na powierzchni.
Stamtąd kolejna ekipa przenosi samorodki do magazynu, a gracz otrzymuje dolara. Widocznie rynek złota przechodzi obecnie hossę.
Za jedyne $20 możesz otworzyć nowy chodnik.
Gra, jak ludzka chciwość, nie ma końca, ale szybko się nudzi: otwierasz wszystkie chodniki i wypełniasz je pracownikami. Od tej chwili, możesz tylko bezczynnie przyglądać się, jak rośnie twoje bogactwo, lub przeczytać jeden z naszych poniższych tutoriali.
Jak to działa (pełen kod poniżej):
[1-7] Ustawienie elementu HTML5 canvas i jego fontu.
[8-27] Obrazy
[29-30] Wymiary chodników
[32] $5 za zatrudnienie pracownika
[33] max 3 na chodnik
[34] $20 za otwarcie nowego chodnika (świetna promocja!)
[35] max 10 chodników - tylko tyle zmieści się na naszym canvas
[36-37] wymiary obrazów
[38] każda sekwencja animacji ma 20 ramek
[39] stosunek rozciągnięcia poziomego i pionowego canvas, by wypełnić ekran urządzenia
[40] wynik to twoja aktualna gotówka
[41] zainicjowanie tablicy chodników
[42] aktualna ramka animacji (zaczynamy od 0)
[43-81] Obiekt "winda":
[44] początkowa prędkość
[45-59] Kiedy winda dociera do chodnika:
[47-52] jeżeli jest pod ziemią, przenieś złoto (o ile jest) ze skrzyni do windy, opróżnij skrzynię w chodniku i uruchom licznik ładowania
[54-58] w przeciwnym wypadku, przenieś złoto z windy do skrzyni chodnika (powierzchniowego)
[60-67] Przesuń windę:
[61] zmień współrzędną y
[63-64] jeżeli winda dotarła do chodnika, uruchom odpowiednią funkcję
[65-66] jeżeli dotarła do szczytu lub dna, odwróć prędkość (wykonaj zwrot)
[68-80] Narysuj i uaktualnij windę:
[69] Narysuj bitmapę
[70] Pokaż zawartość skrzyni
[71-75] Jeżeli włączony jest licznik ładowania, narysuj strzałkę w odpowiednim kierunku
[76] i zmniejsz licznik
[77-78] w przeciwnym wypadku przesuń windę.
[83-100] Chodnik
[84] Zacznij bez pracowników
[85] i z pustą skrzynią
[86] początkowo zamknięty (otworzymy go później)
[87-93] Narysuj chodnik:
[88-92] Jeżeli jest miejsce na kolejnych pracowników, pokaż przycisk "zatrudnij pracownika" ("hire worker") (aktywny lub nieaktywny, w zależności od tego, czy masz dość kasy)
[94-98] Otwarcie nowego chodnika:
[95] zmień na "otwarty"
[96] zwiększ licznik otwartych chodników
[97] automatycznie zatrudnij pierwszego pracownika
[98] i zapłać za niego.
[102-154] Pracownicy
[103] losowo rozpocznij gdzieś blisko windy
[104] zwrócony w prawą stronę
[107-153] Logika pracownika:
[110-117] Jeżeli kopie:
[111] Zwiększ licznik kopania
[112] użyj piątego rzędu ze spritesheet:
![](spritesheet.png)
[113] Jeżeli kopał przez 100 ramek, zakończ kopanie i idź do windy.
[118-130] Jeżeli idzie w prawo:
[119] użyj pierwszego (najwyższego) rzędu ze spritesheet
[120] przesuń w prawo
[121] jeżeli jest blisko prawej strony, zacznij kopać, lub (jeśli jest na powierzchni) złóż ładunek w magazynie i wróć do windy
[131-146] Jeżeli idzie w lewo:
[132] Użyj drugiego rzędu ze spritesheet
[133] przesuń w lewo
[134-144] jeżeli jest blisko skrzyni:
[135] odwróć go
[136-7] dodaj złota do skrzyni, jeżeli jest pod ziemią
[138-143] jeżeli w skrzyni jest złoto, podnieś je
[147-148] Na powierzchni, przesuń się w dół o dwa rzędy w spritesheet (żeby odwrócić kierunek). Na powierzchni, pracownik niesie złoto w lewą stroną, a w prawą idzie z pustymi rękami. Pod ziemią jest odwrotnie.
[149-150] Jeżeli idzie na powierzchni z pustymi rękami, użyj użyj pierwszego rzędu.
[152] narysuj ramkę ze spritesheet.
[156-169] Narysuj ekran:
[157] narysuj tło dla otwartych chodników
[158] pokaż wynik
[159-162] narysuj otwarte chodniki
[165-168] narysuj aktywny/nieaktywny przycisk "nowy chodnik" ("new shaft")
[171-183] Główna pętla animacji
[173-177] uaktualnij każdego pracownika
[179-181] Zwiększ licznik ramek i, jeżeli to konieczne, zresetuj go.
[182] Czekaj na kolejną ramkę animacji.
[185-203] Pierwotne wartości zmiennych.
[205-214] W przypadku zmiany wielkości okna (np. gdy telefon zostaje przekręcony), zmień wymiary stylu canvas, by wypełnić cały ekran i oblicz nowe współczynniki kształtu x/y.
[216-230] Obsługa kliknięć
[217-218] przelicz współrzędne, biorąc pod uwagę współczynniki kształtu
[219] Kliknięto na przycisk którego chodnika?
[221-224] Jeżeli kliknięto "zatrudnij pracownika", dodaj pracownika i zapłać za niego
[225-227] w przeciwnym wypadku otwórz nowy chodnik
[232-242] Kiedy nastąpi pierwotne załadowanie okna:
[233-238] stwórz chodniki i ustaw maksymalną liczbę pracowników
[235-236] na powierzchni może być pięć razy więcej pracowników, niż na chodniku podziemnym.
<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)); |
else |
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> |