Сегодня я расскажу вам, как создать простейший ToDo лист простейшую инкрементальную IDLE игру на JavaScript, потратив меньше одного дня ежегодных каникул. Для этого предлагаю выбрать сову игру попроще и не пропускать шагов между овалом и готовой совой пустым проектом и готовой игрой.
![](https://habrastorage.org/r/w780q1/webt/q5/ne/i7/q5nei7lkowkaq4_vmnveajwuwra.jpeg)
Людям, знающим как делать такие игры, будет скучно; людям, знающим JS, рекомендую смотреть на код сквозь пальцы (во избежание травм) и читать только про механики. Под катом последовательная инструкция, ориентированная на новичков.
Инкрементальные игры — это игры, в основе которых лежит [бесконечный] основной цикл, состоящий из накопления ресурсов, их постоянных трат и ускорения дохода. Главная их особенность в постоянном росте чисел. Инструкция расскажет, как сделать IDLE (ленивую) игру, в которой ресурсы пополняются таймером, а не активным кликаньем.
Начнем с того, что создадим текстовый файл с именем index.html и напишем в него следующее:
На текущий момент никакой игры пока нет, но есть всего лишь статичная надпись.
Ссылка на игру в текущем состоянии: 0df7a27.
В основе всех игр в этом жанре лежат два условно-бесконечных игровых цикла.
Первый цикл заключается в накоплении ресурсов.
Второй цикл заключается в трате ресурсов.
Давайте реализуем первый цикл.
В первую очередь, строка
обеспечивает нам возможность обращаться к ней по её id, и менять содержимое внутри тегов, в нашем с вами случае — значение наличных монет.
Во-вторых, появился скрипт.
Создаем переменную coppers, и задаем её область видимости ключевым словом let. Аналогично объявлением copperGrowth, которая будет отвечать за скорость прироста медных монет.
Далее создаем объект, в который мы поместим наш таймер, который будет дергать функцию endOfTurnCalc каждые 2000 мс (2 секунды). Функция обеспечивает прирост медных монет и обновление интерфейса. Ура, половина игры сделана: у нас есть цикл накопления ресурсов. Следующая задача — научиться их тратить.
Ссылка на игру в текущем состоянии: e5d96e1.
Давайте добавим кнопку, которая будет позволять нам тратить медные монеты на улучшения медной шахты, ее предлагаю поставить перед нашей строкой с монетами. Кнопка, ее название, функция, которую кнопка вызывает, имя кнопки — содержимое внутри тэгов.
Добавим код, который позволит кнопке апгрейда работать:
и код, который окажет влияние на скорость добычи новых монет:
Зададим цену улучшения шахты, определим уровень шахты по умолчанию, пропишем в функцию проверку достаточности монет на апгрейд шахты.
В случае, если денег достаточно, то списываем цену апгрейда, повышаем текущий уровень шахт, рассчитываем цену для следующего улучшения; выводим на экран текущие значения медных монет и стоимости следующего улучшения.
Ссылка на игру в текущем состоянии: c731ec5.
Ну что же, играть уже можно — можно копить, и, что приятнее, тратить накопленное. Но нам нужно подкрепление успеха — игрок не только вычислять прирост скорости накопления монет, методом вычитания от нового значения старого, но и, по-хорошему, сразу видеть текущую скорость накопления монет. Сделаем?
В интерфейс добавляем еще одну строчку:
Теперь наш интерфейс описан следующими строчками:
Вносим изменение в скрипт, в функцию upgCopperMine():
Ссылка на игру в текущем состоянии: 3ac06b6.
Отлично! У нас есть условно-бесконечная игра. Теперь надо остановится на секунду, и задуматься — части людей нравится, когда конечной цели нет, и можно играть, пока не надоест, другая часть считает, что условия конечности, достижимости игры должны быть. Игру для первых мы уже сделали, но что мешает нам сделать небольшое изменение, чтобы у игры была цель и условие победы? Давайте сделаем.
Добавили переменную, в которую положим значение, которое нам нужно достичь и изменим функцию из цикла, добавив проверку на достижение цели. При достижении цели мы очищаем интервал из нашего объекта с таймером и выводим всплывающее сообщение в браузере.
Есть небольшой момент: некоторые антивирусы не любят alert'ы и блокируют страницу за их использование.
Ссылка на игру в текущем состоянии: 8fa4041.
Следующая функциональность, которую ожидают люди от игры длиннее пяти минут — это возможность сохранять и загружать игру. Давайте же дадим им её!
Добавляем в интерфейс две строчки, не забыв добавить в предыдущую строчку тег перевода строки:
и теперь расширим наши скрипты, чтобы кнопки заработали:
Кладём все изменяемые ресурсы в локальное хранилище браузера при сохранении и при чтении читаем их обратно и обновляем интерфейс.
Ссылка на игру в текущем состоянии: 54b1ea0.
Все, основная часть игры готова.
К текущему моменту мы рассмотрели:
— создание ресурса;
— добычу ресурса;
— трата ресурса на апгрейд системы его добычи;
— отражение скорости добычи ресурса;
— условие победы;
— запись и чтение сохранения игры в локальное хранилище браузера.
Продолжим? Перед тем как приступить к следующей теме, а именно добавлению второго ресурса в нашу систему (серебра), я предлагаю провести небольшой рефакторинг текущего кода.
Что нужно сделать в рамках рефакторинга?
Во-первых, давайте создадим один игровой объект, а медные монеты, уровень медных шахт и так далее поместим внутрь как свойства этого объекта. Это сильно поможет в будущем, когда мы будем расширять сохранение и загрузку на новые игровые сущности.
Во-вторых, сделаем расчет цены апгрейда динамическим.
В-третьих, избавимся от дублирования кода, который отвечает за обновление интерфейса.
Код, приобретет следующий вид:
Довольно очевидно, что мы создали объект game, и внутри объекта прописали его свойства и значения свойств. Далее, везде, где мы раньше обращались напрямую к переменным, мы теперь обращаемся к тем же свойства объекта game.
Ссылка на игру в текущем состоянии: 8a07f4d.
Давайте теперь обновим систему сохранения и загрузки.
Теперь, вместо сохранения отдельных свойств, мы сохраняем весь объект целиком. Но будьте осторожны: если вы добавите в объект методы, то они не сохранятся таким образом, и последующая перезапись объекта из сохранения удалит все методы…
Ссылка на игру в текущем состоянии: 8eba059.
Давайте же удалим ненужное свойство — цену апгрейда шахты и создадим функцию расчета апгрейда и будем вызывать ее в необходимых местах.
Ссылка на игру в текущем состоянии: 4007924.
Наконец, вынесем повторяющиеся части кода обновления интерфейса в отдельную функцию.
В свою очередь, во всех остальных местах, где мы обращались к тем или иным объектам DOM, теперь поставим вызов функции updateUI():
Ссылка на игру в текущем состоянии: 2245f97.
Теперь предлагаю ввести второй ресурс: серебро, строительство и улучшения серебряных шахт.
Что сделано? В интерфейсе добавлены поля для отображения серебряных монет, скорость их добычи, кнопка для постройки или улучшения серебряной шахты.
Внутрь игрового объекта продублированы свойства silvers, silverGrowth, silversUpgLevel, но для последнего установлено значение 0, потому что серебряной шахты у нас по умолчанию нет.
Добавлена переменная silverMineBasePriceCoppers, которая будет отражать цену постройки серебряной шахты в медных монетах (потому что мы не можем платить за серебряную шахту серебром, которого у нас пока нет).
Модифицирована функция coppersUpgCost и продублирована как silversUpgCost, чтобы цена апгрейда была отличной от нуля, в случае, если текущий уровень шахты равен 0.
Изменили функцию обсчета конца хода, добавив туда расчет дохода от серебряной шахты, а также изменив условие победы — теперь нам нужно сравнивать не количество медных монет, а количество серебряных монет.
Создали функцию upgSilverMine, в которой отразим логику списывания средств (если шахты еще нет, списываем медяки, если шахта уже построена, то списываем серебро для улучшения шахты).
В функцию updateUI добавили необходимые строчки для серебра, а также разделили логику вывода текста на кнопку для улучшения серебряной шахты, таким образом одна кнопка работает и для постройки и для улучшения шахты.
Функции сохранения и загрузки остались без изменений.
Ссылка на игру в текущем состоянии: 03eb0eb.
Может так случиться, что медных монет накопиться много больше, чем надо, в то время как серебро еще нужно заработать. Для этого в подобных играх, как правило, существует возможность обмена разных ресурсов друг на друга.
Давайте добавим в интерфейс пару кнопок:
И добавим пару функций к этим кнопкам:
Ссылка на игру в текущем состоянии: 92219b2.
Так, что еще можно добавить в инкрементальную игру? Механизм Престижа! Конечно, сейчас это скорее излишество, но в случае дальнейшего развития игры, он вам пригодится. Игроки любят его!
В разных играх сделано по-разному, но в целом — механизм дает очки Престижа либо после успешного завершения (прохождения) игры, либо, разблокируется во время игры после достижения некоего целевого порога.
С помощью очков Престижа игрок получает возможность быстрого старта или постоянного бонуса в этой же игре (возможно, что в следующей миссии, если игра разбита на таковые), так, что он может попробовать какую-то другую тактику, но тратит меньше времени на повторное прохождение. Таким образом, можно изучить несколько развилок, опробовав разные решения, и затрачивая на это меньше времени, чем если бы такового механизма не было.
Что нам потребуется для этого? Нужен механизм перезапуска игры со значениями по умолчанию. В качестве постоянного бонуса, будем добавлять очки Престижа к ранее неизменяемым свойствам copperGrowth и silverGrowth.
Добавляем следующие функции winGame, restartGameDialog, restartGame, а также изменим endOfTurnCalc для вызова новых функций и обновим updateUI:
Функция winGame запускает таймер, по истечению которого вызывается диалог подтверждения. В случае подтверждения вызывается функция рестарта, которая сбрасывает все значения на умолчанию, но увеличивает свойства базового прироста монет.
В целом, игра готова:
— создание ресурса;
— добычу ресурса;
— трата ресурса на апгрейд системы его добычи;
— отражение скорости добычи ресурса;
— условие победы;
— запись и чтение сохранения игры в локальное хранилище браузера;
— добавление еще одного ресурса с добычей и апгрейдом его добычи;
— обмен ресурсов;
— механизм рестарта игры и механизм Престижа.
Ссылка на игру в текущем состоянии: 92219b2.
Наконец, последнее, что в этой статье хочется показать, это другой механизм хранилища, потому что у текущего механизма есть одна, но существенная проблема.
Представьте себе, что вы разрабатываете игру, публикуете ее на веб-сервере (возможно, на github pages), а люди в нее играют. Со временем, вы вносите изменения в игру, которые добавляют новые свойства к игровому объекту game.
Теперь следите за руками.
Игрок заходит со старым сохранением, в котором есть все свойства coppers и silvers, но нет, скажем, свойства gold. Загружается наш game объект и перезаписывает тот, который был создан при загрузке веб-страницы, и теперь внутри объекта есть только те свойства, которые были в его сохранении. А код-то мы уже обновили! И этот код ссылается на обновленные свойства, которых нет. Таким образом, код начинает получить множественные ошибки, вплоть до полной неработоспособности игры. На удивление, чтобы исправить эту проблему, нужно переписать всего две строчки в функции загрузки игры:
Теперь, если добавите в game = { gold: 1, } и загрузитесь с вашего старого сохранения, где золота еще не было, то золото останется в объекте и игровая логика нигде не сломается.
Ссылка на игру в текущем состоянии: 83c258d.
Еще одно. Перенос сохранений между браузерами, давайте его тоже запилим.
Обновим интерфейс.
Добавили две служебных переменных, обновили updateUI(), написали три функции — экспорта, импорта, и функцию, которая изменяет флаг отображения экспорта, таким образом «старый» экспорт через 30 секунд после его формирования будет скрыт.
На сегодня всё.
![](https://habrastorage.org/webt/q5/ne/i7/q5nei7lkowkaq4_vmnveajwuwra.jpeg)
Людям, знающим как делать такие игры, будет скучно; людям, знающим JS, рекомендую смотреть на код сквозь пальцы (во избежание травм) и читать только про механики. Под катом последовательная инструкция, ориентированная на новичков.
Инкрементальные игры — это игры, в основе которых лежит [бесконечный] основной цикл, состоящий из накопления ресурсов, их постоянных трат и ускорения дохода. Главная их особенность в постоянном росте чисел. Инструкция расскажет, как сделать IDLE (ленивую) игру, в которой ресурсы пополняются таймером, а не активным кликаньем.
Начнем с того, что создадим текстовый файл с именем index.html и напишем в него следующее:
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"><!--эта строка нужна для того, чтобы браузер не угадывал кодировку-->
<title>
Простая ленивая инкрементальная игра
</title>
<style>
</style>
</head>
<body>
Медные монеты: 0<br>
</body>
</html>
На текущий момент никакой игры пока нет, но есть всего лишь статичная надпись.
Ссылка на игру в текущем состоянии: 0df7a27.
В основе всех игр в этом жанре лежат два условно-бесконечных игровых цикла.
Первый цикл заключается в накоплении ресурсов.
Второй цикл заключается в трате ресурсов.
Давайте реализуем первый цикл.
Медные монеты: <span id="spnCoppersValue">0</span><br>
<script>
let coppers = 0;
let copperGrowth = 1;
myTimer = setInterval(endOfTurnCalc, 2000);
function endOfTurnCalc() {
coppers = coppers+copperGrowth;
document.getElementById("spnCoppersValue").innerHTML = coppers;
}
</script>
В первую очередь, строка
<span id="spnCoppersValue">0</span><br>
обеспечивает нам возможность обращаться к ней по её id, и менять содержимое внутри тегов, в нашем с вами случае — значение наличных монет.
Во-вторых, появился скрипт.
Создаем переменную coppers, и задаем её область видимости ключевым словом let. Аналогично объявлением copperGrowth, которая будет отвечать за скорость прироста медных монет.
Далее создаем объект, в который мы поместим наш таймер, который будет дергать функцию endOfTurnCalc каждые 2000 мс (2 секунды). Функция обеспечивает прирост медных монет и обновление интерфейса. Ура, половина игры сделана: у нас есть цикл накопления ресурсов. Следующая задача — научиться их тратить.
Ссылка на игру в текущем состоянии: e5d96e1.
Давайте добавим кнопку, которая будет позволять нам тратить медные монеты на улучшения медной шахты, ее предлагаю поставить перед нашей строкой с монетами. Кнопка, ее название, функция, которую кнопка вызывает, имя кнопки — содержимое внутри тэгов.
<button id="btnUpgCopperMine" onclick="upgCopperMine()">Улучшить медную шахту, 10 медных монет</button><br>
Добавим код, который позволит кнопке апгрейда работать:
let coppersUpgCost = 10;
let coppersUpgLevel = 1;
function upgCopperMine() {
if (coppers>coppersUpgCost){
coppers = coppers-coppersUpgCost;
coppersUpgLevel = coppersUpgLevel + 1;
coppersUpgCost = coppersUpgCost*2;
document.getElementById("spnCoppersValue").innerHTML = coppers;
document.getElementById("btnUpgCopperMine").innerHTML = "Улучшить медную шахту, ";
document.getElementById("btnUpgCopperMine").innerHTML += coppersUpgCost.toString();
document.getElementById("btnUpgCopperMine").innerHTML += " медных монет";
}
}
и код, который окажет влияние на скорость добычи новых монет:
function endOfTurnCalc() {
coppers = coppers+copperGrowth*coppersUpgLevel;;
document.getElementById("spnCoppersValue").innerHTML = coppers;
}
Зададим цену улучшения шахты, определим уровень шахты по умолчанию, пропишем в функцию проверку достаточности монет на апгрейд шахты.
В случае, если денег достаточно, то списываем цену апгрейда, повышаем текущий уровень шахт, рассчитываем цену для следующего улучшения; выводим на экран текущие значения медных монет и стоимости следующего улучшения.
Ссылка на игру в текущем состоянии: c731ec5.
Ну что же, играть уже можно — можно копить, и, что приятнее, тратить накопленное. Но нам нужно подкрепление успеха — игрок не только вычислять прирост скорости накопления монет, методом вычитания от нового значения старого, но и, по-хорошему, сразу видеть текущую скорость накопления монет. Сделаем?
В интерфейс добавляем еще одну строчку:
Скорость заработка медных монет: <span id="spnCoppersRate">1</span> в 2 секунды<br>
Теперь наш интерфейс описан следующими строчками:
<button id="btnUpgCopperMine" onclick="upgCopperMine()" style="width: 240px;">Улучшить медную шахту, 10 медных монет</button><br>
Медные монеты: <span id="spnCoppersValue">0</span><br>
Скорость заработка медных монет: <span id="spnCoppersRate">1</span> в 2 секунды<br>
Вносим изменение в скрипт, в функцию upgCopperMine():
if (coppers>coppersUpgCost){
coppers = coppers-coppersUpgCost;
coppersUpgLevel = coppersUpgLevel + 1;
coppersUpgCost = coppersUpgCost*2;
document.getElementById("spnCoppersValue").innerHTML = coppers;
document.getElementById("btnUpgCopperMine").innerHTML = "Улучшить медную шахту, ";
document.getElementById("btnUpgCopperMine").innerHTML += coppersUpgCost.toString();
document.getElementById("btnUpgCopperMine").innerHTML += " медных монет";
document.getElementById("spnCoppersRate").innerHTML = copperGrowth*coppersUpgLevel;
}
Ссылка на игру в текущем состоянии: 3ac06b6.
Отлично! У нас есть условно-бесконечная игра. Теперь надо остановится на секунду, и задуматься — части людей нравится, когда конечной цели нет, и можно играть, пока не надоест, другая часть считает, что условия конечности, достижимости игры должны быть. Игру для первых мы уже сделали, но что мешает нам сделать небольшое изменение, чтобы у игры была цель и условие победы? Давайте сделаем.
let win_condition = 50;
myTimer = setInterval(endOfTurnCalc, 2000);
function endOfTurnCalc() {
if (coppers < win_condition) {
coppers = coppers+copperGrowth*coppersUpgLevel;
document.getElementById("spnCoppersValue").innerHTML = coppers;
} else {
clearTimeout(myTimer);
alert("Вы достигли цели! Вы накопили "+win_condition.toString());
}
}
Добавили переменную, в которую положим значение, которое нам нужно достичь и изменим функцию из цикла, добавив проверку на достижение цели. При достижении цели мы очищаем интервал из нашего объекта с таймером и выводим всплывающее сообщение в браузере.
Есть небольшой момент: некоторые антивирусы не любят alert'ы и блокируют страницу за их использование.
Ссылка на игру в текущем состоянии: 8fa4041.
Следующая функциональность, которую ожидают люди от игры длиннее пяти минут — это возможность сохранять и загружать игру. Давайте же дадим им её!
Добавляем в интерфейс две строчки, не забыв добавить в предыдущую строчку тег перевода строки:
<button id="btnSaveGame" onclick="saveGame()" style="width: 240px;">Сохранить игру</button><br>
<button id="btnLoadGame" onclick="loadGame()" style="width: 240px;">Загрузить игру</button><br>
и теперь расширим наши скрипты, чтобы кнопки заработали:
function saveGame() {
localStorage.setItem('coppers', coppers);
localStorage.setItem('coppersUpgCost', coppersUpgCost);
localStorage.setItem('coppersUpgLevel', coppersUpgLevel);
}
function loadGame() {
coppers = parseInt(localStorage.getItem('coppers'));
coppersUpgCost = parseInt(localStorage.getItem('coppersUpgCost'));
coppersUpgLevel = parseInt(localStorage.getItem('coppersUpgLevel'));
document.getElementById("spnCoppersValue").innerHTML = coppers;
document.getElementById("btnUpgCopperMine").innerHTML = "Улучшить медную шахту, ";
document.getElementById("btnUpgCopperMine").innerHTML += coppersUpgCost.toString();
document.getElementById("btnUpgCopperMine").innerHTML += " медных монет";
document.getElementById("spnCoppersRate").innerHTML = copperGrowth*coppersUpgLevel;
}
Кладём все изменяемые ресурсы в локальное хранилище браузера при сохранении и при чтении читаем их обратно и обновляем интерфейс.
Ссылка на игру в текущем состоянии: 54b1ea0.
Все, основная часть игры готова.
К текущему моменту мы рассмотрели:
— создание ресурса;
— добычу ресурса;
— трата ресурса на апгрейд системы его добычи;
— отражение скорости добычи ресурса;
— условие победы;
— запись и чтение сохранения игры в локальное хранилище браузера.
Продолжим? Перед тем как приступить к следующей теме, а именно добавлению второго ресурса в нашу систему (серебра), я предлагаю провести небольшой рефакторинг текущего кода.
Что нужно сделать в рамках рефакторинга?
Во-первых, давайте создадим один игровой объект, а медные монеты, уровень медных шахт и так далее поместим внутрь как свойства этого объекта. Это сильно поможет в будущем, когда мы будем расширять сохранение и загрузку на новые игровые сущности.
Во-вторых, сделаем расчет цены апгрейда динамическим.
В-третьих, избавимся от дублирования кода, который отвечает за обновление интерфейса.
Код, приобретет следующий вид:
let game = {
coppers: 1,
copperGrowth: 1,
coppersUpgCost: 10,
coppersUpgLevel: 1,
}
let win_condition = 50;
myTimer = setInterval(endOfTurnCalc, 2000);
function endOfTurnCalc() {
if (game.coppers < win_condition) {
game.coppers = game.coppers+game.copperGrowth*game.coppersUpgLevel;
document.getElementById("spnCoppersValue").innerHTML = game.coppers;
} else {
clearTimeout(myTimer);
alert("Вы достигли цели! Вы накопили "+win_condition.toString());
}
}
function upgCopperMine() {
if (game.coppers>game.coppersUpgCost){
game.coppers = game.coppers-game.coppersUpgCost;
game.coppersUpgLevel = game.coppersUpgLevel + 1;
game.coppersUpgCost = game.coppersUpgCost*2;
document.getElementById("spnCoppersValue").innerHTML = game.coppers;
document.getElementById("btnUpgCopperMine").innerHTML = "Улучшить медную шахту, ";
document.getElementById("btnUpgCopperMine").innerHTML += game.coppersUpgCost.toString();
document.getElementById("btnUpgCopperMine").innerHTML += " медных монет";
document.getElementById("spnCoppersRate").innerHTML = game.copperGrowth*game.coppersUpgLevel;
}
}
function saveGame() {
localStorage.setItem('coppers', game.coppers);
localStorage.setItem('coppersUpgCost', game.coppersUpgCost);
localStorage.setItem('coppersUpgLevel', game.coppersUpgLevel);
}
function loadGame() {
game.coppers = parseInt(localStorage.getItem('coppers'));
game.coppersUpgCost = parseInt(localStorage.getItem('coppersUpgCost'));
game.coppersUpgLevel = parseInt(localStorage.getItem('coppersUpgLevel'));
document.getElementById("spnCoppersValue").innerHTML = game.coppers;
document.getElementById("btnUpgCopperMine").innerHTML = "Улучшить медную шахту, ";
document.getElementById("btnUpgCopperMine").innerHTML += game.coppersUpgCost.toString();
document.getElementById("btnUpgCopperMine").innerHTML += " медных монет";
document.getElementById("spnCoppersRate").innerHTML = game.copperGrowth*game.coppersUpgLevel;
}
</script>
Довольно очевидно, что мы создали объект game, и внутри объекта прописали его свойства и значения свойств. Далее, везде, где мы раньше обращались напрямую к переменным, мы теперь обращаемся к тем же свойства объекта game.
Ссылка на игру в текущем состоянии: 8a07f4d.
Давайте теперь обновим систему сохранения и загрузки.
function saveGame() {
localStorage.setItem('gameTutorial', JSON.stringify(game));
}
function loadGame() {
game = JSON.parse(localStorage.getItem('gameTutorial'));
document.getElementById("spnCoppersValue").innerHTML = game.coppers;
document.getElementById("btnUpgCopperMine").innerHTML = "Улучшить медную шахту, ";
document.getElementById("btnUpgCopperMine").innerHTML += game.coppersUpgCost.toString();
document.getElementById("btnUpgCopperMine").innerHTML += " медных монет";
document.getElementById("spnCoppersRate").innerHTML = game.copperGrowth*game.coppersUpgLevel;
}
Теперь, вместо сохранения отдельных свойств, мы сохраняем весь объект целиком. Но будьте осторожны: если вы добавите в объект методы, то они не сохранятся таким образом, и последующая перезапись объекта из сохранения удалит все методы…
Ссылка на игру в текущем состоянии: 8eba059.
Давайте же удалим ненужное свойство — цену апгрейда шахты и создадим функцию расчета апгрейда и будем вызывать ее в необходимых местах.
function coppersUpgCost() {
return game.coppersUpgLevel*10;
}
function upgCopperMine() {
if (game.coppers>=coppersUpgCost()){
game.coppers = game.coppers-coppersUpgCost();
game.coppersUpgLevel = game.coppersUpgLevel + 1;
document.getElementById("spnCoppersValue").innerHTML = game.coppers;
document.getElementById("btnUpgCopperMine").innerHTML = "Улучшить медную шахту, ";
document.getElementById("btnUpgCopperMine").innerHTML += coppersUpgCost().toString();
document.getElementById("btnUpgCopperMine").innerHTML += " медных монет";
document.getElementById("spnCoppersRate").innerHTML = game.copperGrowth*game.coppersUpgLevel;
}
}
function saveGame() {
localStorage.setItem('gameTutorial', JSON.stringify(game));
}
function loadGame() {
game = JSON.parse(localStorage.getItem('gameTutorial'));
document.getElementById("spnCoppersValue").innerHTML = game.coppers;
document.getElementById("btnUpgCopperMine").innerHTML = "Улучшить медную шахту, ";
document.getElementById("btnUpgCopperMine").innerHTML += coppersUpgCost().toString();
document.getElementById("btnUpgCopperMine").innerHTML += " медных монет";
document.getElementById("spnCoppersRate").innerHTML = game.copperGrowth*game.coppersUpgLevel;
}
Ссылка на игру в текущем состоянии: 4007924.
Наконец, вынесем повторяющиеся части кода обновления интерфейса в отдельную функцию.
function updateUI() {
document.getElementById("spnCoppersValue").innerHTML = game.coppers;
document.getElementById("btnUpgCopperMine").innerHTML = "Улучшить медную шахту, ";
document.getElementById("btnUpgCopperMine").innerHTML += coppersUpgCost().toString();
document.getElementById("btnUpgCopperMine").innerHTML += " медных монет";
document.getElementById("spnCoppersRate").innerHTML = game.copperGrowth*game.coppersUpgLevel;
}
В свою очередь, во всех остальных местах, где мы обращались к тем или иным объектам DOM, теперь поставим вызов функции updateUI():
function endOfTurnCalc() {
if (game.coppers < win_condition) {
game.coppers = game.coppers+game.copperGrowth*game.coppersUpgLevel;
updateUI();
} else {
clearTimeout(myTimer);
alert("Вы достигли цели! Вы накопили "+win_condition.toString());
}
}
function upgCopperMine() {
if (game.coppers>=coppersUpgCost()){
game.coppers = game.coppers-coppersUpgCost();
game.coppersUpgLevel = game.coppersUpgLevel + 1;
updateUI();
}
}
function loadGame() {
game = JSON.parse(localStorage.getItem('gameTutorial'));
updateUI();
}
Ссылка на игру в текущем состоянии: 2245f97.
Теперь предлагаю ввести второй ресурс: серебро, строительство и улучшения серебряных шахт.
<button id="btnUpgCopperMine" onclick="upgCopperMine()" style="width: 240px;">Улучшить медную шахту, 10 медных монет</button><br>
Медные монеты: <span id="spnCoppersValue">0</span><br>
Скорость заработка медных монет: <span id="spnCoppersRate">1</span> в 2 секунды<br>
<button id="btnUpgSilverMine" onclick="upgSilverMine()" style="width: 240px;">Построить серебряную шахту, 50 медных монет</button><br>
Серебряные монеты: <span id="spnSilversValue">0</span><br>
Скорость заработка серебряных монет: <span id="spnSilversRate">1</span> в 2 секунды<br>
<button id="btnSaveGame" onclick="saveGame()" style="width: 240px;">Сохранить игру</button><br>
<button id="btnLoadGame" onclick="loadGame()" style="width: 240px;">Загрузить игру</button><br>
<script>
let game = {
coppers: 1,
copperGrowth: 1,
coppersUpgLevel: 1,
silvers: 0,
silverGrowth: 1,
silversUpgLevel: 0,
}
let win_condition = 50;
let silverMineBasePriceCoppers = 100;
myTimer = setInterval(endOfTurnCalc, 2000);
function endOfTurnCalc() {
if (game.silvers < win_condition) {
game.coppers = game.coppers+game.copperGrowth*game.coppersUpgLevel;
game.silvers = game.silvers+game.silverGrowth*game.silversUpgLevel;
updateUI();
} else {
clearTimeout(myTimer);
alert("Вы достигли цели! Вы накопили "+win_condition.toString());
}
}
function coppersUpgCost() {
return game.coppersUpgLevel*10+5;
}
function silversUpgCost() {
return game.silversUpgLevel*10+5;
}
function upgCopperMine() {
if (game.coppers>=coppersUpgCost()){
game.coppers = game.coppers-coppersUpgCost();
game.coppersUpgLevel = game.coppersUpgLevel + 1;
updateUI();
}
}
function upgSilverMine() {
if (game.silversUpgLevel===0){
if (game.coppers>=silverMineBasePriceCoppers){
game.coppers = game.coppers-silverMineBasePriceCoppers;
game.silversUpgLevel = 1;
updateUI();
}
} else {
if (game.silvers>=silversUpgCost()){
game.silvers = game.silvers-silversUpgCost();
game.silversUpgLevel = game.silversUpgLevel + 1;
updateUI();
}
}
}
function updateUI() {
document.getElementById("spnCoppersValue").innerHTML = game.coppers;
document.getElementById("btnUpgCopperMine").innerHTML = "Улучшить медную шахту, ";
document.getElementById("btnUpgCopperMine").innerHTML += coppersUpgCost().toString();
document.getElementById("btnUpgCopperMine").innerHTML += " медных монет";
document.getElementById("spnCoppersRate").innerHTML = game.copperGrowth*game.coppersUpgLevel;
document.getElementById("spnSilversValue").innerHTML = game.silvers;
if (game.silversUpgLevel===0) {
document.getElementById("btnUpgSilverMine").innerHTML = "Построить серебряную шахту, ";
document.getElementById("btnUpgSilverMine").innerHTML += silverMineBasePriceCoppers.toString();
document.getElementById("btnUpgSilverMine").innerHTML += " медных монет";
} else {
document.getElementById("btnUpgSilverMine").innerHTML = "Улучшить серебряную шахту, ";
document.getElementById("btnUpgSilverMine").innerHTML += silversUpgCost().toString();
document.getElementById("btnUpgSilverMine").innerHTML += " серебряных монет";
}
document.getElementById("spnSilversRate").innerHTML = game.silverGrowth*game.silversUpgLevel;
}
function saveGame() {
localStorage.setItem('gameTutorial', JSON.stringify(game));
}
function loadGame() {
game = JSON.parse(localStorage.getItem('gameTutorial'));
updateUI();
}
</script>
Что сделано? В интерфейсе добавлены поля для отображения серебряных монет, скорость их добычи, кнопка для постройки или улучшения серебряной шахты.
Внутрь игрового объекта продублированы свойства silvers, silverGrowth, silversUpgLevel, но для последнего установлено значение 0, потому что серебряной шахты у нас по умолчанию нет.
Добавлена переменная silverMineBasePriceCoppers, которая будет отражать цену постройки серебряной шахты в медных монетах (потому что мы не можем платить за серебряную шахту серебром, которого у нас пока нет).
Модифицирована функция coppersUpgCost и продублирована как silversUpgCost, чтобы цена апгрейда была отличной от нуля, в случае, если текущий уровень шахты равен 0.
Изменили функцию обсчета конца хода, добавив туда расчет дохода от серебряной шахты, а также изменив условие победы — теперь нам нужно сравнивать не количество медных монет, а количество серебряных монет.
Создали функцию upgSilverMine, в которой отразим логику списывания средств (если шахты еще нет, списываем медяки, если шахта уже построена, то списываем серебро для улучшения шахты).
В функцию updateUI добавили необходимые строчки для серебра, а также разделили логику вывода текста на кнопку для улучшения серебряной шахты, таким образом одна кнопка работает и для постройки и для улучшения шахты.
Функции сохранения и загрузки остались без изменений.
Ссылка на игру в текущем состоянии: 03eb0eb.
Может так случиться, что медных монет накопиться много больше, чем надо, в то время как серебро еще нужно заработать. Для этого в подобных играх, как правило, существует возможность обмена разных ресурсов друг на друга.
Давайте добавим в интерфейс пару кнопок:
<button id="btnUpgCopperMine" onclick="upgCopperMine()" style="width: 240px;">Улучшить медную шахту, 15 медных монет</button><br>
Медные монеты: <span id="spnCoppersValue">0</span><br>
Скорость заработка медных монет: <span id="spnCoppersRate">1</span> в 2 секунды<br>
<button id="btnBuySilver" onclick="buySilver()" style="width: 240px;">Купить 1 серебро за 100 медных</button><br>
<hr>
<button id="btnUpgSilverMine" onclick="upgSilverMine()" style="width: 240px;">Построить серебряную шахту, 50 медных монет</button><br>
Серебряные монеты: <span id="spnSilversValue">0</span><br>
Скорость заработка серебряных монет: <span id="spnSilversRate">0</span> в 2 секунды<br>
<button id="btnBuySilver" onclick="buyCoppers()" style="width: 240px;">Купить 100 медных за 1 серебро </button><br>
<hr>
<button id="btnSaveGame" onclick="saveGame()" style="width: 240px;">Сохранить игру</button><br>
<button id="btnLoadGame" onclick="loadGame()" style="width: 240px;">Загрузить игру</button><br>
И добавим пару функций к этим кнопкам:
function buySilver() {
if (game.coppers>=100) {
game.coppers = game.coppers - 100;
game.silvers = game.silvers + 1;
updateUI();
}
}
function buyCoppers() {
if (game.silvers>=1) {
game.coppers = game.coppers + 100;
game.silvers = game.silvers - 1;
updateUI();
}
}
Ссылка на игру в текущем состоянии: 92219b2.
Так, что еще можно добавить в инкрементальную игру? Механизм Престижа! Конечно, сейчас это скорее излишество, но в случае дальнейшего развития игры, он вам пригодится. Игроки любят его!
В разных играх сделано по-разному, но в целом — механизм дает очки Престижа либо после успешного завершения (прохождения) игры, либо, разблокируется во время игры после достижения некоего целевого порога.
С помощью очков Престижа игрок получает возможность быстрого старта или постоянного бонуса в этой же игре (возможно, что в следующей миссии, если игра разбита на таковые), так, что он может попробовать какую-то другую тактику, но тратит меньше времени на повторное прохождение. Таким образом, можно изучить несколько развилок, опробовав разные решения, и затрачивая на это меньше времени, чем если бы такового механизма не было.
Что нам потребуется для этого? Нужен механизм перезапуска игры со значениями по умолчанию. В качестве постоянного бонуса, будем добавлять очки Престижа к ранее неизменяемым свойствам copperGrowth и silverGrowth.
Добавляем следующие функции winGame, restartGameDialog, restartGame, а также изменим endOfTurnCalc для вызова новых функций и обновим updateUI:
function endOfTurnCalc() {
if (game.silvers < win_condition) {
game.coppers = game.coppers+game.copperGrowth*game.coppersUpgLevel;
game.silvers = game.silvers+game.silverGrowth*game.silversUpgLevel;
updateUI();
} else {
winGame();
}
}
function winGame() {
clearTimeout(myTimer);
alert("Вы достигли цели! Вы накопили "+win_condition.toString());
myRestartTimer = setInterval(restartGameDialog, 2000);
}
function restartGameDialog() {
if (confirm('Хотите сыграть еще раз с очками Престижа?')) {
restartGame();
} else {
clearTimeout(myRestartTimer);
}
}
function restartGame() {
game.coppers = 1;
game.copperGrowth = game.copperGrowth+1;
game.coppersUpgLevel = 1;
game.silvers = 0;
game.silverGrowth = game.silverGrowth+1;
game.silversUpgLevel = 0;
clearTimeout(myRestartTimer);
myTimer = setInterval(endOfTurnCalc, 2000);
updateUI();
}
function updateUI() {
...
if (game.copperGrowth!==1) {
document.getElementById("divLblPrestige").innerHTML = "Мультипликатор от эффекта Престижа равен "+game.copperGrowth.toString();
document.getElementById("divLblPrestige").style.display = "block";
} else {
document.getElementById("divLblPrestige").style.display = "none";
}
}
Функция winGame запускает таймер, по истечению которого вызывается диалог подтверждения. В случае подтверждения вызывается функция рестарта, которая сбрасывает все значения на умолчанию, но увеличивает свойства базового прироста монет.
В целом, игра готова:
— создание ресурса;
— добычу ресурса;
— трата ресурса на апгрейд системы его добычи;
— отражение скорости добычи ресурса;
— условие победы;
— запись и чтение сохранения игры в локальное хранилище браузера;
— добавление еще одного ресурса с добычей и апгрейдом его добычи;
— обмен ресурсов;
— механизм рестарта игры и механизм Престижа.
Ссылка на игру в текущем состоянии: 92219b2.
Наконец, последнее, что в этой статье хочется показать, это другой механизм хранилища, потому что у текущего механизма есть одна, но существенная проблема.
Представьте себе, что вы разрабатываете игру, публикуете ее на веб-сервере (возможно, на github pages), а люди в нее играют. Со временем, вы вносите изменения в игру, которые добавляют новые свойства к игровому объекту game.
Теперь следите за руками.
Игрок заходит со старым сохранением, в котором есть все свойства coppers и silvers, но нет, скажем, свойства gold. Загружается наш game объект и перезаписывает тот, который был создан при загрузке веб-страницы, и теперь внутри объекта есть только те свойства, которые были в его сохранении. А код-то мы уже обновили! И этот код ссылается на обновленные свойства, которых нет. Таким образом, код начинает получить множественные ошибки, вплоть до полной неработоспособности игры. На удивление, чтобы исправить эту проблему, нужно переписать всего две строчки в функции загрузки игры:
function loadGame() {
gameTemp = JSON.parse(localStorage.getItem('gameTutorial'));
for (var propertyName in gameTemp) { game[propertyName] = gameTemp[propertyName]; }
updateUI();
}
Теперь, если добавите в game = { gold: 1, } и загрузитесь с вашего старого сохранения, где золота еще не было, то золото останется в объекте и игровая логика нигде не сломается.
Ссылка на игру в текущем состоянии: 83c258d.
Еще одно. Перенос сохранений между браузерами, давайте его тоже запилим.
<hr>
<button id="btnSaveGame" onclick="saveGame()" style="width: 240px;">Сохранить игру</button><br>
<br>
<button id="btnExportGame" onclick="exportGame()" style="width: 240px;">Экспортировать игру</button><br>
<div id="divLblExport" style="display: none"></div>
<br>
<hr>
<button id="btnLoadGame" onclick="loadGame()" style="width: 240px;">Загрузить игру</button><br>
<br>
<button id="btnImportGame" onclick="importGame()" style="width: 240px;">Импортировать игру</button><br>
Обновим интерфейс.
let countdown = 30;
let showExport = 0;
function updateUI() {
...
if (showExport===1){
document.getElementById("divLblExport").style.display = "block";
} else {
document.getElementById("divLblExport").style.display = "none";
}
}
function exportGame() {
exportTimer = setInterval(exportCountdown, 1000);
document.getElementById("divLblExport").innerHTML = btoa(JSON.stringify(game));
showExport = 1;
updateUI();
}
function exportCountdown() {
if (countdown > 0) {
countdown = countdown - 1;
} else {
clearTimeout(exportTimer);
countdown = 30;
showExport = 0;
updateUI();
}
}
function importGame() {
let importString = prompt('Введите длинную строку экспорта');
gameTemp = JSON.parse(atob(importString));
for (var propertyName in gameTemp) { game[propertyName] = gameTemp[propertyName]; }
updateUI();
}
Добавили две служебных переменных, обновили updateUI(), написали три функции — экспорта, импорта, и функцию, которая изменяет флаг отображения экспорта, таким образом «старый» экспорт через 30 секунд после его формирования будет скрыт.
На сегодня всё.