У нашего кликера уже есть очки, энергия, одна покупка и победа. Теперь завершим игру: добавим полный перезапуск, проверим правила после нескольких попыток и самостоятельно изменим поведение отдыха. В результате у тебя будет маленькая законченная игра, устройство которой ты можешь объяснить.
Собирай кристаллы, восстанавливай энергию и при желании купи улучшение. Цель — накопить 20 очков. После победы начни новую партию той же кнопкой, которой можно сбросить незавершённую попытку.
Сначала сыграй одну партию и нажми «Новая партия». Обрати внимание: исчезают и очки, и купленное улучшение, а энергия возвращается к десяти.
С какой версии начинаем
Нужен результат урока «Как найти ошибку в JavaScript, когда кнопка не работает». Ты уже умеешь хранить число в переменной, находить элемент, передавать функцию в addEventListener, проверять условие и возвращать значение через return. Здесь мы собираем эти навыки вместе.
Продолжай свою рабочую папку. Если её нет, скачай архив ниже и открой start/index.html. Рядом должны лежать style.css и game.js. Стартовые файлы совпадают с проверенной финальной версией предыдущего занятия. В них уже можно выиграть, но кнопки новой партии ещё нет.
start/ — результат предыдущего урока; step-01/ — перезапуск; step-02/ — подписи из правил; final/ — завершённый кликер. Инструкция по запуску и версиям доступна в просмотрщике исходников. Папка solutions/ содержит ответ на самостоятельное задание: отложи её до своей попытки.
Для этих трёх файлов достаточно открыть index.html в браузере: сетевых запросов, модулей и сохранений здесь нет. Если ты уже запускаешь проект локальным сервером, продолжай тем же способом. После изменения файла сохрани его и обнови страницу.
Что должна сбросить новая партия
Перезагрузка страницы заново выполняет скрипт, поэтому игра начинается с нуля. Добавим кнопку, чтобы игрок мог начать новую партию прямо в игре. Кроме того, нам важно научиться возвращать начальное состояние самим: этот принцип понадобится и в карточках, и в лабиринте.
Перезапуск нельзя свести к надписи «Очки: 0». Число score останется прежним, и следующий клик вернёт старый счёт. Надпись — только представление данных. Сначала меняем данные, потом вызываем render().
| Переменная | Что помнит | В начале партии |
|---|---|---|
score | Накопленные очки | 0 |
energy | Сколько сборов осталось до отдыха | 10 |
clickPower | Награду за один сбор | 1 |
upgraded | Куплено ли улучшение | false |
won | Закончилась ли партия победой | false |
Константы upgradePrice = 10 и winningScore = 20 описывают правила. Во время партии они не меняются, поэтому возвращать их к прежним значениям не требуется. А upgraded и won нужно сбросить обязательно: иначе магазин или сбор останутся заблокированы.
Добавляем кнопку и полный сброс
Открой index.html. Внутри блока с игровыми кнопками, после кнопки улучшения, добавь ещё одну:
<button id="reset-button" type="button">Новая партия</button>
Сохрани файл и обнови страницу. Кнопка должна появиться рядом с остальными и переноситься на следующую строку на узком экране. Пока она ничего не меняет: HTML создаёт элемент, а действие мы подключим в JavaScript.
В начале game.js, рядом с остальными вызовами getElementById, найди новую кнопку:
const resetButton = document.getElementById('reset-button');
После функции handleUpgrade добавь функцию перезапуска. Объяви её отдельно, после закрывающей скобки handleUpgrade:
function handleReset() {
score = 0;
energy = 10;
clickPower = 1;
upgraded = false;
won = false;
render();
}
Все пять присваиваний выполняются при каждом перезапуске. Только после них render() обновляет показатели и доступность кнопок. Сам render состояние не меняет: несколько вызовов подряд не должны начислять очки или покупать улучшение.
Внизу файла уже есть три строки, связывающие сбор, отдых и покупку с функциями. Рядом с ними, перед последним render(), добавь одну строку:
resetButton.addEventListener('click', handleReset);
Мы передаём имя handleReset без скобок: браузер вызовет функцию при клике. Обработчик регистрируется один раз при загрузке файла. Внутри handleReset не должно быть addEventListener. Иначе каждый перезапуск может добавлять новую реакцию на будущее нажатие.
Проверка этапа: собери несколько очков, отдохни, нажми «Новая партия» и снова собери один кристалл. Должно получиться ровно одно очко и девять единиц энергии. Если кнопка не найдена, проверь одинаковое написание reset-button в HTML и селекторе; загляни в Console и номер строки. Рабочий результат этого этапа находится в step-01/.
Проверяем, что партия действительно закончена
Победа уже добавлена в предыдущих занятиях. Теперь нужно проверить её вместе с новой кнопкой. У игры одна ясная цель: текущее количество очков достигает двадцати. Очки, потраченные на улучшение, в текущий счёт не входят. Достигнув цели, игрок видит результат и может начать снова.
Разберись, через какие части программы проходит действие. handleGather проверяет won и энергию, получает награду из calculateReward, меняет очки и энергию, проверяет победу и вызывает render. handleUpgrade проверяет условия через canBuyUpgrade, списывает цену и меняет силу клика. Перезапуск возвращает данные, на которые опираются все эти функции.
Кнопки становятся недоступными через disabled, но проверки внутри обработчиков тоже остаются. Недоступная кнопка подсказывает игроку, что действие запрещено. Проверка в функции не позволит выполнить его и при вызове из консоли. Проверяй обе части.
| Действие | Ожидаемый результат |
|---|---|
| Свежий запуск; один сбор | Очки 1, энергия 9, сила 1 |
| Покупка до 10 очков | Недоступна, счёт не становится отрицательным |
| Десять сборов, затем покупка | Очки 0, энергия 0, сила 2; повторная покупка недоступна |
| Отдых после покупки; десять сборов | Очки 20 и победа; сбор, отдых и покупка недоступны |
| Новая партия после победы; один сбор | Очки 1, энергия 9, сила 1; улучшение снова можно будет купить |
| Пять быстрых перезапусков; один сбор | Всё ещё ровно одно очко: обработчик сработал один раз |
| Перезапуск при нулевой энергии до победы | Энергия 10, сбор снова доступен, очки 0 |
Запиши фактический результат рядом с ожидаемым. Если что-то не совпало, сначала сформулируй наблюдение: например, «после сброса сила осталась равна двум». Затем найди переменную, которая хранит это значение, и проверь соответствующее присваивание. Не заменяй весь файл финалом, пока не понял причину расхождения.
Одна цена в правилах и в интерфейсе
Пока число 20 повторяется в условии победы и в надписях. Если позже изменить цель, можно забыть обновить текст. Пусть функция render берёт цель из winningScore, а цену — из upgradePrice. Для этого достаточно знакомого соединения строк через плюс.
В render() после powerElement.textContent = clickPower; добавь:
upgradeButton.textContent = 'Улучшить за ' + upgradePrice;
В ветке if (won) замени строку сообщения о победе, а в последнем else — строку с целью:
// Внутри if (won):
statusElement.textContent = 'Победа! Ты собрал ' + winningScore + ' кристаллов.';
// Внутри последнего else:
statusElement.textContent = 'Цель: ' + winningScore + ' кристаллов. Улучшение за ' + upgradePrice + ' удваивает силу.';
Это замены в двух местах, а не две соседние строки в конце функции. Сообщение о пустой энергии оставь прежним. Теперь временно поставь winningScore = 22, обнови страницу и проверь новую подпись и победу при 22. Верни 20 перед дальнейшей работой. Та же проверка цены: временно поставь 12, проверь текст покупки и верни 10. Версия step-02/ содержит эти изменения с исходными правилами 20 и 10.
final/ повторяет работающий step-02/. Теперь в игре можно набрать нужные очки, победить и начать новую партию.
Самостоятельно: отдых восстанавливает энергию постепенно
Сначала сохрани рабочую копию кликера. В новой версии измени одно правило: «Отдохнуть» прибавляет три единицы энергии, а максимальный запас остаётся десять. Во время победы отдых по-прежнему недоступен. Новая партия возвращает ровно десять единиц независимо от того, сколько было до сброса.
Попробуй написать изменение сам: найди функцию отдыха и при необходимости обнови подпись кнопки. Другие правила сохраняются. До запуска предскажи результат для энергии 0, 8 и 10. Затем проверь эти случаи через обычные действия: потрать всю энергию, потрать две единицы или начни новую партию.
Ожидаемый результат: из 0 получается 3, из 8 — 10, из 10 — 10. Можно отдохнуть несколько раз, но выйти за 10 нельзя. После победы энергия не меняется; после сброса снова 10. Объясни своими словами, почему одного energy = energy + 3 недостаточно.
Подсказка 1: в какой функции искать
Изменение должно происходить в handleRest. Сохрани проверку won в начале. После неё сначала прибавь восстановление, затем проверь, не стал ли запас больше предела. Последним оставь render().
Подсказка 2: как ограничить запас
Используй уже знакомое условие if (energy > 10). Если оно верно, присвой энергии 10. Этого условия достаточно, чтобы запас не превысил десять.
Сверить решение после своей попытки
Открой объяснение во вкладке «Решение» просмотрщика исходников или запусти вариант с постепенным отдыхом. Его файлы лежат в solutions/ скачанного ZIP. Сравни именно функцию отдыха. Если воспользовался ответом, закрой его и повтори похожее правило с восстановлением двух единиц, объясняя каждое действие.
Когда можно двигаться дальше
Покажи работающую партию, покупку, победу и сброс. Затем без финального образца объясни, какие данные хранят состояние, почему их нельзя заменить текстом надписи и почему обработчик перезапуска подключён снаружи функции. Проверь своё правило отдыха при 0, 8 и 10 единицах энергии, а также после победы.
Небольшое закрепление: по одной таблице начальных значений восстанови handleReset, не подглядывая в готовую функцию. Справочник синтаксиса использовать можно. Если пропадает кнопка, вернись к DOM и селекторам; если срабатывает неверное правило — к условиям; если не можешь найти причину ошибки — к предыдущему занятию по отладке.