├── README.md ├── game.js ├── img ├── balloon.png ├── boom.png └── landscape.png ├── index.html ├── package.json └── style.css /README.md: -------------------------------------------------------------------------------- 1 | # Простая HTML5 игра 2 | 3 | [Демо-версия игры](https://netology-code.github.io/neto-bubble-game/) 4 | 5 | Лопни как можно больше шариков за 15 секунд. Игра создана в рамках [открытого занятия](http://netology.ru/free-lessons/dev-browser-game) по случаю очередного набора курса по [Web API](http://netology.ru/programs/html-javascript) в университете [Нетология](http://netology.ru/). 6 | 7 | Для запуска локально: 8 | 9 | 1. Клонируйте репозиторий себе `git clone https://github.com/netology-code/neto-bubble-game.git` 10 | 2. Установите зависимости `npm install`. 11 | 3. Запустите локальный сервер `npm start`. 12 | -------------------------------------------------------------------------------- /game.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | function rand(from, to) { 3 | return Math.floor((to - from + 1) * Math.random()) + from; 4 | } 5 | 6 | function randomItem(list) { 7 | const next = list[rand(0, list.length - 1)]; 8 | if (randomItem.prev && randomItem.prev === next) { 9 | return randomItem(list); 10 | } 11 | randomItem.prev = next; 12 | return next; 13 | } 14 | 15 | function loadTopScore() { 16 | if (!localStorage) return 0; 17 | const score = localStorage.getItem('topScore'); 18 | return score ? score : 0; 19 | } 20 | 21 | function saveTopScore(score) { 22 | if (!localStorage) return; 23 | localStorage.setItem('topScore', score); 24 | } 25 | 26 | function show(hole) { 27 | hole.classList.remove('boom'); 28 | hole.classList.add('up'); 29 | } 30 | 31 | function hide(hole) { 32 | hole.classList.remove('up'); 33 | } 34 | 35 | function timeToString(time) { 36 | const MSECONDS_IN_SEC = 1000; 37 | const MSECONDS_IN_MIN = 60 * MSECONDS_IN_SEC; 38 | 39 | let min = Math.floor(time / MSECONDS_IN_MIN); 40 | let sec = Math.floor((time % MSECONDS_IN_MIN) / MSECONDS_IN_SEC); 41 | let msec = (time % MSECONDS_IN_MIN) % MSECONDS_IN_SEC; 42 | let spacer = msec > 500 ? ':' : ' '; 43 | return [min, sec] 44 | .map(number => number >= 10 ? number : `0${number}`) 45 | .join(spacer); 46 | } 47 | 48 | function updateTimer() { 49 | if (!isStarted) { 50 | return; 51 | } 52 | 53 | let timeout = GAME_TIMEOUT - (Date.now() - startedAt); 54 | if (timeout < 0 ) { 55 | timeout = 0; 56 | } 57 | timer.innerHTML = timeToString(timeout); 58 | } 59 | 60 | function updateScoreboard(points) { 61 | scoreboard.dataset.points = points; 62 | bestScore.dataset.points = topScore; 63 | } 64 | 65 | function handleClick() { 66 | const hole = this.parentElement; 67 | if (!hole.timeout) { 68 | return; 69 | } 70 | clearTimeout(hole.timeout); 71 | hole.classList.add('boom'); 72 | setTimeout(() => { 73 | hide(hole); 74 | ++points; 75 | updateScoreboard(points); 76 | }, 50); 77 | } 78 | 79 | function next() { 80 | const hole = randomItem(holes); 81 | show(hole); 82 | hole.timeout = setTimeout(i => { 83 | hide(hole); 84 | }, rand(800, 2500)); 85 | } 86 | 87 | function tic() { 88 | setTimeout(() => { 89 | if (isStarted) { 90 | next(); 91 | tic(); 92 | } else { 93 | startButton.style.display = 'initial'; 94 | topScore = Math.max(points, topScore); 95 | saveTopScore(topScore); 96 | updateScoreboard(points); 97 | clearInterval(timerInterval); 98 | } 99 | }, rand(500, 2500)); 100 | } 101 | 102 | function start() { 103 | points = 0; 104 | startedAt = Date.now(); 105 | updateScoreboard(points); 106 | startButton.style.display = 'none'; 107 | isStarted = true; 108 | setTimeout(() => isStarted = false, GAME_TIMEOUT); 109 | timerInterval = setInterval(updateTimer, 250); 110 | tic(); 111 | } 112 | 113 | const GAME_TIMEOUT = 15000; 114 | let timeout, timerInterval, isStarted = false, startedAt; 115 | let topScore = loadTopScore(); 116 | let points = 0; 117 | 118 | const holes = document.getElementsByClassName('hole'); 119 | const bubbles = document.getElementsByClassName('bubble'); 120 | const scoreboard = document.getElementById('currentScoreView'); 121 | const bestScore = document.getElementById('topScoreView'); 122 | const startButton = document.querySelector('.startButton'); 123 | const timer = document.querySelector('.timer'); 124 | Array.from(bubbles).forEach(bubble => bubble.addEventListener('click', handleClick)); 125 | startButton.addEventListener('click', start); 126 | 127 | updateScoreboard(points); 128 | -------------------------------------------------------------------------------- /img/balloon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VitRod/JavaScript/2b077f31cf086617618c7fd665cac62a9cc1a689/img/balloon.png -------------------------------------------------------------------------------- /img/boom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VitRod/JavaScript/2b077f31cf086617618c7fd665cac62a9cc1a689/img/boom.png -------------------------------------------------------------------------------- /img/landscape.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VitRod/JavaScript/2b077f31cf086617618c7fd665cac62a9cc1a689/img/landscape.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Игра в шарики на JavaScript 7 | 8 | 9 | 10 |
11 |
12 | 00:00 13 |
14 |
15 | очков 16 |
17 |
18 | Лучший результат: очков 19 |
20 |
21 | 22 |
23 | 24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "neto-bubble-game", 3 | "version": "1.0.0", 4 | "description": "Simple HTML5 bubble game", 5 | "main": "index.html", 6 | "scripts": { 7 | "start": "browser-sync start --server --files '**/*.html' '**/*.css' '**/*.js' '**/*.png' '**/*.jpeg' '**/*.jpg'", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/netology-code/wg-hj-fl-game.git" 13 | }, 14 | "keywords": ["game", "javascript", "vanilla javascript", "html5", "bubbles"], 15 | "contributors": [ 16 | "Lina Dubrovskaya ", 17 | "Dima Fitiskin " 18 | ], 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/netology-code/wg-hj-fl-game/issues" 22 | }, 23 | "homepage": "https://github.com/netology-code/wg-hj-fl-game#readme", 24 | "devDependencies": { 25 | "browser-sync": "^2.18.8" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | html { 2 | margin: 0; 3 | padding: 0; 4 | width: 100%; 5 | height: 100%; 6 | font-family: sans-serif; 7 | } 8 | 9 | body { 10 | margin: 0 auto; 11 | padding: 0; 12 | width: 100%; 13 | max-width: 900px; 14 | height: 100%; 15 | } 16 | 17 | .gamefield { 18 | position: relative; 19 | width: 100%; 20 | height: 300px; 21 | min-height: 200px; 22 | max-height: calc( 100% - 70px ); 23 | padding: 0 20%; 24 | box-sizing: border-box; 25 | background: #afafaf url(img/landscape.png) no-repeat; 26 | background-size: 100% 100%; 27 | font-size: 0; 28 | } 29 | 30 | .hole { 31 | display: inline-block; 32 | text-align: center; 33 | width: 20%; 34 | height: 100%; 35 | overflow: hidden; 36 | } 37 | 38 | .bubble { 39 | width: 30px; 40 | height: 50px; 41 | background: url(img/balloon.png) no-repeat center center; 42 | background-size: 100% auto; 43 | transition: top 0.45s; 44 | display: inline-block; 45 | position: relative; 46 | top: 100%; 47 | } 48 | 49 | .up > .bubble { 50 | top: 20px; 51 | } 52 | 53 | .boom > .bubble { 54 | background-image: url(img/boom.png); 55 | } 56 | 57 | .captured > .bubble { 58 | margin-left: 30px; 59 | width: 40px; 60 | height: 40px; 61 | } 62 | 63 | 64 | header { 65 | margin: 0; 66 | padding: 10px; 67 | font-size: 14px; 68 | display: flex; 69 | justify-content: space-between; 70 | align-items: center; 71 | } 72 | 73 | .startButton { 74 | position: absolute; 75 | top: 50%; 76 | left: 50%; 77 | padding: 10px 50px; 78 | cursor: pointer; 79 | border: none; 80 | border-radius: 3px; 81 | background-color: #ccc; 82 | font-size: 20px; 83 | text-align: center; 84 | transform: translate(-50%, -50%); 85 | } 86 | .startButton:hover { 87 | background-color: #ddd; 88 | } 89 | .scoreboard::before { 90 | content: attr(data-points) ' '; 91 | } 92 | .timer { 93 | height: 30px; 94 | padding: 0 10px; 95 | margin-left: 10px; 96 | text-align: center; 97 | line-height: inherit; 98 | box-shadow: 0 0 1px #ccc; 99 | } 100 | footer { 101 | padding: 10px; 102 | text-align: center; 103 | } 104 | --------------------------------------------------------------------------------