Sebastiankay
commited on
Commit
•
44f8732
1
Parent(s):
41c3a47
Update index.html
Browse filesadding initial index.html
- index.html +709 -12
index.html
CHANGED
@@ -1,19 +1,716 @@
|
|
1 |
-
<!
|
2 |
<html>
|
3 |
<head>
|
4 |
-
<
|
5 |
-
<meta
|
6 |
-
<
|
7 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
8 |
</head>
|
9 |
<body>
|
10 |
-
<div class="
|
11 |
-
<
|
12 |
-
<
|
13 |
-
<p>
|
14 |
-
Also don't forget to check the
|
15 |
-
<a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
|
16 |
-
</p>
|
17 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
18 |
</body>
|
19 |
</html>
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
<html>
|
3 |
<head>
|
4 |
+
<title>Bubble Shooter</title>
|
5 |
+
<meta charset="UTF-8" />
|
6 |
+
<meta name="description" content="One-minute creation by AI Coding Autonomous Agent MOUSE-I" />
|
7 |
+
<meta name="keywords" content="AI Coding, Bubble Shooter, MOUSE-I, Sebastian Kay, Browser game" />
|
8 |
+
<meta name="author" content="Sebastian Kay" />
|
9 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
10 |
+
<link href="https://fonts.googleapis.com/css2?family=Tiny5&display=swap" rel="stylesheet" />
|
11 |
+
<style>
|
12 |
+
* {
|
13 |
+
margin: 0;
|
14 |
+
padding: 0;
|
15 |
+
box-sizing: border-box;
|
16 |
+
font-family: "Tiny5", cursive;
|
17 |
+
}
|
18 |
+
|
19 |
+
body {
|
20 |
+
background-color: #1a1a1a;
|
21 |
+
display: flex;
|
22 |
+
flex-direction: column;
|
23 |
+
align-items: center;
|
24 |
+
min-height: 100vh;
|
25 |
+
color: #fff;
|
26 |
+
}
|
27 |
+
|
28 |
+
.game-container {
|
29 |
+
position: relative;
|
30 |
+
margin: auto 0;
|
31 |
+
background: #2d2d2d;
|
32 |
+
padding: 20px;
|
33 |
+
border-radius: 10px;
|
34 |
+
box-shadow: 0 0 20px rgba(0, 0, 0, 0.3);
|
35 |
+
}
|
36 |
+
|
37 |
+
.info-panel {
|
38 |
+
display: flex;
|
39 |
+
justify-content: space-between;
|
40 |
+
align-items: center;
|
41 |
+
margin-bottom: 10px;
|
42 |
+
padding: 10px;
|
43 |
+
background: rgba(0, 0, 0, 0.2);
|
44 |
+
border-radius: 5px;
|
45 |
+
font-size: 14px;
|
46 |
+
line-height: 1.5;
|
47 |
+
}
|
48 |
+
|
49 |
+
canvas {
|
50 |
+
border: 2px solid #444;
|
51 |
+
border-radius: 5px;
|
52 |
+
}
|
53 |
+
|
54 |
+
.controls {
|
55 |
+
position: absolute;
|
56 |
+
margin-top: 26px;
|
57 |
+
left: 0;
|
58 |
+
right: 0;
|
59 |
+
text-align: center;
|
60 |
+
font-size: 0.9rem;
|
61 |
+
color: rgba(136, 136, 136, 0.2);
|
62 |
+
}
|
63 |
+
|
64 |
+
button {
|
65 |
+
background: #444;
|
66 |
+
color: #fff;
|
67 |
+
border: none;
|
68 |
+
padding: 10px 20px;
|
69 |
+
font-size: 12px;
|
70 |
+
cursor: pointer;
|
71 |
+
margin: 5px;
|
72 |
+
border-radius: 5px;
|
73 |
+
font-family: "Tiny5", cursive;
|
74 |
+
}
|
75 |
+
|
76 |
+
button:hover {
|
77 |
+
background: #555;
|
78 |
+
}
|
79 |
+
|
80 |
+
div.game-over,
|
81 |
+
.start-screen {
|
82 |
+
position: absolute;
|
83 |
+
top: 50%;
|
84 |
+
left: 50%;
|
85 |
+
transform: translate(-50%, -50%);
|
86 |
+
text-align: center;
|
87 |
+
display: none;
|
88 |
+
z-index: 20;
|
89 |
+
h2 {
|
90 |
+
--h-bg-color: #b9fecd;
|
91 |
+
background: var(--h-bg-color);
|
92 |
+
border: 5px solid var(--h-bg-color);
|
93 |
+
transform: rotate(-1deg);
|
94 |
+
font-size: 2.4rem;
|
95 |
+
color: rgba(0, 0, 0, 0.5);
|
96 |
+
border-radius: 5px;
|
97 |
+
}
|
98 |
+
h2.game-over {
|
99 |
+
--h-bg-color: #ffb3ba !important;
|
100 |
+
background: var(--h-bg-color);
|
101 |
+
border: 5px solid var(--h-bg-color);
|
102 |
+
transform: rotate(-1deg);
|
103 |
+
font-size: 2.4rem;
|
104 |
+
color: rgba(0, 0, 0, 0.5);
|
105 |
+
border-radius: 5px;
|
106 |
+
}
|
107 |
+
}
|
108 |
+
|
109 |
+
.pause-screen {
|
110 |
+
inset: 0;
|
111 |
+
backdrop-filter: blur(2px);
|
112 |
+
background: rgba(0, 0, 0, 0.5);
|
113 |
+
position: absolute;
|
114 |
+
display: none;
|
115 |
+
z-index: 20;
|
116 |
+
.inner {
|
117 |
+
position: absolute;
|
118 |
+
top: 50%;
|
119 |
+
left: 50%;
|
120 |
+
transform: translate(-50%, -50%);
|
121 |
+
text-align: center;
|
122 |
+
background: rgba(0, 0, 0, 0.7);
|
123 |
+
padding: 40px;
|
124 |
+
border-radius: 10px;
|
125 |
+
}
|
126 |
+
}
|
127 |
+
|
128 |
+
.score {
|
129 |
+
position: absolute;
|
130 |
+
top: 20px;
|
131 |
+
right: 20px;
|
132 |
+
text-align: right;
|
133 |
+
}
|
134 |
+
</style>
|
135 |
</head>
|
136 |
<body>
|
137 |
+
<div class="start-screen">
|
138 |
+
<h2>BUBBLE SHOOTER</h2>
|
139 |
+
<button id="startButton">START GAME</button>
|
|
|
|
|
|
|
|
|
140 |
</div>
|
141 |
+
<div class="game-over">
|
142 |
+
<h2 class="game-over">GAME OVER</h2>
|
143 |
+
<button id="restartButton">PLAY AGAIN</button>
|
144 |
+
</div>
|
145 |
+
<div class="pause-screen">
|
146 |
+
<div class="inner">
|
147 |
+
<h2>PAUSED</h2>
|
148 |
+
<button id="resumeButton">RESUME</button>
|
149 |
+
</div>
|
150 |
+
</div>
|
151 |
+
<div class="game-container">
|
152 |
+
<div class="info-panel">
|
153 |
+
<div>LEVEL: <span id="level">1</span></div>
|
154 |
+
<div>SCORE: <span id="score">0</span></div>
|
155 |
+
<div>HIGH: <span id="highScore">0</span></div>
|
156 |
+
<button id="pauseButton" class="btn">ǁ (P)</button>
|
157 |
+
</div>
|
158 |
+
<canvas width="400" height="600" id="game"></canvas>
|
159 |
+
<div class="controls">
|
160 |
+
<p>← → : AIM | ↑ : SHOOT | P : PAUSE</p>
|
161 |
+
<p>OR USE MOUSE TO AIM AND CLICK TO SHOOT</p>
|
162 |
+
</div>
|
163 |
+
</div>
|
164 |
+
|
165 |
+
<script>
|
166 |
+
const canvas = document.getElementById("game")
|
167 |
+
const context = canvas.getContext("2d")
|
168 |
+
const grid = 39
|
169 |
+
const body = document.querySelector("body")
|
170 |
+
const buttonsAll = document.getElementsByTagName("button")
|
171 |
+
const startButton = document.getElementById("startButton")
|
172 |
+
const restartButton = document.getElementById("restartButton")
|
173 |
+
const pauseButton = document.getElementById("pauseButton")
|
174 |
+
const resumeButton = document.getElementById("resumeButton")
|
175 |
+
|
176 |
+
const levelValue = document.getElementById("level")
|
177 |
+
const scoreValue = document.getElementById("score")
|
178 |
+
const highScoreValue = document.getElementById("highScore")
|
179 |
+
|
180 |
+
const startScreen = document.querySelector(".start-screen")
|
181 |
+
const gameoverScreen = document.querySelector("div.game-over")
|
182 |
+
|
183 |
+
let addedLevel = 0
|
184 |
+
let isOddRow = false
|
185 |
+
let addRowInterval
|
186 |
+
|
187 |
+
function initGame() {
|
188 |
+
gameState = {
|
189 |
+
isGameOver: false,
|
190 |
+
isPaused: false,
|
191 |
+
isPlaying: true,
|
192 |
+
score: 0,
|
193 |
+
level: 1,
|
194 |
+
}
|
195 |
+
|
196 |
+
let addRowInterval
|
197 |
+
updateUI()
|
198 |
+
}
|
199 |
+
|
200 |
+
// each even row is 10 bubbles long and each odd row is 9 bubbles long.
|
201 |
+
const level1 = Array.from({ length: 4 }, () => Array.from({ length: 10 }, () => Math.floor(Math.random() * 4)))
|
202 |
+
|
203 |
+
// create a mapping between color short code (R, G, B, Y) and color name
|
204 |
+
const colorMap = {
|
205 |
+
0: "#FFB3BA",
|
206 |
+
1: "#BAFFC9",
|
207 |
+
2: "#BAE1FF",
|
208 |
+
3: "#FFFFBA",
|
209 |
+
}
|
210 |
+
const colors = Object.values(colorMap)
|
211 |
+
|
212 |
+
// use a 1px gap between each bubble
|
213 |
+
const bubbleGap = 0.8
|
214 |
+
|
215 |
+
// the size of the outer walls for the game
|
216 |
+
const wallSize = 0
|
217 |
+
const bubbles = []
|
218 |
+
let particles = []
|
219 |
+
|
220 |
+
// helper function to convert deg to radians
|
221 |
+
function degToRad(deg) {
|
222 |
+
return (deg * Math.PI) / 180
|
223 |
+
}
|
224 |
+
|
225 |
+
// rotate a point by an angle
|
226 |
+
function rotatePoint(x, y, angle) {
|
227 |
+
let sin = Math.sin(angle)
|
228 |
+
let cos = Math.cos(angle)
|
229 |
+
|
230 |
+
return {
|
231 |
+
x: x * cos - y * sin,
|
232 |
+
y: x * sin + y * cos,
|
233 |
+
}
|
234 |
+
}
|
235 |
+
|
236 |
+
// get a random integer between the range of [min,max]
|
237 |
+
function getRandomInt(min, max) {
|
238 |
+
min = Math.ceil(min)
|
239 |
+
max = Math.floor(max)
|
240 |
+
|
241 |
+
return Math.floor(Math.random() * (max - min + 1)) + min
|
242 |
+
}
|
243 |
+
|
244 |
+
// get the distance between two points
|
245 |
+
function getDistance(obj1, obj2) {
|
246 |
+
const distX = obj1.x - obj2.x
|
247 |
+
const distY = obj1.y - obj2.y
|
248 |
+
return Math.sqrt(distX * distX + distY * distY)
|
249 |
+
}
|
250 |
+
|
251 |
+
// check for collision between two circles
|
252 |
+
function collides(obj1, obj2) {
|
253 |
+
return getDistance(obj1, obj2) < obj1.radius + obj2.radius
|
254 |
+
}
|
255 |
+
|
256 |
+
// find the closest bubbles that collide with the object
|
257 |
+
function getClosestBubble(obj, activeState = false) {
|
258 |
+
const closestBubbles = bubbles.filter((bubble) => bubble.active == activeState && collides(obj, bubble))
|
259 |
+
|
260 |
+
if (!closestBubbles.length) {
|
261 |
+
return
|
262 |
+
}
|
263 |
+
|
264 |
+
return (
|
265 |
+
closestBubbles
|
266 |
+
// turn the array of bubbles into an array of distances
|
267 |
+
.map((bubble) => {
|
268 |
+
return {
|
269 |
+
distance: getDistance(obj, bubble),
|
270 |
+
bubble,
|
271 |
+
}
|
272 |
+
})
|
273 |
+
.sort((a, b) => a.distance - b.distance)[0].bubble
|
274 |
+
)
|
275 |
+
}
|
276 |
+
|
277 |
+
// create the bubble grid bubble. passing a color will create
|
278 |
+
// an active bubble
|
279 |
+
function createBubble(x, y, color, isOddRow = false) {
|
280 |
+
const row = Math.floor(y / grid)
|
281 |
+
const col = Math.floor(x / grid)
|
282 |
+
let startX
|
283 |
+
if (row % 2 === 0) {
|
284 |
+
if (isOddRow) startX = 0.5 * grid
|
285 |
+
else startX = 0
|
286 |
+
} else startX = 0.5 * grid
|
287 |
+
const center = grid / 2
|
288 |
+
|
289 |
+
bubbles.push({
|
290 |
+
x: wallSize + (grid + bubbleGap) * col + startX + center,
|
291 |
+
y: wallSize + (grid + bubbleGap - 4) * row + center,
|
292 |
+
|
293 |
+
radius: grid / 2,
|
294 |
+
color: color,
|
295 |
+
active: color ? true : false,
|
296 |
+
})
|
297 |
+
}
|
298 |
+
// MARK: UPDATE UI
|
299 |
+
function updateUI() {
|
300 |
+
scoreValue.textContent = gameState.score
|
301 |
+
highScoreValue.textContent = localStorage.getItem("highScore") || 0
|
302 |
+
if (scoreValue.textContent > highScoreValue.textContent) {
|
303 |
+
localStorage.setItem("highScore", scoreValue.textContent)
|
304 |
+
}
|
305 |
+
if (gameState.level < Math.floor(gameState.score / 2000 + 1)) {
|
306 |
+
console.log("NEW LEVEL")
|
307 |
+
gameState.level = Math.floor(gameState.score / 2000 + 1)
|
308 |
+
levelValue.textContent = gameState.level
|
309 |
+
addRowInterval = setInterval(addNewRow, (30 - gameState.level) * 1000)
|
310 |
+
}
|
311 |
+
}
|
312 |
+
|
313 |
+
// get all bubbles that touch the passed in bubble
|
314 |
+
function getNeighbors(bubble) {
|
315 |
+
const neighbors = []
|
316 |
+
|
317 |
+
// check each of the 6 directions by "moving" the bubble by a full
|
318 |
+
// grid in each of the 6 directions (60 degree intervals)
|
319 |
+
const dirs = [
|
320 |
+
// right
|
321 |
+
rotatePoint(grid, 0, 0), // up-right
|
322 |
+
rotatePoint(grid, 0, degToRad(60)), // up-left
|
323 |
+
rotatePoint(grid, 0, degToRad(120)), // left
|
324 |
+
rotatePoint(grid, 0, degToRad(180)), // down-left
|
325 |
+
rotatePoint(grid, 0, degToRad(240)), // down-right
|
326 |
+
rotatePoint(grid, 0, degToRad(300)),
|
327 |
+
]
|
328 |
+
|
329 |
+
for (let i = 0; i < dirs.length; i++) {
|
330 |
+
const dir = dirs[i]
|
331 |
+
|
332 |
+
const newBubble = {
|
333 |
+
x: bubble.x + dir.x,
|
334 |
+
y: bubble.y + dir.y,
|
335 |
+
radius: bubble.radius,
|
336 |
+
}
|
337 |
+
const neighbor = getClosestBubble(newBubble, true)
|
338 |
+
if (neighbor && neighbor !== bubble && !neighbors.includes(neighbor)) {
|
339 |
+
neighbors.push(neighbor)
|
340 |
+
}
|
341 |
+
}
|
342 |
+
|
343 |
+
return neighbors
|
344 |
+
}
|
345 |
+
|
346 |
+
// remove bubbles that create a match of 3 colors
|
347 |
+
function removeMatch(targetBubble) {
|
348 |
+
const matches = [targetBubble]
|
349 |
+
|
350 |
+
bubbles.forEach((bubble) => (bubble.processed = false))
|
351 |
+
targetBubble.processed = true
|
352 |
+
|
353 |
+
// loop over the neighbors of matching colors for more matches
|
354 |
+
let neighbors = getNeighbors(targetBubble)
|
355 |
+
for (let i = 0; i < neighbors.length; i++) {
|
356 |
+
let neighbor = neighbors[i]
|
357 |
+
|
358 |
+
if (!neighbor.processed) {
|
359 |
+
neighbor.processed = true
|
360 |
+
|
361 |
+
if (neighbor.color === targetBubble.color) {
|
362 |
+
matches.push(neighbor)
|
363 |
+
neighbors = neighbors.concat(getNeighbors(neighbor))
|
364 |
+
}
|
365 |
+
}
|
366 |
+
}
|
367 |
+
|
368 |
+
// MARK: MATCHES
|
369 |
+
if (matches.length >= 3) {
|
370 |
+
console.log("Matches found: " + matches.length)
|
371 |
+
const scoreMultiplier = matches.length * 100 + matches.length * 10
|
372 |
+
gameState.score += scoreMultiplier
|
373 |
+
console.log(gameState.score)
|
374 |
+
matches.forEach((bubble) => {
|
375 |
+
bubble.active = false
|
376 |
+
})
|
377 |
+
|
378 |
+
updateUI()
|
379 |
+
}
|
380 |
+
}
|
381 |
+
|
382 |
+
// make any floating bubbles (bubbles that don't have a bubble chain
|
383 |
+
// that touch the ceiling) drop down the screen
|
384 |
+
function dropFloatingBubbles() {
|
385 |
+
const activeBubbles = bubbles.filter((bubble) => bubble.active)
|
386 |
+
activeBubbles.forEach((bubble) => (bubble.processed = false))
|
387 |
+
|
388 |
+
// start at the bubbles that touch the ceiling
|
389 |
+
let neighbors = activeBubbles.filter((bubble) => bubble.y - grid <= wallSize)
|
390 |
+
|
391 |
+
// process all bubbles that form a chain with the ceiling bubbles
|
392 |
+
for (let i = 0; i < neighbors.length; i++) {
|
393 |
+
let neighbor = neighbors[i]
|
394 |
+
|
395 |
+
if (!neighbor.processed) {
|
396 |
+
neighbor.processed = true
|
397 |
+
neighbors = neighbors.concat(getNeighbors(neighbor))
|
398 |
+
}
|
399 |
+
}
|
400 |
+
|
401 |
+
// any bubble that is not processed doesn't touch the ceiling
|
402 |
+
activeBubbles
|
403 |
+
.filter((bubble) => !bubble.processed)
|
404 |
+
.forEach((bubble) => {
|
405 |
+
bubble.active = false
|
406 |
+
// create a particle bubble that falls down the screen
|
407 |
+
particles.push({
|
408 |
+
x: bubble.x,
|
409 |
+
y: bubble.y,
|
410 |
+
color: bubble.color,
|
411 |
+
radius: bubble.radius,
|
412 |
+
active: true,
|
413 |
+
})
|
414 |
+
})
|
415 |
+
}
|
416 |
+
|
417 |
+
// fill the grid with inactive bubbles
|
418 |
+
for (let row = 0; row < 10; row++) {
|
419 |
+
for (let col = 0; col < (row % 2 === 0 ? 10 : 9); col++) {
|
420 |
+
const color = level1[row]?.[col]
|
421 |
+
createBubble(col * grid, row * grid, colorMap[color])
|
422 |
+
}
|
423 |
+
}
|
424 |
+
|
425 |
+
function addNewRow() {
|
426 |
+
console.log("Added new row")
|
427 |
+
console.log(Date.now())
|
428 |
+
// move all bubbles one row down
|
429 |
+
bubbles.forEach((bubble) => (bubble.y += grid))
|
430 |
+
|
431 |
+
const level = addedLevel++
|
432 |
+
const offset = level % 2 === 0 ? grid : 0
|
433 |
+
isOddRow = level % 2 === 0
|
434 |
+
console.log("Is odd row 1: " + isOddRow)
|
435 |
+
// create a new row at the top
|
436 |
+
for (let col = 0; col < (level % 2 === 0 ? 9 : 10); col++) {
|
437 |
+
const color = colors[Math.floor(Math.random() * colors.length)]
|
438 |
+
createBubble(col * grid, 0, color, isOddRow)
|
439 |
+
}
|
440 |
+
}
|
441 |
+
|
442 |
+
const curBubblePos = {
|
443 |
+
// place the current bubble horizontally in the middle of the screen
|
444 |
+
x: canvas.width / 2,
|
445 |
+
y: canvas.height - 40,
|
446 |
+
}
|
447 |
+
const curBubble = {
|
448 |
+
x: curBubblePos.x,
|
449 |
+
y: curBubblePos.y,
|
450 |
+
color: colors[getRandomInt(0, colors.length - 1)],
|
451 |
+
radius: grid / 2, // a circles radius is half the width (diameter)
|
452 |
+
|
453 |
+
// how fast the bubble should go in either the x or y direction
|
454 |
+
speed: 16,
|
455 |
+
|
456 |
+
// bubble velocity
|
457 |
+
dx: 0,
|
458 |
+
dy: 0,
|
459 |
+
}
|
460 |
+
|
461 |
+
// angle (in radians) of the shooting arrow
|
462 |
+
let shootDeg = 0
|
463 |
+
const minDeg = degToRad(-80)
|
464 |
+
const maxDeg = degToRad(80)
|
465 |
+
let shootDir = 0
|
466 |
+
|
467 |
+
const nextBubblePos = {
|
468 |
+
x: canvas.width - 40,
|
469 |
+
y: canvas.height - 40,
|
470 |
+
}
|
471 |
+
|
472 |
+
let nextBubble = {
|
473 |
+
x: nextBubblePos.x,
|
474 |
+
y: nextBubblePos.y,
|
475 |
+
color: colors[getRandomInt(0, colors.length - 1)],
|
476 |
+
radius: grid / 2,
|
477 |
+
}
|
478 |
+
|
479 |
+
// reset the bubble to shoot to the bottom of the screen
|
480 |
+
function getNewBubble() {
|
481 |
+
curBubble.x = curBubblePos.x
|
482 |
+
curBubble.y = curBubblePos.y
|
483 |
+
curBubble.dx = curBubble.dy = 0
|
484 |
+
|
485 |
+
// Use nextBubble's color for current bubble
|
486 |
+
curBubble.color = nextBubble.color
|
487 |
+
|
488 |
+
// Generate a new nextBubble color
|
489 |
+
nextBubble.color = colors[getRandomInt(0, colors.length - 1)]
|
490 |
+
}
|
491 |
+
|
492 |
+
// handle collision between the current bubble and another bubble
|
493 |
+
function handleCollision(bubble) {
|
494 |
+
bubble.color = curBubble.color
|
495 |
+
bubble.active = true
|
496 |
+
getNewBubble()
|
497 |
+
removeMatch(bubble)
|
498 |
+
dropFloatingBubbles()
|
499 |
+
}
|
500 |
+
|
501 |
+
// MARK: GAME LOOP START
|
502 |
+
function loop() {
|
503 |
+
requestAnimationFrame(loop)
|
504 |
+
if (gameState.isPaused || gameState.isGameOver) return
|
505 |
+
|
506 |
+
context.clearRect(0, 0, canvas.width, canvas.height)
|
507 |
+
|
508 |
+
// move the shooting arrow
|
509 |
+
shootDeg = shootDeg + degToRad(2) * shootDir
|
510 |
+
|
511 |
+
// prevent shooting arrow from going below/above min/max
|
512 |
+
if (shootDeg < minDeg) {
|
513 |
+
shootDeg = minDeg
|
514 |
+
} else if (shootDeg > maxDeg) {
|
515 |
+
shootDeg = maxDeg
|
516 |
+
}
|
517 |
+
|
518 |
+
// move current bubble by it's velocity
|
519 |
+
curBubble.x += curBubble.dx
|
520 |
+
curBubble.y += curBubble.dy
|
521 |
+
|
522 |
+
// prevent bubble from going through walls by changing its velocity
|
523 |
+
if (curBubble.x - grid / 2 < wallSize) {
|
524 |
+
curBubble.x = wallSize + grid / 2
|
525 |
+
curBubble.dx *= -1
|
526 |
+
} else if (curBubble.x + grid / 2 > canvas.width - wallSize) {
|
527 |
+
curBubble.x = canvas.width - wallSize - grid / 2
|
528 |
+
curBubble.dx *= -1
|
529 |
+
}
|
530 |
+
|
531 |
+
// check to see if bubble collides with the top wall
|
532 |
+
if (curBubble.y - grid / 2 < wallSize) {
|
533 |
+
// make the closest inactive bubble active
|
534 |
+
const closestBubble = getClosestBubble(curBubble)
|
535 |
+
handleCollision(closestBubble)
|
536 |
+
}
|
537 |
+
|
538 |
+
// check to see if bubble collides with another bubble
|
539 |
+
for (let i = 0; i < bubbles.length; i++) {
|
540 |
+
const bubble = bubbles[i]
|
541 |
+
|
542 |
+
if (bubble.active && collides(curBubble, bubble)) {
|
543 |
+
const closestBubble = getClosestBubble(curBubble)
|
544 |
+
// MARK: GAME-OVER
|
545 |
+
if (!closestBubble) {
|
546 |
+
//window.alert("Game Over")
|
547 |
+
//window.location.reload()
|
548 |
+
gameState.isGameOver = true
|
549 |
+
clearInterval(addRowInterval)
|
550 |
+
const highScore = localStorage.getItem("highScore") || 0
|
551 |
+
if (gameState.score > highScore) {
|
552 |
+
localStorage.setItem("highScore", gameState.score)
|
553 |
+
}
|
554 |
+
document.querySelector("div.game-over").style.display = "block"
|
555 |
+
return
|
556 |
+
}
|
557 |
+
|
558 |
+
if (closestBubble) {
|
559 |
+
handleCollision(closestBubble)
|
560 |
+
}
|
561 |
+
}
|
562 |
+
}
|
563 |
+
|
564 |
+
// move bubble particles
|
565 |
+
particles.forEach((particle) => {
|
566 |
+
particle.y += 8
|
567 |
+
})
|
568 |
+
|
569 |
+
// remove particles that went off the screen
|
570 |
+
particles = particles.filter((particles) => particles.y < canvas.height - grid / 2)
|
571 |
+
|
572 |
+
// draw walls
|
573 |
+
context.fillStyle = "lightgrey"
|
574 |
+
context.fillRect(0, 0, canvas.width, wallSize)
|
575 |
+
context.fillRect(0, 0, wallSize, canvas.height)
|
576 |
+
context.fillRect(canvas.width - wallSize, 0, wallSize, canvas.height)
|
577 |
+
context.beginPath()
|
578 |
+
bottomBorderImage = new Image()
|
579 |
+
bottomBorderImage.src =
|
580 |
+
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAZAAAABGCAYAAADir8JKAAAACXBIWXMAAAsSAAALEgHS3X78AAAEGElEQVR4nO3dzVHbQBjG8Yc0ALnyHuwOcCqIU0HoICrBHYR0QAe4BKcCnAridIAPew6ugBy0zGSYsZFWH+t39f/N+Cjp2QsPq5VWFy8vLwKmyMwWkpaSFpLmkj6/c8gvSU+SdpK2IYTdgPGAs3dBgWBKzOxW0uvvsuPpDpI2kjYhhE3XbIA3FAiKZ2ZXklaSKkmzgS6zl7SWdB9CeB7oGsBZoUBQNDO7U10eXWcbTR1Ul8jdSNcDsqFAUCQzW6qeEQw143jPXlIVQthmuj4wuA+5AwB9M7N7SY/KVx6K136MWYAiMQNBMcxsrnpR+yZzlLf+SLoNITzlDgL0iQJBEeIjuVuNt9bR1kHSkkd/URJuYcE9B+Uh1dm2MStQBAoErjkpj1eUCIrCLSy4Fdc8dvJRHv87SFqwJgLvmIHApfhy4Eb+ykOqM/PmOtyjQODVnc7vaas2bnjEF95xCwvuxJcEH3Pn6MkXXjaEV8xA4NE6d4AerXMHAFJRIHAl7m2V8w3zvs3imAB3uIUFN+LC+ZN8LpyfcpA0ZxdfeMMMBJ5UKq88pHpMVe4QQFsUCDxZ5Q4woJLHhkJRIHAhfkmwpLWPt2ZxjIAbFAi8mMIf1ymMEQWhQODFFP64TmGMKAgFgrMXNx8scfH8rUs2WoQnFAg8WOYOMKJl7gBAUxQIPJjSf+VTGiuco0DgwTx3gBHNcwcAmqJA4MHn3AFGNKWxwjkKBACQ5OL6+prNsADguMlsud/2UwnMQAAASSgQAEASCgQAkIQCAQAkoUAA4LSr3AFG1GqsFAgAnDal3QFajZUCAYDTKJAjKBAAOI0COYICAYDTZmY2zx1iaHGMrb76SYEAwPuWuQOMYNn2AAoEAN43ha9Fth4je2EBQDMfQwjPuUMMwcyuJP1texwzEABopsodYEBVykEUCAA0s8odYEBJY6NAAKCZmZlVuUP0LY6p1dNXr1gDAYDm9pIWpayFxLWPnRILhBkIADQ3U1m3slZKLA+JGQgApPgUQtjlDtGFmS0k/e5yDmYgANDeOneAHqy7noACAYD2bsxsnTtEqpj9put5KBAASPPN41NZMfO3Ps5FgQBAugdPJRKzPvR1PgoEALpxUSJ9l4dEgQBAH866RIYoD4kCAYC+PJzjwnrM1Ht5SLwHAgB9+yOpyv2eSHzPY60enrY6hgIBgGH8kHQ/9rYncXuSlaTvQ1+LW1gAMIzvknZjro3Ea+00QnlIzEAAYAx7SfeS1n3PSOKMo1LHfa1SUCAAMK6fkjaStiGEp5QTmNlc9TfMbyV97StYWxQIAOSzV33L6fV3bHZyJWnx32/UmcYxFAgAIAmL6ACAJBQIACAJBQIASEKBAACSUCAAgCQUCAAgCQUCAEhCgQAAklAgAIAk/wDMhdGTLpCVPwAAAABJRU5ErkJggg=="
|
581 |
+
context.drawImage(bottomBorderImage, 0, canvas.height - 70)
|
582 |
+
context.closePath()
|
583 |
+
|
584 |
+
// Draw next bubble
|
585 |
+
context.fillStyle = nextBubble.color
|
586 |
+
context.beginPath()
|
587 |
+
context.arc(nextBubble.x, nextBubble.y, nextBubble.radius, 0, 2 * Math.PI)
|
588 |
+
context.fill()
|
589 |
+
|
590 |
+
// draw bubbles and particles
|
591 |
+
bubbles.concat(particles).forEach((bubble) => {
|
592 |
+
if (!bubble.active) return
|
593 |
+
context.fillStyle = bubble.color
|
594 |
+
|
595 |
+
// draw a circle
|
596 |
+
context.beginPath()
|
597 |
+
context.arc(bubble.x, bubble.y, bubble.radius, 0, 2 * Math.PI)
|
598 |
+
context.fill()
|
599 |
+
})
|
600 |
+
|
601 |
+
// draw fire arrow. since we're rotating the canvas we need to save
|
602 |
+
// the state and restore it when we're done
|
603 |
+
context.save()
|
604 |
+
context.translate(curBubblePos.x, curBubblePos.y)
|
605 |
+
context.rotate(shootDeg)
|
606 |
+
context.translate(0, -10)
|
607 |
+
|
608 |
+
// draw arrow ↑
|
609 |
+
shooterImage = new Image()
|
610 |
+
shooterImage.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAB2CAYAAAD1J9I6AAAACXBIWXMAAAsSAAALEgHS3X78AAAAjElEQVRIie3QsQ3DIBRF0QvKFFB4JbMJo2STMFIo/hzfhRMJWQlBSvtuywOhE9ydnPMO3IGNsw5UM2shpbQDDz5XQkrpOdy81uPkEGCLk0MA4utD3+oRqJNBjWbWgHJ5qQPFzFpw959/+G9wA0Qt6neiBkS9PhC1qMdEDYh6fSBqUY+JGhD1+kDUJ/UByN4RqOiQZ/8AAAAASUVORK5CYII="
|
611 |
+
context.drawImage(shooterImage, 0, -90)
|
612 |
+
|
613 |
+
context.restore()
|
614 |
+
|
615 |
+
// draw current bubble
|
616 |
+
context.fillStyle = curBubble.color
|
617 |
+
context.beginPath()
|
618 |
+
context.arc(curBubble.x, curBubble.y, curBubble.radius, 0, 2 * Math.PI)
|
619 |
+
context.fill()
|
620 |
+
}
|
621 |
+
// MARK: GAME LOOP ENDE
|
622 |
+
|
623 |
+
// MARK: EVENT HANDLER START
|
624 |
+
|
625 |
+
// Mousemove event for aiming
|
626 |
+
document.addEventListener("mousemove", (e) => {
|
627 |
+
const rect = canvas.getBoundingClientRect()
|
628 |
+
const centerX = rect.left + canvas.width / 2
|
629 |
+
const centerY = rect.top + canvas.height
|
630 |
+
const mouseX = e.clientX - centerX
|
631 |
+
const mouseY = e.clientY - centerY
|
632 |
+
shootDeg = Math.atan2(mouseX, -mouseY) // Calculate angle relative to center
|
633 |
+
|
634 |
+
// Constrain angle within -80 to 80 degrees
|
635 |
+
const maxAngleRad = degToRad(180)
|
636 |
+
const minAngleRad = degToRad(-180)
|
637 |
+
shootDeg = Math.max(minAngleRad, Math.min(maxAngleRad, shootDeg))
|
638 |
+
})
|
639 |
+
|
640 |
+
// Click event for shooting
|
641 |
+
canvas.addEventListener("click", (e) => {
|
642 |
+
if (curBubble.dx === 0 && curBubble.dy === 0 && !gameState.isPaused && !gameState.isGameOver) {
|
643 |
+
// Only shoot if bubble isn't moving & game is active
|
644 |
+
curBubble.dx = Math.sin(shootDeg) * curBubble.speed
|
645 |
+
curBubble.dy = -Math.cos(shootDeg) * curBubble.speed
|
646 |
+
}
|
647 |
+
})
|
648 |
+
|
649 |
+
function pauseGame() {
|
650 |
+
gameState.isPaused = !gameState.isPaused
|
651 |
+
document.querySelector(".pause-screen").style.display = gameState.isPaused ? "block" : "none"
|
652 |
+
canvas.style.cursor = gameState.isPaused ? "initial" : "none"
|
653 |
+
|
654 |
+
// Clear velocity when paused to prevent movement
|
655 |
+
if (gameState.isPaused) {
|
656 |
+
curBubble.dx = 0
|
657 |
+
curBubble.dy = 0
|
658 |
+
}
|
659 |
+
}
|
660 |
+
|
661 |
+
document.addEventListener("keydown", (e) => {
|
662 |
+
switch (e.key) {
|
663 |
+
case "ArrowLeft":
|
664 |
+
if (gameState.isPaused) break
|
665 |
+
if (gameState.isPlaying) gameState.shooterAngle = Math.max(gameState.shooterAngle - 0.1, -Math.PI / 3)
|
666 |
+
break
|
667 |
+
case "ArrowRight":
|
668 |
+
if (gameState.isPaused) break
|
669 |
+
if (gameState.isPlaying) gameState.shooterAngle = Math.min(gameState.shooterAngle + 0.1, Math.PI / 3)
|
670 |
+
break
|
671 |
+
case "ArrowUp":
|
672 |
+
if (gameState.isPaused) break
|
673 |
+
if (gameState.isPlaying && curBubble.dx === 0 && curBubble.dy === 0) {
|
674 |
+
curBubble.dx = Math.sin(shootDeg) * curBubble.speed
|
675 |
+
curBubble.dy = -Math.cos(shootDeg) * curBubble.speed
|
676 |
+
}
|
677 |
+
break
|
678 |
+
case "p":
|
679 |
+
case "Escape":
|
680 |
+
pauseGame()
|
681 |
+
break
|
682 |
+
}
|
683 |
+
})
|
684 |
+
|
685 |
+
document.addEventListener("keyup", (e) => {
|
686 |
+
if ((e.code === "ArrowLeft" && shootDir === -1) || (e.code === "ArrowRight" && shootDir === 1)) {
|
687 |
+
shootDir = 0
|
688 |
+
}
|
689 |
+
})
|
690 |
+
|
691 |
+
startButton.addEventListener("click", () => {
|
692 |
+
document.querySelector("div.start-screen").style.display = "none"
|
693 |
+
canvas.style.cursor = "none"
|
694 |
+
initGame()
|
695 |
+
requestAnimationFrame(loop)
|
696 |
+
})
|
697 |
+
|
698 |
+
document.getElementById("pauseButton").addEventListener("click", () => {
|
699 |
+
if (!gameState.isPlaying || gameState.isPaused) return
|
700 |
+
pauseGame()
|
701 |
+
})
|
702 |
+
|
703 |
+
resumeButton.addEventListener("click", () => {
|
704 |
+
pauseGame()
|
705 |
+
})
|
706 |
+
|
707 |
+
restartButton.addEventListener("click", () => {
|
708 |
+
window.location.reload()
|
709 |
+
})
|
710 |
+
|
711 |
+
// MARK: EVENT HANDLER ENDE
|
712 |
+
|
713 |
+
startScreen.style.display = "block"
|
714 |
+
</script>
|
715 |
</body>
|
716 |
</html>
|