Global Game Jam 2025, Day 3
It's 10:00 and I have already created a few images for the game. The avatar for the tutorial is a little Shrimp:
Time to see Shrimpy in action:
1 #include "bpop_main.h"
2 #include <string.h>
3 #include <stdlib.h>
4 #include <stdio.h>
5
6 GameScene currentScene = GAME_SCENE_MENU;
7 GameScene nextScene = GAME_SCENE_NONE;
8
9 GameDifficulty gameDifficulty = GAME_DIFFICULTY_NORMAL;
10
11 Storage storage = {
12 .version = 1,
13 };
14
15 Texture2D texShrimpy;
16 Texture2D texUiAtlas;
17 Texture2D texGround;
18 Font fontMainMedium;
19 Font fontMainLarge;
20
21 NPatchInfo nPatchButton = {
22 .source = {0,0,240,145},
23 .left = 10,
24 .top = 10,
25 .right = 10,
26 .bottom = 10,
27 .layout = NPATCH_NINE_PATCH
28 };
29 Rectangle rectUiAtlasCircle = {
30 .x = 250,
31 .y = 0,
32 .width = 148,
33 .height = 148,
34 };
35 Rectangle rectUiAtlasShrimpy = {
36 .x = 420,
37 .y = 390,
38 .width = 512-420,
39 .height = 512-390,
40 };
41
42 int outlineSizeLoc = 0;
43 int outlineColorLoc = 0;
44 int textureSizeLoc = 0;
45
46 Camera3D camera = {
47 .position = { 0.0f, 0.0f, -10.0f },
48 .target = { 0.0f, 0.0f, 0.0f },
49 .up = { 0.0f, 1.0f, 0.0f },
50 .projection = CAMERA_ORTHOGRAPHIC,
51 .fovy = 320.0f,
52 };
53
54
55 Playfield playfield = {
56 .fieldSize = {250, 300},
57 .waterLevel = 200.0f,
58 .minHeight = 90.0f,
59 .maxHeight = 280.0f,
60 };
61
62 GameTime gameTime = {
63 .fixedDeltaTime = 1.0f / 60.0f,
64 };
65
66 Color bubbleTypeColors[] = {
67 COLOR_FROM_HEX(0xaaccff),
68 COLOR_FROM_HEX(0x2277ff),
69 COLOR_FROM_HEX(0x33ffaa),
70 COLOR_FROM_HEX(0x9933aa),
71 COLOR_FROM_HEX(0xaa33ff),
72 };
73
74 Model bubbleModel;
75 Shader bubbleOutlineShader;
76 RenderTexture2D bubbleFieldTexture;
77
78 int isClickActionBlocked = 0;
79
80 void SaveStorage();
81
82 void DrawText2(const char *text, int x, int y, Color color)
83 {
84 DrawTextEx(fontMainMedium, text, (Vector2){x, y}, fontMainMedium.baseSize, 1, color);
85 }
86
87 void DrawText3(const char *text, int x, int y, Color color)
88 {
89 DrawTextEx(fontMainLarge, text, (Vector2){x, y}, fontMainMedium.baseSize, 1, color);
90 }
91
92 float MeasureText2(const char *text)
93 {
94 return MeasureTextEx(fontMainMedium, text, fontMainMedium.baseSize, 1).x;
95 }
96
97 float BubbleLevelRadius(int level)
98 {
99 return powf((level + 1) * 120, .5f);
100 }
101
102 int IsClickActioned()
103 {
104 int result = 0;
105 if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON) && !isClickActionBlocked)
106 {
107 result = 1;
108 }
109 return result;
110 }
111
112 int Button(const char *text, Vector2 position, Vector2 size)
113 {
114 Rectangle rect = {position.x, position.y, size.x, size.y};
115 int isHovered = !isClickActionBlocked && CheckCollisionPointRec(GetMousePosition(), rect);
116 int result = isHovered && IsClickActioned();
117
118 Color color = isHovered ? COLOR_FROM_HEX(0xcceeff) : COLOR_FROM_HEX(0xaaccff);
119 if (!isClickActionBlocked && isHovered && IsMouseButtonDown(MOUSE_LEFT_BUTTON))
120 {
121 color = COLOR_FROM_HEX(0xddf8ff);
122 }
123
124 DrawTextureNPatch(texUiAtlas, nPatchButton, rect, Vector2Zero(), 0.0f, color);
125
126 int fontSize = fontMainMedium.baseSize;
127 Vector2 tsize = MeasureTextEx(fontMainMedium, text, fontSize, 1);
128 DrawTextEx(fontMainMedium, text, (Vector2){position.x + size.x * 0.5f - tsize.x * 0.5f, position.y + size.y * 0.5f - tsize.y * 0.5f}, fontSize, 1, BLACK);
129 return result;
130 }
131
132 void PlayfieldFixedUpdate(Playfield *playfield)
133 {
134 for (int i = 0; i < MAX_BUBBLES; i++)
135 {
136 Bubble *bubble = &playfield->bubbles[i];
137 bubble->sameLevelContact = 0;
138 }
139
140 for (int i = 0; i < MAX_BUBBLES; i++)
141 {
142 Bubble *bubble = &playfield->bubbles[i];
143 if (!bubble->flagIsActive) continue;
144 float r = bubble->radius;
145
146 for (int j = i + 1; j < MAX_BUBBLES; j++)
147 {
148 Bubble *other = &playfield->bubbles[j];
149 if (!other->flagIsActive) continue;
150 float otherR = other->radius;
151 float sumR2 = (r + otherR) * (r + otherR);
152 float d2 = Vector2DistanceSqr(bubble->position, other->position);
153 int canMerge = bubble->bubbleLevel == other->bubbleLevel && bubble->bubbleType == other->bubbleType;
154 if (d2 < sumR2 * 1.05f)
155 {
156 if (canMerge)
157 {
158 bubble->sameLevelContact = 1;
159 other->sameLevelContact = 1;
160 }
161 if (canMerge && bubble->bubbleMergeCooldown <= 0.0f
162 && other->bubbleMergeCooldown <= 0.0f)
163 {
164 // merge bubbles
165 bubble->bubbleLevel++;
166 bubble->radius = BubbleLevelRadius(bubble->bubbleLevel);
167 bubble->bubbleMergeCooldown = 1.0f;
168 other->flagIsActive = 0;
169 }
170 }
171
172 if (d2 < sumR2)
173 {
174 float overlap = r + otherR - sqrtf(d2);
175 Vector2 normal = Vector2Normalize(Vector2Subtract(other->position, bubble->position));
176 // resolve overlap by moving the bubbles apart
177 const float errorCorrection = 0.25f;
178 bubble->position = Vector2Subtract(bubble->position, Vector2Scale(normal, overlap * errorCorrection));
179 other->position = Vector2Add(other->position, Vector2Scale(normal, overlap * errorCorrection));
180
181 // bounce off each other
182 Vector2 relativeVelocity = Vector2Subtract(bubble->velocity, other->velocity);
183 float dot = Vector2DotProduct(relativeVelocity, normal);
184 if (dot > 0.0f)
185 {
186 // DrawLineV(bubble->position, other->position, COLOR_FROM_HEX(0xff0000));
187 float impulse = -dot * 0.85f;
188 bubble->velocity = Vector2Add(bubble->velocity, Vector2Scale(normal, impulse));
189 other->velocity = Vector2Subtract(other->velocity, Vector2Scale(normal, impulse));
190 }
191 }
192 }
193
194 if (!bubble->sameLevelContact)
195 {
196 bubble->bubbleMergeCooldown = 1.0f;
197 }
198 else
199 {
200 bubble->bubbleMergeCooldown -= gameTime.fixedDeltaTime;
201 }
202
203 float buoyancy = -20.0f;
204 if (bubble->position.y < playfield->waterLevel)
205 {
206 buoyancy = (playfield->waterLevel - bubble->position.y) * 0.5f;
207 }
208
209 int isOutsideZone = bubble->position.y < playfield->minHeight + bubble->radius ||
210 bubble->position.y > playfield->maxHeight - bubble->radius;
211 bubble->lifeTime += gameTime.fixedDeltaTime;
212 bubble->isOutsideZone = isOutsideZone && bubble->lifeTime > 1.0f;
213
214 float horizontalV = 0.0f;
215 if (playfield->enableTutorialMerge) {
216 horizontalV = fminf(fmaxf(-10.0f, (playfield->fieldSize.x / 2 - bubble->position.x) * 0.1f), 10.0f);
217 }
218 bubble->velocity = Vector2Add(bubble->velocity, (Vector2){horizontalV, buoyancy});
219 bubble->velocity = Vector2Scale(bubble->velocity, 0.92f);
220 bubble->position.x += bubble->velocity.x * gameTime.fixedDeltaTime;
221 bubble->position.y += bubble->velocity.y * gameTime.fixedDeltaTime;
222 if ((bubble->position.x < r && bubble->velocity.x < 0.0f) ||
223 (bubble->position.x > playfield->fieldSize.x - r && bubble->velocity.x > 0.0f))
224 {
225 bubble->velocity.x *= -0.9f;
226 }
227
228 bubble->position.x = (bubble->position.x < r) ? r : (bubble->position.x > playfield->fieldSize.x - r) ? playfield->fieldSize.x - r : bubble->position.x;
229 // bubble->position.y = (bubble->position.y < r) ? r : (bubble->position.y > playfield->fieldSize.y - r) ? playfield->fieldSize.y - r : bubble->position.y;
230
231 // debug velocity
232 // DrawLineV(bubble->position, Vector2Add(bubble->position, Vector2Scale(bubble->velocity, 1.0f)), COLOR_FROM_HEX(0xff0000));
233 }
234
235 int outsideCount = 0;
236 for (int i = 0; i < MAX_BUBBLES; i++)
237 {
238 Bubble *bubble = &playfield->bubbles[i];
239 if (bubble->isOutsideZone)
240 {
241 outsideCount++;
242 }
243 }
244
245 if (outsideCount == 0)
246 {
247 playfield->strikes = 0;
248 playfield->gameoverCooldown = 0.0f;
249 }
250 if (playfield->strikes >= MAX_STRIKES)
251 {
252 playfield->gameoverCooldown += gameTime.fixedDeltaTime;
253 if (playfield->gameoverCooldown > GAMEOVER_COOLDOWN)
254 {
255 nextScene = GAME_SCENE_GAMEOVER;
256 }
257 }
258 }
259
260 void PlayfieldTryAddBubble(Playfield *playfield, Vector2 position)
261 {
262 if (playfield->strikes >= MAX_STRIKES) return;
263
264 for (int i = 0; i < MAX_BUBBLES; i++)
265 {
266 Bubble *bubble = &playfield->bubbles[i];
267 if (!bubble->flagIsActive)
268 {
269 bubble->flagIsActive = 1;
270 bubble->position = position;
271 bubble->velocity = (Vector2){GetRandomValue(-100, 100), GetRandomValue(-100, 100)};
272 bubble->bubbleType = playfield->nextBubbleType;
273 bubble->bubbleLevel = playfield->nextBubbleLevel;
274 bubble->radius = BubbleLevelRadius(bubble->bubbleLevel);
275 bubble->lifeTime = 0.0f;
276 playfield->spawnedBubbleCount++;
277
278 for (int j = 0; j < MAX_BUBBLES; j += 1)
279 {
280 Bubble *other = &playfield->bubbles[j];
281 if (!other->flagIsActive) continue;
282 if (other->isOutsideZone)
283 {
284 playfield->strikes++;
285 break;
286 }
287 }
288
289 playfield->nextBubbleType = GetRandomValue(0, gameDifficulty > 0 ? gameDifficulty : 1);
290 playfield->nextBubbleLevel = GetRandomValue(0, 3);
291 break;
292 }
293 }
294
295 }
296
297 PlayfieldScores CalculatePlayfieldScores(Playfield *playfield)
298 {
299 PlayfieldScores scores = {0};
300 for (int i = 0; i < MAX_BUBBLES; i++)
301 {
302 Bubble *bubble = &playfield->bubbles[i];
303 if (bubble->flagIsActive)
304 {
305 scores.bubbleCount++;
306 uint32_t bubbleScore = 1 << bubble->bubbleLevel;
307 scores.score += bubbleScore;
308
309 if (bubble->isOutsideZone)
310 {
311 scores.outsideBubbleCount++;
312 }
313 }
314 }
315 scores.score += playfield->spawnedBubbleCount;
316 return scores;
317 }
318
319 Vector2 PlayfieldPositionToSpawnPosition(Playfield *playfield, Vector2 position)
320 {
321 Vector2 spawnPosition = position;
322 spawnPosition.y = BubbleLevelRadius(5);
323 return spawnPosition;
324 }
325
326 Vector2 PlayfieldScreenToSpawnPosition(Playfield *playfield, Camera3D camera, Vector2 screenPosition)
327 {
328 Vector3 cursorPosition = GetScreenToWorldRay(screenPosition, camera).position;
329 cursorPosition.x += playfield->fieldSize.x / 2;
330 cursorPosition.y += playfield->fieldSize.y / 2;
331
332 Vector2 pos = {cursorPosition.x, cursorPosition.y};
333 return PlayfieldPositionToSpawnPosition(playfield, pos);
334 }
335
336 void DrawBubble(Vector3 position, int level, Color color)
337 {
338 float bubbleExtraRadius = 5.0f;
339 float r = BubbleLevelRadius(level) + bubbleExtraRadius;
340 DrawModel(bubbleModel, position, r, color);
341 if (level < 1) return;
342 position.z -= r;
343 float tinyR = level < 6 ? 2 : 4;
344 int count = level < 6 ? level : level - 5;
345 for (int i = 0; i < count; i++)
346 {
347 float ang = (i * 25.0f + 30.0f) * DEG2RAD;
348 float offsetR = i % 2 == 0 ? 0.4f : 0.7f;
349 Vector3 offset = {cosf(ang) * offsetR * r, sinf(ang) * offsetR * r, 0};
350 DrawModel(bubbleModel, Vector3Add(position, offset), tinyR, WHITE);
351 }
352 }
353
354 void PlayfieldDrawBubbles(Playfield *playfield, Camera3D camera)
355 {
356 DrawCube((Vector3){0, 0, 0}, playfield->fieldSize.x, playfield->fieldSize.y, 0, COLOR_FROM_HEX(0xbbddff));
357 DrawCube((Vector3){0, (playfield->waterLevel - playfield->fieldSize.y) * 0.5f, 0}, playfield->fieldSize.x,
358 playfield->waterLevel, 0, COLOR_FROM_HEX(0x225588));
359
360 // cursor bubble
361 if (currentScene == GAME_SCENE_PLAY)
362 {
363 Vector2 mousePos = GetMousePosition();
364 Vector2 spawnPosition = PlayfieldScreenToSpawnPosition(playfield, camera, mousePos);
365 Vector3 drawPos = (Vector3){spawnPosition.x - playfield->fieldSize.x * 0.5f, spawnPosition.y - playfield->fieldSize.y * 0.5f, 0};
366 if (playfield->strikes < MAX_STRIKES && drawPos.x >= -playfield->fieldSize.x * 0.5f && drawPos.x <= playfield->fieldSize.x * 0.5f)
367 {
368 DrawBubble(drawPos, playfield->nextBubbleLevel, bubbleTypeColors[playfield->nextBubbleType]);
369 }
370 }
371
372 // DrawRectangle(0, 0, playfield->fieldSize.x, playfield->fieldSize.y, COLOR_FROM_HEX(0x225588));
373 rlPushMatrix();
374 rlTranslatef(-playfield->fieldSize.x / 2, -playfield->fieldSize.y / 2, 0);
375 // draw bubbles into playfield space
376 float blink = sinf(gameTime.time * 10.0f) * 0.2f + 0.5f;
377 for (int i = 0; i < MAX_BUBBLES; i++)
378 {
379 Bubble *bubble = &playfield->bubbles[i];
380 if (!bubble->flagIsActive) continue;
381 Vector3 position = {bubble->position.x, bubble->position.y, 0};
382 Color bubbleColor = bubbleTypeColors[bubble->bubbleType];
383 int isOutsideZone = bubble->isOutsideZone;
384
385 if (isOutsideZone)
386 {
387 bubbleColor = ColorLerp(bubbleColor, COLOR_FROM_HEX(0xff4433), blink);
388 }
389 // lazy: encode id into rgb values
390 bubbleColor.r = bubbleColor.r - i * 2 % 4;
391 bubbleColor.g = bubbleColor.g - (i * 2 / 4) % 4;
392 bubbleColor.b = bubbleColor.b - (i * 2 / 16);
393 bubbleColor.a = 255;
394 DrawBubble(position, bubble->bubbleLevel, bubbleColor);
395 }
396 rlPopMatrix();
397 }
398
399 void PlayfieldDrawRange(Playfield *playfield, Camera3D camera)
400 {
401 Color rangeLimitColor = COLOR_FROM_HEX(0xff4400);
402 int divides = 10;
403 float divWidth = playfield->fieldSize.x / divides;
404 for (int i = 0; i < divides; i+=2)
405 {
406 float x = i * divWidth - playfield->fieldSize.x * 0.5f + divWidth * 1.0f;
407 DrawCube((Vector3){x, playfield->minHeight - playfield->fieldSize.y * 0.5f, 0}, divWidth, 5, 5, rangeLimitColor);
408 DrawCube((Vector3){x, playfield->maxHeight - playfield->fieldSize.y * 0.5f, 0}, divWidth, 5, 5, rangeLimitColor);
409 }
410 }
411
412 void PlayfieldFullDraw(Playfield *playfield, Camera3D camera)
413 {
414 ClearBackground(COLOR_FROM_HEX(0x4488cc));
415
416 BeginTextureMode(bubbleFieldTexture);
417 rlSetClipPlanes(-128.0f, 128.0f);
418 BeginMode3D(camera);
419
420 ClearBackground(BLANK);
421 PlayfieldDrawBubbles(playfield, camera);
422 EndMode3D();
423 EndTextureMode();
424
425 float outlineSize = 1.0f;
426 float outlineColor[4] = { 1.0f, 1.0f, 1.0f, 1.0f };
427 float textureSize[2] = { (float)bubbleFieldTexture.texture.width, (float)bubbleFieldTexture.texture.height };
428
429 SetShaderValue(bubbleOutlineShader, outlineSizeLoc, &outlineSize, SHADER_UNIFORM_FLOAT);
430 SetShaderValue(bubbleOutlineShader, outlineColorLoc, outlineColor, SHADER_UNIFORM_VEC4);
431 SetShaderValue(bubbleOutlineShader, textureSizeLoc, textureSize, SHADER_UNIFORM_VEC2);
432
433 rlDisableDepthMask();
434 BeginShaderMode(bubbleOutlineShader);
435 DrawTexturePro(bubbleFieldTexture.texture, (Rectangle){0, 0, (float)bubbleFieldTexture.texture.width,
436 -(float)bubbleFieldTexture.texture.height}, (Rectangle){0, 0, (float)GetScreenWidth(), (float)GetScreenHeight()},
437 (Vector2){0, 0}, 0.0f, WHITE);
438 EndShaderMode();
439 rlEnableDepthMask();
440
441 BeginMode3D(camera);
442 PlayfieldDrawRange(playfield, camera);
443 EndMode3D();
444
445 const char *difficultyText = "Tutorial";
446 switch (gameDifficulty)
447 {
448 case GAME_DIFFICULTY_EASY: difficultyText = "Easy"; break;
449 case GAME_DIFFICULTY_NORMAL: difficultyText = "Normal"; break;
450 case GAME_DIFFICULTY_HARD: difficultyText = "Hard"; break;
451 default:
452 break;
453 }
454 const char *modeText = TextFormat("Mode: %s", difficultyText);
455 int screenWidth = GetScreenWidth();
456 int x = screenWidth - 220;
457 DrawText2("Highscores", x, 25, WHITE);
458 DrawText2(modeText, x, 55, WHITE);
459 DifficultyScores table = storage.scores[gameDifficulty];
460 for (int i = 0; i < 8; i++)
461 {
462 HighscoreEntry entry = table.highscores[i];
463 if (entry.score == 0) break;
464 char buffer[64];
465 sprintf(buffer, "%d:", i + 1);
466 int y = 110 + i * 30;
467 DrawText2(buffer, x + 18 - MeasureText2(buffer), y, WHITE);
468 sprintf(buffer, "%d", entry.score);
469 DrawText2(buffer, x + 50 - MeasureText2(buffer) / 2, y, WHITE);
470 sprintf(buffer, "%s", entry.date);
471 DrawText2(buffer, screenWidth - 10 - MeasureText2(buffer), y, WHITE);
472 }
473 DrawTexture(texShrimpy, GetScreenWidth() - 200, GetScreenHeight() - 260, WHITE);
474 }
475
476 void UpdateSceneGameOver()
477 {
478 if (IsKeyPressed(KEY_ENTER))
479 {
480 nextScene = GAME_SCENE_MENU;
481 }
482 // Draw
483 //----------------------------------------------------------------------------------
484
485 ClearBackground(COLOR_FROM_HEX(0x4488cc));
486
487 PlayfieldFullDraw(&playfield, camera);
488
489 DrawText3("Game Over", 20, 20, WHITE);
490
491 PlayfieldScores scores = CalculatePlayfieldScores(&playfield);
492 DrawText2(TextFormat("Final Score: %d", scores.score), 20, 90, WHITE);
493
494 if (Button("< Menu", (Vector2){20, GetScreenHeight() - 110}, (Vector2){180, 90}))
495 {
496 nextScene = GAME_SCENE_MENU;
497 }
498 }
499
500
501 void UpdateScenePlay()
502 {
503 if (IsKeyPressed(KEY_ESCAPE))
504 {
505 nextScene = GAME_SCENE_MENU;
506 }
507
508 if (IsClickActioned() && playfield.strikes < MAX_STRIKES)
509 {
510 Vector2 pos = PlayfieldScreenToSpawnPosition(&playfield, camera,
511 GetMousePosition());
512 if (pos.y >= 0.0f && pos.y <= playfield.fieldSize.y
513 && pos.x >= 0.0f && pos.x <= playfield.fieldSize.x)
514 {
515 PlayfieldTryAddBubble(&playfield, pos);
516 }
517 }
518
519 while (gameTime.fixedTime < gameTime.time)
520 {
521 gameTime.fixedTime += gameTime.fixedDeltaTime;
522 PlayfieldFixedUpdate(&playfield);
523 }
524
525 // Draw
526 //----------------------------------------------------------------------------------
527
528 PlayfieldFullDraw(&playfield, camera);
529
530 PlayfieldScores scores = CalculatePlayfieldScores(&playfield);
531 int y = 50;
532 DrawText2("Shrimpy Bubble", 10, 20, WHITE);
533 DrawText2("Merge", 150, 42, WHITE);
534
535 DrawText2(TextFormat("Score: %d", scores.score), 10, y+=30, WHITE);
536 // DrawText2(TextFormat("Bubbles: %d", scores.bubbleCount), 10, y+=30, WHITE);
537 // DrawText2(TextFormat("Spawned: %d", playfield.spawnedBubbleCount), 10, y+=30, WHITE);
538 // DrawText2(TextFormat("Outside: %d", scores.outsideBubbleCount), 10, y+=30, WHITE);
539 DrawText2(TextFormat("Strikes: %d", playfield.strikes), 10, y+=30,
540 playfield.strikes > 0 ? ColorLerp(WHITE, COLOR_FROM_HEX(0xff4433), sinf(gameTime.time * 10.0f)) : WHITE);
541 if (playfield.strikes >= MAX_STRIKES)
542 {
543 DrawText2(TextFormat("Gameover in: %.1fs", GAMEOVER_COOLDOWN - playfield.gameoverCooldown), 10, y+=30, RED);
544 }
545
546 if (Button("< Menu", (Vector2){10, GetScreenHeight() - 100}, (Vector2){150, 80}))
547 {
548 nextScene = GAME_SCENE_MENU;
549 }
550 }
551
552 void UpdateSceneSettings()
553 {
554 ClearBackground(COLOR_FROM_HEX(0x4488cc));
555 int hCenter = 200;
556
557 DrawTextEx(fontMainLarge, "Shrimpy Bubble Merge Settings",
558 (Vector2) {50, 15}, fontMainLarge.baseSize, 1, WHITE);
559
560 DrawTexture(texShrimpy, GetScreenWidth() - 400, GetScreenHeight() - 260, WHITE);
561
562 int buttonH = 60;
563 int y = 100;
564 if (Button("Clear data", (Vector2){hCenter - 100, y}, (Vector2){200, buttonH}))
565 {
566 nextScene = GAME_SCENE_MENU;
567 memset(&storage, 0, sizeof(storage));
568 SaveStorage();
569 }
570
571 if (Button("< Menu", (Vector2){hCenter - 100, y += 60}, (Vector2){200, buttonH}))
572 {
573 nextScene = GAME_SCENE_MENU;
574 }
575 }
576
577 void UpdateSceneMenu()
578 {
579 ClearBackground(COLOR_FROM_HEX(0x4488cc));
580
581 int hCenter = 200;
582
583 DrawTextEx(fontMainLarge, "Shrimpy Bubble Merge",
584 (Vector2) {50, 15}, fontMainLarge.baseSize, 1, WHITE);
585
586 DrawTexture(texShrimpy, GetScreenWidth() - 400, GetScreenHeight() - 260, WHITE);
587
588 int buttonH = 60;
589 int y = 100;
590 if (Button("Tutorial", (Vector2){hCenter - 100, y}, (Vector2){200, buttonH}))
591 {
592 gameDifficulty = GAME_DIFFICULTY_TUTORIAL;
593 nextScene = GAME_SCENE_PLAY;
594 }
595
596 if (Button("Play easy", (Vector2){hCenter - 100, y += 60}, (Vector2){200, buttonH}))
597 {
598 gameDifficulty = GAME_DIFFICULTY_EASY;
599 nextScene = GAME_SCENE_PLAY;
600 }
601
602 if (Button("Play normal", (Vector2){hCenter - 100, y+=60}, (Vector2){200, buttonH}))
603 {
604 gameDifficulty = GAME_DIFFICULTY_NORMAL;
605 nextScene = GAME_SCENE_PLAY;
606 }
607
608 if (Button("Play hard", (Vector2){hCenter - 100, y+=60}, (Vector2){200, buttonH}))
609 {
610 gameDifficulty = GAME_DIFFICULTY_HARD;
611 nextScene = GAME_SCENE_PLAY;
612 }
613
614 if (Button("Settings", (Vector2){hCenter - 100, y+=60}, (Vector2){200, buttonH}))
615 {
616 nextScene = GAME_SCENE_SETTINGS;
617 }
618 }
619 #if defined(PLATFORM_WEB)
620 #include <emscripten.h>
621
622 // Function to store data in Local Storage
623 void StoreData(const char *key, const char *value) {
624 EM_ASM_({
625 localStorage.setItem(UTF8ToString($0), UTF8ToString($1));
626 }, key, value);
627 }
628
629 // Function to retrieve data from Local Storage
630 const char* RetrieveData(const char *key) {
631 return (const char*)EM_ASM_INT({
632 var value = localStorage.getItem(UTF8ToString($0));
633 if (value === null) {
634 return 0;
635 }
636 var lengthBytes = lengthBytesUTF8(value) + 1;
637 var stringOnWasmHeap = _malloc(lengthBytes);
638 stringToUTF8(value, stringOnWasmHeap, lengthBytes);
639 return stringOnWasmHeap;
640 }, key);
641 }
642 #else
643 void StoreData(const char *key, const char *value) {}
644 const char* RetrieveData(const char *key) { return 0; }
645 #endif
646
647 uint32_t RetrieveUInt32(const char *key)
648 {
649 const char *value = RetrieveData(key);
650 if (value)
651 {
652 uint32_t result = atoi(value);
653 free((void*)value);
654 return result;
655 }
656 return 0;
657 }
658
659 void StoreUInt32(const char *key, uint32_t value)
660 {
661 char buffer[16];
662 sprintf(buffer, "%d", value);
663 StoreData(key, buffer);
664 }
665
666 void RetrieveFixedChar(const char *key, char *buffer, int size)
667 {
668 const char *value = RetrieveData(key);
669 if (value)
670 {
671 strncpy(buffer, value, size);
672 free((void*)value);
673 }
674 else
675 {
676 buffer[0] = '\0';
677 }
678 }
679
680 void StoreFixedChar(const char *key, const char *value)
681 {
682 StoreData(key, value);
683 }
684
685
686 void LoadStorage()
687 {
688 // ignore version as this is first version. Upgrades need to be backwards compatible
689 for (int i = 0; i < 4; i++)
690 {
691 for (int j = 0; j < 8; j++)
692 {
693 char key[64];
694 sprintf(key, "storage.highscore_%d_%d", i, j);
695 storage.scores[i].highscores[j].score = RetrieveUInt32(key);
696 sprintf(key, "storage.name_%d_%d", i, j);
697 RetrieveFixedChar(key, storage.scores[i].highscores[j].name, 16);
698 sprintf(key, "storage.date_%d_%d", i, j);
699 RetrieveFixedChar(key, storage.scores[i].highscores[j].date, 16);
700 }
701 }
702 storage.tutorialStep = RetrieveUInt32("storage.tutorialStep");
703 storage.tutorialStepCount = RetrieveUInt32("storage.tutorialStepCount");
704 storage.tutorialCompleted = RetrieveUInt32("storage.tutorialCompleted");
705 storage.startups = RetrieveUInt32("storage.startups");
706 StoreUInt32("storage.startups", storage.startups + 1);
707 }
708
709 void SaveStorage()
710 {
711 StoreUInt32("storage.version", 1);
712 for (int i = 0; i < 4; i++)
713 {
714 for (int j = 0; j < 8; j++)
715 {
716 char key[64];
717 sprintf(key, "storage.highscore_%d_%d", i, j);
718 StoreUInt32(key, storage.scores[i].highscores[j].score);
719 sprintf(key, "storage.name_%d_%d", i, j);
720 StoreFixedChar(key, storage.scores[i].highscores[j].name);
721 sprintf(key, "storage.date_%d_%d", i, j);
722 StoreFixedChar(key, storage.scores[i].highscores[j].date);
723 }
724 }
725 StoreUInt32("storage.tutorialStep", storage.tutorialStep);
726 StoreUInt32("storage.tutorialStepCount", storage.tutorialStepCount);
727 StoreUInt32("storage.tutorialCompleted", storage.tutorialCompleted);
728 }
729
730 void LogStorage()
731 {
732 TraceLog(LOG_INFO, "[storage] Storage log");
733 for (int i = 0; i < 4; i++)
734 {
735 for (int j = 0; j < 8; j++)
736 {
737 TraceLog(LOG_INFO, "[storage] Highscore %d %d: %d %s %s", i, j, storage.scores[i].highscores[j].score,
738 storage.scores[i].highscores[j].name, storage.scores[i].highscores[j].date);
739 }
740 }
741 TraceLog(LOG_INFO, "[storage] Tutorial: %d %d %d", storage.tutorialStep, storage.tutorialStepCount, storage.tutorialCompleted);
742 TraceLog(LOG_INFO, "[storage] Startups: %d", storage.startups);
743 }
744
745 #include <time.h>
746 void StoreHighScore(PlayfieldScores scores)
747 {
748 DifficultyScores *difficultyScores = &storage.scores[gameDifficulty];
749
750 for (int i = 0; i < 8; i++)
751 {
752 if (scores.score > difficultyScores->highscores[i].score)
753 {
754 for (int j = 7; j > i; j--)
755 {
756 difficultyScores->highscores[j] = difficultyScores->highscores[j - 1];
757 }
758 difficultyScores->highscores[i].score = scores.score;
759 time_t now = time(0);
760 struct tm *tm = localtime(&now);
761 strftime(difficultyScores->highscores[i].date, 16, "%Y-%m-%d", tm);
762 difficultyScores->highscores[i].name[0] = '\0';
763 SaveStorage();
764 break;
765 }
766 }
767 }
768
769 void TutorialBubble(int step, const char *text, int x, int y, int width, int height)
770 {
771 if (playfield.tutorialStep != step) return;
772 Rectangle rect = {x, y, width, height};
773 DrawTextureNPatch(texUiAtlas, nPatchButton, rect, Vector2Zero(), 0.0f, COLOR_FROM_HEX(0xcceeff));
774
775 Font font = fontMainMedium;
776 int lineHeight = font.baseSize - 4;
777 int lineCount = 1;
778 for (int i = 0; i < strlen(text); i++)
779 {
780 if (text[i] == '\n')
781 {
782 lineCount++;
783 }
784 }
785 int yTop = y;
786 float textHeight = lineCount * lineHeight;
787 const char *line = text;
788 y = y + height * 0.5f - textHeight * 0.5f;
789 while (*line) {
790 char lineBuffer[256];
791 const char *end = line;
792 while (*end && *end != '\n') {
793 lineBuffer[end - line] = *end;
794 end++;
795 }
796 lineBuffer[end - line] = '\0';
797
798 Vector2 tsize = MeasureTextEx(font, lineBuffer, font.baseSize, 1);
799 DrawTextEx(font, lineBuffer, (Vector2){ x + width * 0.5f - tsize.x * 0.5f, y }, font.baseSize, 1, BLACK);
800
801 line = end;
802 while (*line == '\n') line++;
803 y += lineHeight;
804 }
805
806 DrawTextureRec(texUiAtlas, rectUiAtlasCircle, (Vector2){x + width - 30, yTop}, COLOR_FROM_HEX(0xccddff));
807 DrawTextureRec(texUiAtlas, rectUiAtlasShrimpy, (Vector2){x + width, yTop + 10}, WHITE);
808 }
809
810 void TutorialSetClicksBlocked(int step, int blocked)
811 {
812 if (playfield.tutorialStep != step) return;
813 isClickActionBlocked = blocked;
814 }
815
816 void TutorialProceedOnClick(int step)
817 {
818 if (playfield.tutorialStep != step) return;
819 if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) {
820 playfield.nextTutorialStep = step + 1;
821 }
822 }
823
824 void TutorialHighlightCircle(int step, int x, int y, int radius)
825 {
826 if (playfield.tutorialStep != step) return;
827 float pulse = sinf(gameTime.time * 10.0f) * 0.25f + 1.0f;
828 DrawCircleLines(x, y, radius * pulse, COLOR_FROM_HEX(0xff0000));
829 DrawCircleLines(x, y, radius * pulse + 0.5f, COLOR_FROM_HEX(0xff0000));
830 DrawCircleLines(x, y, radius * pulse + 1.0f, COLOR_FROM_HEX(0xff0000));
831 }
832
833 void TutorialSpawnBubble(int step, int x, int y, int level, int type, int nextLevel, int nextType)
834 {
835 if (playfield.tutorialStep != step || !playfield.tutorialStepStart) return;
836 Vector2 pos = PlayfieldScreenToSpawnPosition(&playfield, camera,
837 (Vector2){x, y});
838 playfield.nextBubbleLevel = level;
839 playfield.nextBubbleType = type;
840 PlayfieldTryAddBubble(&playfield, pos);
841 playfield.nextBubbleLevel = nextLevel;
842 playfield.nextBubbleType = nextType;
843 }
844
845 void UpdateTutorialSystem_PlayTutorial()
846 {
847 int step = 0;
848 int boxHeight = 105;
849 TutorialSetClicksBlocked(step, 1);
850 TutorialBubble(step, "Welcome to my fishtank...", 10, 10, 600, boxHeight);
851 TutorialProceedOnClick(step);
852 step++;
853
854 TutorialSetClicksBlocked(step, 1);
855 TutorialBubble(step, "It is very boring here.\nSo I invented this little game:", 10, 10, 600, boxHeight);
856 TutorialProceedOnClick(step);
857 step++;
858
859 TutorialSetClicksBlocked(step, 1);
860 TutorialBubble(step, "Please tap on the marked area ...", 10, 10, 600, boxHeight);
861 TutorialProceedOnClick(step);
862 TutorialHighlightCircle(step, GetScreenWidth() / 2, GetScreenHeight() - 50, 20);
863 step++;
864
865 TutorialSpawnBubble(step, GetScreenWidth() / 2, GetScreenHeight() - 50, 1, 0, 1, 0);
866 TutorialBubble(step, "Very good! Now do it again!", 10, 10, 600, boxHeight);
867 TutorialProceedOnClick(step);
868 TutorialHighlightCircle(step, GetScreenWidth() / 2, GetScreenHeight() - 50, 20);
869 TutorialSetClicksBlocked(step, 1);
870 step++;
871
872 TutorialSpawnBubble(step, GetScreenWidth() / 2, GetScreenHeight() - 50, 1, 0, 2, 1);
873 TutorialBubble(step,
874 "These two bubbles have the same size and\n"
875 "color. When they touch, they will merge.", 10, 10, 600, boxHeight);
876 TutorialProceedOnClick(step);
877 TutorialSetClicksBlocked(step, 1);
878 step++;
879
880 TutorialBubble(step, "But currently, they don't :(\nLet me help you a little.", 10, 10, 600, boxHeight);
881 TutorialProceedOnClick(step);
882 TutorialSetClicksBlocked(step, 1);
883 step++;
884 TutorialBubble(step, "...", 10, 10, 600, boxHeight);
885 if (step == playfield.tutorialStep) {
886 playfield.enableTutorialMerge = 1;
887 if (playfield.bubbles[0].bubbleLevel == 2)
888 {
889 playfield.nextTutorialStep = step + 1;
890 }
891 }
892 step++;
893 TutorialBubble(step, "See? The two bubbles merged and now\nit is bigger - look and the two dots it got.", 10, 10, 600, boxHeight);
894 TutorialProceedOnClick(step);
895 TutorialSetClicksBlocked(step, 1);
896 step++;
897 TutorialBubble(step, "Oh, the next bubble has a different color.\nTap again!", 10, 10, 600, boxHeight);
898 TutorialProceedOnClick(step);
899 TutorialHighlightCircle(step, GetScreenWidth() / 2, GetScreenHeight() - 50, 20);
900 TutorialSetClicksBlocked(step, 1);
901
902 step++;
903 TutorialSpawnBubble(step, GetScreenWidth() / 2, GetScreenHeight() - 50, 2, 1, 2, 1);
904 TutorialBubble(step, "It won't merge with the bright bubble.\nI don't know why.", 10, 10, 600, boxHeight);
905 TutorialProceedOnClick(step);
906
907 step++;
908 TutorialBubble(step, "But look! We have another bubble of \nthe same color! Release it!", 10, 10, 600, boxHeight);
909 TutorialProceedOnClick(step);
910 TutorialHighlightCircle(step, GetScreenWidth() / 2, GetScreenHeight() - 50, 20);
911 step++;
912 TutorialSpawnBubble(step, GetScreenWidth() / 2, GetScreenHeight() - 50, 2, 1, 2, 1);
913 TutorialBubble(step, "Wait for it...", 10, 10, 600, boxHeight);
914 if (step == playfield.tutorialStep) {
915 playfield.enableTutorialMerge = 1;
916 if (playfield.bubbles[1].bubbleLevel == 3)
917 {
918 playfield.nextTutorialStep = step + 1;
919 }
920 }
921
922 step++;
923 TutorialBubble(step, "Just watch out: When the bubbles go too high\nor low, they all pop :(", 10, 10, 600, boxHeight);
924 TutorialProceedOnClick(step);
925
926 step++;
927 TutorialBubble(step, "I love these bubbles!\nGo and try to see how many you can get!", 10, 10, 600, boxHeight);
928 TutorialProceedOnClick(step);
929 if (step == playfield.tutorialStep) {
930 playfield.enableTutorialMerge = 0;
931 }
932
933 step++;
934 TutorialSetClicksBlocked(step, 0);
935
936 if (playfield.strikes > 0 && playfield.tutorialStep == step)
937 {
938 playfield.nextTutorialStep = step + 1;
939 }
940 step++;
941
942 TutorialBubble(step, "Oh no! The bubbles are about to pop!\nCareful, you have 2 more tries!", 10, 10, 600, boxHeight);
943 TutorialProceedOnClick(step);
944 TutorialSetClicksBlocked(step, 1);
945 step++;
946 TutorialSetClicksBlocked(step, 0);
947 }
948
949 void UpdateTutorialSystem()
950 {
951 if (currentScene == GAME_SCENE_PLAY && gameDifficulty == GAME_DIFFICULTY_TUTORIAL)
952 {
953 UpdateTutorialSystem_PlayTutorial();
954 }
955
956 if (playfield.nextTutorialStep != playfield.tutorialStep)
957 {
958 playfield.tutorialStepStart = 1;
959 }
960 else
961 {
962 playfield.tutorialStepStart = 0;
963 }
964 playfield.tutorialStep = playfield.nextTutorialStep;
965 }
966
967 int main(void)
968 {
969 // Initialization
970 //--------------------------------------------------------------------------------------
971 const int screenWidth = 800;
972 const int screenHeight = 450;
973
974 InitWindow(screenWidth, screenHeight, "Shrimpy Bubble Merge");
975
976 SetTargetFPS(60); // Set our game to run at 60 frames-per-second
977 //--------------------------------------------------------------------------------------
978
979 TraceLog(LOG_INFO, "loading shaders");
980 bubbleOutlineShader = LoadShader(0, "data/bubble_outline.fs");
981 // Get shader locations
982 outlineSizeLoc = GetShaderLocation(bubbleOutlineShader, "outlineSize");
983 outlineColorLoc = GetShaderLocation(bubbleOutlineShader, "outlineColor");
984 textureSizeLoc = GetShaderLocation(bubbleOutlineShader, "textureSize");
985
986 LoadStorage();
987 LogStorage();
988
989 texShrimpy = LoadTexture("data/shrimpy.png");
990 texUiAtlas = LoadTexture("data/ui-atlas.png");
991 texGround = LoadTexture("data/ground.png");
992 fontMainMedium = LoadFontEx("data/IndieFlower-Regular.ttf", 40, 0, 0);
993 fontMainLarge = LoadFontEx("data/IndieFlower-Regular.ttf", 70, 0, 0);
994 bubbleModel = LoadModelFromMesh(GenMeshSphere(1.0f, 4, 24));
995 // Main game loop
996 while (!WindowShouldClose()) // Detect window close button or ESC key
997 {
998 if (bubbleFieldTexture.texture.width != GetScreenWidth() || bubbleFieldTexture.texture.height != GetScreenHeight())
999 {
1000 UnloadRenderTexture(bubbleFieldTexture);
1001 bubbleFieldTexture = LoadRenderTexture(GetScreenWidth(), GetScreenHeight());
1002 }
1003
1004 float dt = GetFrameTime();
1005 // clamp dt to prevent large time steps, e.g. when browser tab is inactive
1006 if (dt > 0.2f) dt = 0.2f;
1007 gameTime.time += dt;
1008 gameTime.deltaTime = dt;
1009
1010 BeginDrawing();
1011
1012 int y = GetScreenHeight() - texGround.height;
1013 for (int x=0; x<screenWidth; x+=texGround.width)
1014 {
1015 DrawTexture(texGround, x, y, WHITE);
1016 }
1017
1018 // draw wavy water line at top
1019 Vector2 waterLine[80];
1020 float waterXStep = GetScreenWidth() / 38;
1021 for (int i = 0; i < 40; i++)
1022 {
1023 waterLine[i * 2] = (Vector2){i * waterXStep, 0};
1024 waterLine[i * 2 + 1] = (Vector2){i * waterXStep, 15
1025 + sinf(gameTime.time * 2.9f + waterLine[i * 2 + 1].x * 0.008f) * 2.0f
1026 + sinf(gameTime.time * 4.3f + waterLine[i * 2 + 1].x * 0.015f) * 1.0f
1027 + sinf(gameTime.time * 6.9f + waterLine[i * 2 + 1].x * 0.032f) * 0.5f
1028 };
1029 }
1030 DrawTriangleStrip(waterLine, 80, COLOR_FROM_HEX(0x88ccff));
1031
1032 for (int i = 0; i < 40; i++)
1033 {
1034 float x = i * waterXStep;
1035 waterLine[i * 2 + 1] = (Vector2){x, 16
1036 + sinf(gameTime.time * 2.9f + x * 0.008f) * 2.0f
1037 + sinf(gameTime.time * 4.3f + x * 0.015f) * 1.0f
1038 + sinf(gameTime.time * 6.9f + x * 0.032f) * 0.5f
1039 };
1040 waterLine[i * 2] = (Vector2){x, waterLine[i * 2 + 1].y - 2.0f};
1041 }
1042 DrawTriangleStrip(waterLine, 80, COLOR_FROM_HEX(0xeef8ff));
1043
1044
1045
1046 switch (currentScene)
1047 {
1048 default: UpdateSceneMenu(); break;
1049 case GAME_SCENE_SETTINGS: UpdateSceneSettings(); break;
1050 case GAME_SCENE_PLAY: UpdateScenePlay(); break;
1051 case GAME_SCENE_GAMEOVER: UpdateSceneGameOver(); break;
1052 }
1053
1054 UpdateTutorialSystem();
1055
1056 switch (nextScene)
1057 {
1058 case GAME_SCENE_NONE: break;
1059 default: currentScene = nextScene; break;
1060 case GAME_SCENE_GAMEOVER:
1061 StoreHighScore(CalculatePlayfieldScores(&playfield));
1062 currentScene = GAME_SCENE_GAMEOVER;
1063 break;
1064 case GAME_SCENE_PLAY:
1065 playfield = (Playfield){
1066 .fieldSize = {230, 300},
1067 .waterLevel = 200.0f,
1068 .minHeight = 90.0f,
1069 .maxHeight = 280.0f,
1070 };
1071 if (gameDifficulty == GAME_DIFFICULTY_TUTORIAL)
1072 {
1073 playfield.waterLevel = 180.0f;
1074 }
1075 currentScene = GAME_SCENE_PLAY; break;
1076 }
1077
1078 if (nextScene != GAME_SCENE_NONE)
1079 {
1080 isClickActionBlocked = 0;
1081 playfield.tutorialStep = 0;
1082 playfield.nextTutorialStep = 0;
1083 }
1084
1085 nextScene = GAME_SCENE_NONE;
1086 EndDrawing();
1087 }
1088
1089 // De-Initialization
1090 //--------------------------------------------------------------------------------------
1091 CloseWindow(); // Close window and OpenGL context
1092 //--------------------------------------------------------------------------------------
1093
1094 return 0;
1095 }
1 #ifndef __BPOP_MAIN_H__
2 #define __BPOP_MAIN_H__
3
4 #include "raylib.h"
5 #include "raymath.h"
6 #include "rlgl.h"
7 #include <stdint.h>
8 #include <math.h>
9
10 typedef enum GameDifficulty
11 {
12 GAME_DIFFICULTY_TUTORIAL,
13 GAME_DIFFICULTY_EASY,
14 GAME_DIFFICULTY_NORMAL,
15 GAME_DIFFICULTY_HARD,
16 } GameDifficulty;
17
18 typedef struct HighscoreEntry
19 {
20 uint32_t score;
21 char name[16];
22 char date[16];
23 } HighscoreEntry;
24
25 typedef struct DifficultyScores
26 {
27 HighscoreEntry highscores[8];
28 } DifficultyScores;
29
30
31 typedef struct Storage
32 {
33 uint32_t version;
34 uint32_t startups;
35 DifficultyScores scores[4];
36 uint32_t tutorialStep;
37 uint32_t tutorialStepCount;
38 uint32_t tutorialCompleted;
39 } Storage;
40
41 typedef enum GameScene
42 {
43 GAME_SCENE_NONE,
44 GAME_SCENE_MENU,
45 GAME_SCENE_DIFFICULTY_SELECT,
46 GAME_SCENE_PLAY,
47 GAME_SCENE_GAMEOVER,
48 GAME_SCENE_HIGHSCORES,
49 GAME_SCENE_SETTINGS,
50 } GameScene;
51
52
53 #define MAX_BUBBLE_TYPES 3
54
55 typedef struct Bubble
56 {
57 uint8_t flagIsActive:1;
58 uint8_t sameLevelContact:1;
59 uint8_t isOutsideZone:1;
60 uint8_t bubbleType:3;
61 uint8_t bubbleLevel;
62
63 Vector2 position;
64 Vector2 velocity;
65 float bubbleMergeCooldown;
66 float radius;
67 float lifeTime;
68 } Bubble;
69
70 #define MAX_BUBBLES 64
71 #define MAX_STRIKES 3
72 #define GAMEOVER_COOLDOWN 2.0f
73
74 typedef struct PlayfieldScores
75 {
76 uint8_t bubbleCount;
77 uint8_t outsideBubbleCount;
78 uint32_t score;
79 } PlayfieldScores;
80
81 typedef struct Playfield
82 {
83 Bubble bubbles[MAX_BUBBLES];
84 Matrix transform;
85 Vector2 fieldSize;
86 float waterLevel;
87 float maxHeight;
88 float minHeight;
89 uint8_t nextBubbleLevel;
90 uint8_t nextBubbleType;
91 // every placed bubble when a bubble is outside the zone counts
92 // as a strike, 3 strikes and it's game over. Strikes are
93 // reset when all bubbles are inside the zone.
94 uint8_t strikes;
95 uint8_t tutorialStepStart;
96 uint8_t enableTutorialMerge;
97 float gameoverCooldown;
98 uint32_t spawnedBubbleCount;
99 uint32_t tutorialStep;
100 uint32_t nextTutorialStep;
101 } Playfield;
102
103 typedef struct GameTime
104 {
105 float time;
106 float deltaTime;
107 float fixedTime;
108 float fixedDeltaTime;
109 } GameTime;
110
111 #define COLOR_FROM_HEX(hexValue) (Color){((hexValue) >> 16) & 0xFF, ((hexValue) >> 8) & 0xFF, (hexValue) & 0xFF, 0xFF}
112 #endif
Much nicer now. But time is running out. It is 14:50 and I need to focus on the important things... Graphic is all rudiemtary in. Sounds are missing and the game over screen should pop the bubbles. After that, there is also visualization improvements for the bubble interactions, like when they touch and can merge.
1 #include "bpop_main.h"
2 #include <string.h>
3 #include <stdlib.h>
4 #include <stdio.h>
5
6 int playSounds = 1;
7 GameScene currentScene = GAME_SCENE_MENU;
8 GameScene nextScene = GAME_SCENE_NONE;
9 PlayfieldScores finalScores;
10 Sound sfxPop;
11 GameDifficulty gameDifficulty = GAME_DIFFICULTY_NORMAL;
12
13 Storage storage = {
14 .version = 1,
15 };
16
17 Texture2D texShrimpy;
18 Texture2D texLeftCorner;
19 Texture2D texUiAtlas;
20 Texture2D texGround;
21 Font fontMainMedium;
22 Font fontMainLarge;
23
24 NPatchInfo nPatchButton = {
25 .source = {0,0,240,145},
26 .left = 10,
27 .top = 10,
28 .right = 10,
29 .bottom = 10,
30 .layout = NPATCH_NINE_PATCH
31 };
32 Rectangle rectUiAtlasCircle = {
33 .x = 250,
34 .y = 0,
35 .width = 148,
36 .height = 148,
37 };
38 Rectangle rectUiAtlasShrimpy = {
39 .x = 420,
40 .y = 390,
41 .width = 512-420,
42 .height = 512-390,
43 };
44
45 int outlineSizeLoc = 0;
46 int outlineColorLoc = 0;
47 int textureSizeLoc = 0;
48
49 Camera3D camera = {
50 .position = { 0.0f, 0.0f, -10.0f },
51 .target = { 0.0f, 0.0f, 0.0f },
52 .up = { 0.0f, 1.0f, 0.0f },
53 .projection = CAMERA_ORTHOGRAPHIC,
54 .fovy = 320.0f,
55 };
56
57
58 Playfield playfield = {
59 .fieldSize = {250, 300},
60 .waterLevel = 200.0f,
61 .minHeight = 90.0f,
62 .maxHeight = 280.0f,
63 };
64
65 GameTime gameTime = {
66 .fixedDeltaTime = 1.0f / 60.0f,
67 };
68
69 Color bubbleTypeColors[] = {
70 COLOR_FROM_HEX(0xaaccff),
71 COLOR_FROM_HEX(0x2277ff),
72 COLOR_FROM_HEX(0x33ffaa),
73 COLOR_FROM_HEX(0x9933aa),
74 COLOR_FROM_HEX(0xaa33ff),
75 };
76
77 Model bubbleModel;
78 Shader bubbleOutlineShader;
79 RenderTexture2D bubbleFieldTexture;
80
81 int isClickActionBlocked = 0;
82
83 PlayfieldScores CalculatePlayfieldScores(Playfield *playfield);
84 void SaveStorage();
85
86 void DrawText2(const char *text, int x, int y, Color color)
87 {
88 DrawTextEx(fontMainMedium, text, (Vector2){x, y}, fontMainMedium.baseSize, 1, color);
89 }
90
91 void DrawText3(const char *text, int x, int y, Color color)
92 {
93 DrawTextEx(fontMainLarge, text, (Vector2){x, y}, fontMainMedium.baseSize, 1, color);
94 }
95
96 float MeasureText2(const char *text)
97 {
98 return MeasureTextEx(fontMainMedium, text, fontMainMedium.baseSize, 1).x;
99 }
100
101 float BubbleLevelRadius(int level)
102 {
103 return powf((level + 1) * 120, .5f);
104 }
105
106 int IsClickActioned()
107 {
108 int result = 0;
109 if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON) && !isClickActionBlocked)
110 {
111 result = 1;
112 }
113 return result;
114 }
115
116 int Button(const char *text, Vector2 position, Vector2 size)
117 {
118 Rectangle rect = {position.x, position.y, size.x, size.y};
119 int isHovered = !isClickActionBlocked && CheckCollisionPointRec(GetMousePosition(), rect);
120 int result = isHovered && IsClickActioned();
121
122 Color color = isHovered ? COLOR_FROM_HEX(0xcceeff) : COLOR_FROM_HEX(0xaaccff);
123 if (!isClickActionBlocked && isHovered && IsMouseButtonDown(MOUSE_LEFT_BUTTON))
124 {
125 color = COLOR_FROM_HEX(0xddf8ff);
126 }
127
128 DrawTextureNPatch(texUiAtlas, nPatchButton, rect, Vector2Zero(), 0.0f, color);
129
130 int fontSize = fontMainMedium.baseSize;
131 Vector2 tsize = MeasureTextEx(fontMainMedium, text, fontSize, 1);
132 DrawTextEx(fontMainMedium, text, (Vector2){position.x + size.x * 0.5f - tsize.x * 0.5f, position.y + size.y * 0.5f - tsize.y * 0.5f}, fontSize, 1, BLACK);
133 return result;
134 }
135
136 void PlayfieldFixedUpdate(Playfield *playfield, int popBubbles)
137 {
138 for (int i = 0; i < MAX_BUBBLES; i++)
139 {
140 Bubble *bubble = &playfield->bubbles[i];
141 bubble->sameLevelContact = 0;
142 }
143
144 for (int i = 0; i < MAX_BUBBLES; i++)
145 {
146 Bubble *bubble = &playfield->bubbles[i];
147 if (!bubble->flagIsActive) continue;
148 float r = bubble->radius;
149
150 for (int j = i + 1; j < MAX_BUBBLES; j++)
151 {
152 Bubble *other = &playfield->bubbles[j];
153 if (!other->flagIsActive) continue;
154 float otherR = other->radius;
155 float sumR2 = (r + otherR) * (r + otherR);
156 float d2 = Vector2DistanceSqr(bubble->position, other->position);
157 int canMerge = bubble->bubbleLevel == other->bubbleLevel && bubble->bubbleType == other->bubbleType;
158 if (d2 < sumR2 * 1.05f)
159 {
160 if (canMerge)
161 {
162 bubble->sameLevelContact = 1;
163 other->sameLevelContact = 1;
164 }
165 if (canMerge && bubble->bubbleMergeCooldown <= 0.0f
166 && other->bubbleMergeCooldown <= 0.0f)
167 {
168 // merge bubbles
169 bubble->bubbleLevel++;
170 bubble->position = Vector2Lerp(bubble->position, other->position, 0.5f);
171 bubble->radius = BubbleLevelRadius(bubble->bubbleLevel);
172 bubble->bubbleMergeCooldown = 1.0f;
173
174 playfield->poppingBubbles[i] = *bubble;
175 playfield->poppingBubbles[i].lifeTime = 0.0f;
176 playfield->comboPopPoints += playfield->comboPop;
177 playfield->comboPop++;
178
179 if (playSounds)
180 PlaySound(sfxPop);
181
182 other->flagIsActive = 0;
183 }
184 }
185
186 if (d2 < sumR2)
187 {
188 float overlap = r + otherR - sqrtf(d2);
189 Vector2 normal = Vector2Normalize(Vector2Subtract(other->position, bubble->position));
190 // resolve overlap by moving the bubbles apart
191 const float errorCorrection = 0.25f;
192 bubble->position = Vector2Subtract(bubble->position, Vector2Scale(normal, overlap * errorCorrection));
193 other->position = Vector2Add(other->position, Vector2Scale(normal, overlap * errorCorrection));
194
195 // bounce off each other
196 Vector2 relativeVelocity = Vector2Subtract(bubble->velocity, other->velocity);
197 float dot = Vector2DotProduct(relativeVelocity, normal);
198 if (dot > 0.0f)
199 {
200 // DrawLineV(bubble->position, other->position, COLOR_FROM_HEX(0xff0000));
201 float impulse = -dot * 0.85f;
202 bubble->velocity = Vector2Add(bubble->velocity, Vector2Scale(normal, impulse));
203 other->velocity = Vector2Subtract(other->velocity, Vector2Scale(normal, impulse));
204 }
205 }
206 }
207
208 if (!bubble->sameLevelContact)
209 {
210 bubble->bubbleMergeCooldown = 1.0f;
211 }
212 else
213 {
214 bubble->bubbleMergeCooldown -= gameTime.fixedDeltaTime;
215 }
216
217 float buoyancy = -20.0f;
218 if (bubble->position.y < playfield->waterLevel)
219 {
220 buoyancy = (playfield->waterLevel - bubble->position.y) * 0.5f;
221 }
222
223 int isOutsideZone = bubble->position.y < playfield->minHeight + bubble->radius ||
224 bubble->position.y > playfield->maxHeight - bubble->radius;
225 bubble->lifeTime += gameTime.fixedDeltaTime;
226 bubble->isOutsideZone = isOutsideZone && bubble->lifeTime > 1.0f;
227
228 float horizontalV = 0.0f;
229 if (playfield->enableTutorialMerge) {
230 horizontalV = fminf(fmaxf(-10.0f, (playfield->fieldSize.x / 2 - bubble->position.x) * 0.1f), 10.0f);
231 }
232 bubble->velocity = Vector2Add(bubble->velocity, (Vector2){horizontalV, buoyancy});
233 bubble->velocity = Vector2Scale(bubble->velocity, 0.92f);
234 bubble->position.x += bubble->velocity.x * gameTime.fixedDeltaTime;
235 bubble->position.y += bubble->velocity.y * gameTime.fixedDeltaTime;
236 if ((bubble->position.x < r && bubble->velocity.x < 0.0f) ||
237 (bubble->position.x > playfield->fieldSize.x - r && bubble->velocity.x > 0.0f))
238 {
239 bubble->velocity.x *= -0.9f;
240 }
241
242 bubble->position.x = (bubble->position.x < r) ? r : (bubble->position.x > playfield->fieldSize.x - r) ? playfield->fieldSize.x - r : bubble->position.x;
243
244 if (popBubbles && GetRandomValue(0, 1000) < 50)
245 {
246 playfield->poppingBubbles[i] = *bubble;
247 playfield->poppingBubbles[i].lifeTime = 0.0f;
248 bubble->flagIsActive = 0;
249 }
250 // bubble->position.y = (bubble->position.y < r) ? r : (bubble->position.y > playfield->fieldSize.y - r) ? playfield->fieldSize.y - r : bubble->position.y;
251
252 // debug velocity
253 // DrawLineV(bubble->position, Vector2Add(bubble->position, Vector2Scale(bubble->velocity, 1.0f)), COLOR_FROM_HEX(0xff0000));
254 }
255
256 int outsideCount = 0;
257 for (int i = 0; i < MAX_BUBBLES; i++)
258 {
259 Bubble *bubble = &playfield->bubbles[i];
260 if (bubble->isOutsideZone)
261 {
262 outsideCount++;
263 }
264 }
265
266 if (outsideCount == 0)
267 {
268 playfield->strikes = 0;
269 playfield->gameoverCooldown = 0.0f;
270 }
271 if (playfield->strikes >= MAX_STRIKES && !popBubbles)
272 {
273 playfield->gameoverCooldown += gameTime.fixedDeltaTime;
274 if (playfield->gameoverCooldown > GAMEOVER_COOLDOWN)
275 {
276 finalScores = CalculatePlayfieldScores(playfield);
277 nextScene = GAME_SCENE_GAMEOVER;
278 }
279 }
280 }
281
282 void PlayfieldTryAddBubble(Playfield *playfield, Vector2 position)
283 {
284 if (playfield->strikes >= MAX_STRIKES) return;
285
286 for (int i = 0; i < MAX_BUBBLES; i++)
287 {
288 Bubble *bubble = &playfield->bubbles[i];
289 if (!bubble->flagIsActive)
290 {
291 bubble->flagIsActive = 1;
292 bubble->position = position;
293 bubble->velocity = (Vector2){GetRandomValue(-100, 100), GetRandomValue(-100, 100)};
294 bubble->bubbleType = playfield->nextBubbleType;
295 bubble->bubbleLevel = playfield->nextBubbleLevel;
296 bubble->radius = BubbleLevelRadius(bubble->bubbleLevel);
297 bubble->lifeTime = 0.0f;
298 playfield->spawnedBubbleCount++;
299
300 for (int j = 0; j < MAX_BUBBLES; j += 1)
301 {
302 Bubble *other = &playfield->bubbles[j];
303 if (!other->flagIsActive) continue;
304 if (other->isOutsideZone)
305 {
306 playfield->strikes++;
307 break;
308 }
309 }
310 playfield->comboPop = 0;
311 playfield->nextBubbleType = GetRandomValue(0, gameDifficulty > 0 ? gameDifficulty : 1);
312 playfield->nextBubbleLevel = GetRandomValue(0, 3);
313 break;
314 }
315 }
316
317 }
318
319 PlayfieldScores CalculatePlayfieldScores(Playfield *playfield)
320 {
321 PlayfieldScores scores = {0};
322 for (int i = 0; i < MAX_BUBBLES; i++)
323 {
324 Bubble *bubble = &playfield->bubbles[i];
325 if (bubble->flagIsActive)
326 {
327 scores.bubbleCount++;
328 uint32_t bubbleScore = 1 << bubble->bubbleLevel;
329 scores.score += bubbleScore;
330
331 if (bubble->isOutsideZone)
332 {
333 scores.outsideBubbleCount++;
334 }
335 }
336 }
337 scores.score += playfield->spawnedBubbleCount;
338 scores.score += playfield->comboPopPoints;
339 return scores;
340 }
341
342 Vector2 PlayfieldPositionToSpawnPosition(Playfield *playfield, Vector2 position)
343 {
344 Vector2 spawnPosition = position;
345 spawnPosition.y = BubbleLevelRadius(5);
346 return spawnPosition;
347 }
348
349 Vector2 PlayfieldScreenToSpawnPosition(Playfield *playfield, Camera3D camera, Vector2 screenPosition)
350 {
351 Vector3 cursorPosition = GetScreenToWorldRay(screenPosition, camera).position;
352 cursorPosition.x += playfield->fieldSize.x / 2;
353 cursorPosition.y += playfield->fieldSize.y / 2;
354
355 Vector2 pos = {cursorPosition.x, cursorPosition.y};
356 return PlayfieldPositionToSpawnPosition(playfield, pos);
357 }
358
359 void DrawBubblePop(Vector3 position, float t, int level, Color color)
360 {
361 // int oldSeed = GetRandomValue(0, 0xfffffff);
362 // SetRandomSeed((int)(position.x * 1000) ^ (int)(position.y * 1000) + level * 1000);
363 color.a = 255 * (1.0f - t);
364 DrawModel(bubbleModel, position, BubbleLevelRadius(level) * (1 + t), color);
365 }
366
367 void DrawBubble(Vector3 position, int level, Color color)
368 {
369 float bubbleExtraRadius = 5.0f;
370 float r = BubbleLevelRadius(level) + bubbleExtraRadius;
371 DrawModel(bubbleModel, position, r, color);
372 if (level < 1) return;
373 position.z -= r;
374 float tinyR = level < 6 ? 2 : 4;
375 int count = level < 6 ? level : level - 5;
376 for (int i = 0; i < count; i++)
377 {
378 float ang = (i * 25.0f + 30.0f) * DEG2RAD;
379 float offsetR = i % 2 == 0 ? 0.4f : 0.7f;
380 Vector3 offset = {cosf(ang) * offsetR * r, sinf(ang) * offsetR * r, 0};
381 DrawModel(bubbleModel, Vector3Add(position, offset), tinyR, WHITE);
382 }
383 }
384
385 void PlayfieldDrawBubbles(Playfield *playfield, Camera3D camera)
386 {
387 DrawCube((Vector3){0, 0, 0}, playfield->fieldSize.x, playfield->fieldSize.y, 0, COLOR_FROM_HEX(0xbbddff));
388 DrawCube((Vector3){0, (playfield->waterLevel - playfield->fieldSize.y) * 0.5f, 0}, playfield->fieldSize.x,
389 playfield->waterLevel, 0, COLOR_FROM_HEX(0x225588));
390
391 // cursor bubble
392 if (currentScene == GAME_SCENE_PLAY)
393 {
394 Vector2 mousePos = GetMousePosition();
395 Vector2 spawnPosition = PlayfieldScreenToSpawnPosition(playfield, camera, mousePos);
396 Vector3 drawPos = (Vector3){spawnPosition.x - playfield->fieldSize.x * 0.5f, spawnPosition.y - playfield->fieldSize.y * 0.5f, 0};
397 if (playfield->strikes < MAX_STRIKES && drawPos.x >= -playfield->fieldSize.x * 0.5f && drawPos.x <= playfield->fieldSize.x * 0.5f)
398 {
399 DrawBubble(drawPos, playfield->nextBubbleLevel, bubbleTypeColors[playfield->nextBubbleType]);
400 }
401 }
402
403 // DrawRectangle(0, 0, playfield->fieldSize.x, playfield->fieldSize.y, COLOR_FROM_HEX(0x225588));
404 rlPushMatrix();
405 rlTranslatef(-playfield->fieldSize.x / 2, -playfield->fieldSize.y / 2, 0);
406 // draw bubbles into playfield space
407 float blink = sinf(gameTime.time * 10.0f) * 0.2f + 0.5f;
408 for (int i = 0; i < MAX_BUBBLES; i++)
409 {
410 Bubble *bubble = &playfield->bubbles[i];
411 if (!bubble->flagIsActive) continue;
412 Vector3 position = {bubble->position.x, bubble->position.y, 0};
413 Color bubbleColor = bubbleTypeColors[bubble->bubbleType];
414 int isOutsideZone = bubble->isOutsideZone;
415
416 if (isOutsideZone)
417 {
418 bubbleColor = ColorLerp(bubbleColor, COLOR_FROM_HEX(0xff4433), blink);
419 }
420 // lazy: encode id into rgb values
421 bubbleColor.r = bubbleColor.r - i * 2 % 4;
422 bubbleColor.g = bubbleColor.g - (i * 2 / 4) % 4;
423 bubbleColor.b = bubbleColor.b - (i * 2 / 16);
424 bubbleColor.a = 255;
425 DrawBubble(position, bubble->bubbleLevel, bubbleColor);
426 }
427
428 for (int i = 0; i < MAX_BUBBLES; i++)
429 {
430 Bubble *bubble = &playfield->poppingBubbles[i];
431 if (!bubble->flagIsActive) continue;
432 bubble->lifeTime += gameTime.deltaTime;
433 float t = bubble->lifeTime / 0.125f;
434 if (t >= 1.0f)
435 {
436 bubble->flagIsActive = 0;
437 continue;
438 }
439 Vector3 position = {bubble->position.x, bubble->position.y, 0};
440 DrawBubblePop(position, t, bubble->bubbleLevel, bubbleTypeColors[bubble->bubbleType]);
441 }
442 rlPopMatrix();
443 }
444
445 void PlayfieldDrawRange(Playfield *playfield, Camera3D camera)
446 {
447 Color rangeLimitColor = COLOR_FROM_HEX(0xff4400);
448 int divides = 10;
449 float divWidth = playfield->fieldSize.x / divides;
450 for (int i = 0; i < divides; i+=2)
451 {
452 float x = i * divWidth - playfield->fieldSize.x * 0.5f + divWidth * 1.0f;
453 DrawCube((Vector3){x, playfield->minHeight - playfield->fieldSize.y * 0.5f, 0}, divWidth, 5, 5, rangeLimitColor);
454 DrawCube((Vector3){x, playfield->maxHeight - playfield->fieldSize.y * 0.5f, 0}, divWidth, 5, 5, rangeLimitColor);
455 }
456 }
457
458 void PlayfieldFullDraw(Playfield *playfield, Camera3D camera)
459 {
460 ClearBackground(COLOR_FROM_HEX(0x4488cc));
461
462 BeginTextureMode(bubbleFieldTexture);
463 rlSetClipPlanes(-128.0f, 128.0f);
464 BeginMode3D(camera);
465
466 ClearBackground(BLANK);
467 PlayfieldDrawBubbles(playfield, camera);
468 EndMode3D();
469 EndTextureMode();
470
471 float outlineSize = 1.0f;
472 float outlineColor[4] = { 1.0f, 1.0f, 1.0f, 1.0f };
473 float textureSize[2] = { (float)bubbleFieldTexture.texture.width, (float)bubbleFieldTexture.texture.height };
474
475 SetShaderValue(bubbleOutlineShader, outlineSizeLoc, &outlineSize, SHADER_UNIFORM_FLOAT);
476 SetShaderValue(bubbleOutlineShader, outlineColorLoc, outlineColor, SHADER_UNIFORM_VEC4);
477 SetShaderValue(bubbleOutlineShader, textureSizeLoc, textureSize, SHADER_UNIFORM_VEC2);
478
479 rlDisableDepthMask();
480 BeginShaderMode(bubbleOutlineShader);
481 DrawTexturePro(bubbleFieldTexture.texture, (Rectangle){0, 0, (float)bubbleFieldTexture.texture.width,
482 -(float)bubbleFieldTexture.texture.height}, (Rectangle){0, 0, (float)GetScreenWidth(), (float)GetScreenHeight()},
483 (Vector2){0, 0}, 0.0f, WHITE);
484 EndShaderMode();
485 rlEnableDepthMask();
486
487 BeginMode3D(camera);
488 PlayfieldDrawRange(playfield, camera);
489 EndMode3D();
490
491 const char *difficultyText = "Tutorial";
492 switch (gameDifficulty)
493 {
494 case GAME_DIFFICULTY_EASY: difficultyText = "Easy"; break;
495 case GAME_DIFFICULTY_NORMAL: difficultyText = "Normal"; break;
496 case GAME_DIFFICULTY_HARD: difficultyText = "Hard"; break;
497 default:
498 break;
499 }
500 const char *modeText = TextFormat("Mode: %s", difficultyText);
501 int screenWidth = GetScreenWidth();
502 int x = screenWidth - 220;
503 DrawText2("Highscores", x, 25, WHITE);
504 DrawText2(modeText, x, 55, WHITE);
505 DifficultyScores table = storage.scores[gameDifficulty];
506 for (int i = 0; i < 8; i++)
507 {
508 HighscoreEntry entry = table.highscores[i];
509 if (entry.score == 0) break;
510 char buffer[64];
511 sprintf(buffer, "%d:", i + 1);
512 int y = 110 + i * 30;
513 DrawText2(buffer, x + 18 - MeasureText2(buffer), y, WHITE);
514 sprintf(buffer, "%d", entry.score);
515 DrawText2(buffer, x + 50 - MeasureText2(buffer) / 2, y, WHITE);
516 sprintf(buffer, "%s", entry.date);
517 DrawText2(buffer, screenWidth - 10 - MeasureText2(buffer), y, WHITE);
518 }
519
520 int sx = 0, sy = 0;
521 if (playfield->comboPop > 2)
522 {
523 sx = 1;
524 }
525 if (playfield->strikes > 0)
526 {
527 sx = 0;
528 sy = 1;
529 }
530 if (currentScene == GAME_SCENE_GAMEOVER)
531 {
532 sx = 1;
533 sy = 1;
534 }
535 DrawTextureRec(texShrimpy,
536 (Rectangle){sx * 256, sy * 256, 256, 256},
537 (Vector2){GetScreenWidth() - 200, GetScreenHeight() - 260}, WHITE);
538 }
539
540 void UpdateSceneGameOver()
541 {
542 if (IsKeyPressed(KEY_ENTER))
543 {
544 nextScene = GAME_SCENE_MENU;
545 }
546 // Draw
547 //----------------------------------------------------------------------------------
548
549 ClearBackground(COLOR_FROM_HEX(0x4488cc));
550
551 PlayfieldFixedUpdate(&playfield, 1);
552 PlayfieldFullDraw(&playfield, camera);
553
554 DrawText3("Game Over", 20, 20, WHITE);
555
556 DrawText2(TextFormat("Final Score: %d", finalScores.score), 20, 90, WHITE);
557
558 if (Button("< Menu", (Vector2){20, GetScreenHeight() - 110}, (Vector2){180, 90}))
559 {
560 nextScene = GAME_SCENE_MENU;
561 }
562 }
563
564
565 void UpdateScenePlay()
566 {
567 if (IsKeyPressed(KEY_ESCAPE))
568 {
569 nextScene = GAME_SCENE_MENU;
570 }
571
572 if (IsClickActioned() && playfield.strikes < MAX_STRIKES)
573 {
574 Vector2 pos = PlayfieldScreenToSpawnPosition(&playfield, camera,
575 GetMousePosition());
576 if (pos.y >= 0.0f && pos.y <= playfield.fieldSize.y
577 && pos.x >= 0.0f && pos.x <= playfield.fieldSize.x)
578 {
579 PlayfieldTryAddBubble(&playfield, pos);
580 }
581 }
582
583 while (gameTime.fixedTime < gameTime.time)
584 {
585 gameTime.fixedTime += gameTime.fixedDeltaTime;
586 PlayfieldFixedUpdate(&playfield, 0);
587 }
588
589 // Draw
590 //----------------------------------------------------------------------------------
591
592 PlayfieldFullDraw(&playfield, camera);
593
594 PlayfieldScores scores = CalculatePlayfieldScores(&playfield);
595 int y = 50;
596 DrawText2("Shrimpy Bubble", 10, 20, WHITE);
597 DrawText2("Merge", 150, 42, WHITE);
598
599 DrawText2(TextFormat("Score: %d", scores.score), 10, y+=30, WHITE);
600 DrawText2(TextFormat("Combos: %d", playfield.comboPop), 10, y+=30, WHITE);
601 // DrawText2(TextFormat("Bubbles: %d", scores.bubbleCount), 10, y+=30, WHITE);
602 // DrawText2(TextFormat("Spawned: %d", playfield.spawnedBubbleCount), 10, y+=30, WHITE);
603 // DrawText2(TextFormat("Outside: %d", scores.outsideBubbleCount), 10, y+=30, WHITE);
604 DrawText2(TextFormat("Strikes: %d", playfield.strikes), 10, y+=40,
605 playfield.strikes > 0 ? ColorLerp(WHITE, COLOR_FROM_HEX(0xff4433), sinf(gameTime.time * 10.0f)) : WHITE);
606 if (playfield.strikes >= MAX_STRIKES)
607 {
608 DrawText2(TextFormat("Gameover in: %.1fs", GAMEOVER_COOLDOWN - playfield.gameoverCooldown), 10, y+=30, RED);
609 }
610
611 if (Button("< Menu", (Vector2){10, GetScreenHeight() - 100}, (Vector2){150, 80}))
612 {
613 nextScene = GAME_SCENE_MENU;
614 }
615 }
616
617 void UpdateSceneSettings()
618 {
619 ClearBackground(COLOR_FROM_HEX(0x4488cc));
620 int hCenter = 200;
621
622 DrawTextEx(fontMainLarge, "Shrimpy Bubble Merge Settings",
623 (Vector2) {50, 15}, fontMainLarge.baseSize, 1, WHITE);
624
625 DrawTextureRec(texShrimpy,
626 (Rectangle){0, 256, 256, 256},
627 (Vector2){GetScreenWidth() - 400, GetScreenHeight() - 260}, WHITE);
628
629 int buttonH = 60;
630 int y = 50;
631
632 if (Button(playSounds ? "Sound: On" : "Sound: Off",
633 (Vector2){hCenter - 100, y += 60}, (Vector2){200, buttonH}))
634 {
635 playSounds = !playSounds;
636 SaveStorage();
637 }
638
639 if (Button("Clear data", (Vector2){hCenter - 100, y += 60}, (Vector2){200, buttonH}))
640 {
641 nextScene = GAME_SCENE_MENU;
642 memset(&storage, 0, sizeof(storage));
643 SaveStorage();
644 }
645
646 if (Button("< Menu", (Vector2){hCenter - 100, y += 60}, (Vector2){200, buttonH}))
647 {
648 nextScene = GAME_SCENE_MENU;
649 }
650 }
651
652 void UpdateSceneMenu()
653 {
654 ClearBackground(COLOR_FROM_HEX(0x4488cc));
655
656 int hCenter = 200;
657
658 DrawTextEx(fontMainLarge, "Shrimpy Bubble Merge",
659 (Vector2) {50, 15}, fontMainLarge.baseSize, 1, WHITE);
660
661 DrawTextureRec(texShrimpy,
662 (Rectangle){0, 0, 256, 256},
663 (Vector2){GetScreenWidth() - 400, GetScreenHeight() - 260}, WHITE);
664
665 int buttonH = 60;
666 int y = 100;
667 if (Button("Tutorial", (Vector2){hCenter - 100, y}, (Vector2){200, buttonH}))
668 {
669 gameDifficulty = GAME_DIFFICULTY_TUTORIAL;
670 nextScene = GAME_SCENE_PLAY;
671 }
672
673 if (Button("Play easy", (Vector2){hCenter - 100, y += 60}, (Vector2){200, buttonH}))
674 {
675 gameDifficulty = GAME_DIFFICULTY_EASY;
676 nextScene = GAME_SCENE_PLAY;
677 }
678
679 if (Button("Play normal", (Vector2){hCenter - 100, y+=60}, (Vector2){200, buttonH}))
680 {
681 gameDifficulty = GAME_DIFFICULTY_NORMAL;
682 nextScene = GAME_SCENE_PLAY;
683 }
684
685 if (Button("Play hard", (Vector2){hCenter - 100, y+=60}, (Vector2){200, buttonH}))
686 {
687 gameDifficulty = GAME_DIFFICULTY_HARD;
688 nextScene = GAME_SCENE_PLAY;
689 }
690
691 if (Button("Settings", (Vector2){hCenter - 100, y+=60}, (Vector2){200, buttonH}))
692 {
693 nextScene = GAME_SCENE_SETTINGS;
694 }
695 }
696 #if defined(PLATFORM_WEB)
697 #include <emscripten.h>
698
699 // Function to store data in Local Storage
700 void StoreData(const char *key, const char *value) {
701 EM_ASM_({
702 localStorage.setItem(UTF8ToString($0), UTF8ToString($1));
703 }, key, value);
704 }
705
706 // Function to retrieve data from Local Storage
707 const char* RetrieveData(const char *key) {
708 return (const char*)EM_ASM_INT({
709 var value = localStorage.getItem(UTF8ToString($0));
710 if (value === null) {
711 return 0;
712 }
713 var lengthBytes = lengthBytesUTF8(value) + 1;
714 var stringOnWasmHeap = _malloc(lengthBytes);
715 stringToUTF8(value, stringOnWasmHeap, lengthBytes);
716 return stringOnWasmHeap;
717 }, key);
718 }
719 #else
720 void StoreData(const char *key, const char *value) {}
721 const char* RetrieveData(const char *key) { return 0; }
722 #endif
723
724 uint32_t RetrieveUInt32(const char *key)
725 {
726 const char *value = RetrieveData(key);
727 if (value)
728 {
729 uint32_t result = atoi(value);
730 free((void*)value);
731 return result;
732 }
733 return 0;
734 }
735
736 void StoreUInt32(const char *key, uint32_t value)
737 {
738 char buffer[16];
739 sprintf(buffer, "%d", value);
740 StoreData(key, buffer);
741 }
742
743 void RetrieveFixedChar(const char *key, char *buffer, int size)
744 {
745 const char *value = RetrieveData(key);
746 if (value)
747 {
748 strncpy(buffer, value, size);
749 free((void*)value);
750 }
751 else
752 {
753 buffer[0] = '\0';
754 }
755 }
756
757 void StoreFixedChar(const char *key, const char *value)
758 {
759 StoreData(key, value);
760 }
761
762
763 void LoadStorage()
764 {
765 playSounds = RetrieveUInt32("storage.playSounds");
766 // ignore version as this is first version. Upgrades need to be backwards compatible
767 for (int i = 0; i < 4; i++)
768 {
769 for (int j = 0; j < 8; j++)
770 {
771 char key[64];
772 sprintf(key, "storage.highscore_%d_%d", i, j);
773 storage.scores[i].highscores[j].score = RetrieveUInt32(key);
774 sprintf(key, "storage.name_%d_%d", i, j);
775 RetrieveFixedChar(key, storage.scores[i].highscores[j].name, 16);
776 sprintf(key, "storage.date_%d_%d", i, j);
777 RetrieveFixedChar(key, storage.scores[i].highscores[j].date, 16);
778 }
779 }
780
781 storage.tutorialStep = RetrieveUInt32("storage.tutorialStep");
782 storage.tutorialStepCount = RetrieveUInt32("storage.tutorialStepCount");
783 storage.tutorialCompleted = RetrieveUInt32("storage.tutorialCompleted");
784 storage.startups = RetrieveUInt32("storage.startups");
785 StoreUInt32("storage.startups", storage.startups + 1);
786 }
787
788 void SaveStorage()
789 {
790 StoreUInt32("storage.playSounds", playSounds);
791 StoreUInt32("storage.version", 1);
792 for (int i = 0; i < 4; i++)
793 {
794 for (int j = 0; j < 8; j++)
795 {
796 char key[64];
797 sprintf(key, "storage.highscore_%d_%d", i, j);
798 StoreUInt32(key, storage.scores[i].highscores[j].score);
799 sprintf(key, "storage.name_%d_%d", i, j);
800 StoreFixedChar(key, storage.scores[i].highscores[j].name);
801 sprintf(key, "storage.date_%d_%d", i, j);
802 StoreFixedChar(key, storage.scores[i].highscores[j].date);
803 }
804 }
805 StoreUInt32("storage.tutorialStep", storage.tutorialStep);
806 StoreUInt32("storage.tutorialStepCount", storage.tutorialStepCount);
807 StoreUInt32("storage.tutorialCompleted", storage.tutorialCompleted);
808 }
809
810 void LogStorage()
811 {
812 TraceLog(LOG_INFO, "[storage] Storage log");
813 for (int i = 0; i < 4; i++)
814 {
815 for (int j = 0; j < 8; j++)
816 {
817 TraceLog(LOG_INFO, "[storage] Highscore %d %d: %d %s %s", i, j, storage.scores[i].highscores[j].score,
818 storage.scores[i].highscores[j].name, storage.scores[i].highscores[j].date);
819 }
820 }
821 TraceLog(LOG_INFO, "[storage] Tutorial: %d %d %d", storage.tutorialStep, storage.tutorialStepCount, storage.tutorialCompleted);
822 TraceLog(LOG_INFO, "[storage] Startups: %d", storage.startups);
823 }
824
825 #include <time.h>
826 void StoreHighScore(PlayfieldScores scores)
827 {
828 DifficultyScores *difficultyScores = &storage.scores[gameDifficulty];
829
830 for (int i = 0; i < 8; i++)
831 {
832 if (scores.score > difficultyScores->highscores[i].score)
833 {
834 for (int j = 7; j > i; j--)
835 {
836 difficultyScores->highscores[j] = difficultyScores->highscores[j - 1];
837 }
838 difficultyScores->highscores[i].score = scores.score;
839 time_t now = time(0);
840 struct tm *tm = localtime(&now);
841 strftime(difficultyScores->highscores[i].date, 16, "%Y-%m-%d", tm);
842 difficultyScores->highscores[i].name[0] = '\0';
843 SaveStorage();
844 break;
845 }
846 }
847 }
848
849 void TutorialBubble(int step, const char *text, int x, int y, int width, int height)
850 {
851 if (playfield.tutorialStep != step) return;
852 Rectangle rect = {x, y, width, height};
853 DrawTextureNPatch(texUiAtlas, nPatchButton, rect, Vector2Zero(), 0.0f, COLOR_FROM_HEX(0xcceeff));
854
855 Font font = fontMainMedium;
856 int lineHeight = font.baseSize - 4;
857 int lineCount = 1;
858 for (int i = 0; i < strlen(text); i++)
859 {
860 if (text[i] == '\n')
861 {
862 lineCount++;
863 }
864 }
865 int yTop = y;
866 float textHeight = lineCount * lineHeight;
867 const char *line = text;
868 y = y + height * 0.5f - textHeight * 0.5f;
869 while (*line) {
870 char lineBuffer[256];
871 const char *end = line;
872 while (*end && *end != '\n') {
873 lineBuffer[end - line] = *end;
874 end++;
875 }
876 lineBuffer[end - line] = '\0';
877
878 Vector2 tsize = MeasureTextEx(font, lineBuffer, font.baseSize, 1);
879 DrawTextEx(font, lineBuffer, (Vector2){ x + width * 0.5f - tsize.x * 0.5f, y }, font.baseSize, 1, BLACK);
880
881 line = end;
882 while (*line == '\n') line++;
883 y += lineHeight;
884 }
885
886 DrawTextureRec(texUiAtlas, rectUiAtlasCircle, (Vector2){x + width - 30, yTop}, COLOR_FROM_HEX(0xccddff));
887 DrawTextureRec(texUiAtlas, rectUiAtlasShrimpy, (Vector2){x + width, yTop + 10}, WHITE);
888 }
889
890 void TutorialSetClicksBlocked(int step, int blocked)
891 {
892 if (playfield.tutorialStep != step) return;
893 isClickActionBlocked = blocked;
894 }
895
896 void TutorialProceedOnClick(int step)
897 {
898 if (playfield.tutorialStep != step) return;
899 if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) {
900 playfield.nextTutorialStep = step + 1;
901 }
902 }
903
904 void TutorialHighlightCircle(int step, int x, int y, int radius)
905 {
906 if (playfield.tutorialStep != step) return;
907 float pulse = sinf(gameTime.time * 10.0f) * 0.25f + 1.0f;
908 DrawCircleLines(x, y, radius * pulse, COLOR_FROM_HEX(0xff0000));
909 DrawCircleLines(x, y, radius * pulse + 0.5f, COLOR_FROM_HEX(0xff0000));
910 DrawCircleLines(x, y, radius * pulse + 1.0f, COLOR_FROM_HEX(0xff0000));
911 }
912
913 void TutorialSpawnBubble(int step, int x, int y, int level, int type, int nextLevel, int nextType)
914 {
915 if (playfield.tutorialStep != step || !playfield.tutorialStepStart) return;
916 Vector2 pos = PlayfieldScreenToSpawnPosition(&playfield, camera,
917 (Vector2){x, y});
918 playfield.nextBubbleLevel = level;
919 playfield.nextBubbleType = type;
920 PlayfieldTryAddBubble(&playfield, pos);
921 playfield.nextBubbleLevel = nextLevel;
922 playfield.nextBubbleType = nextType;
923 }
924
925 void UpdateTutorialSystem_PlayTutorial()
926 {
927 int step = 0;
928 int boxHeight = 105;
929 TutorialSetClicksBlocked(step, 1);
930 TutorialBubble(step, "Welcome to my fishtank...", 10, 10, 600, boxHeight);
931 TutorialProceedOnClick(step);
932 step++;
933
934 TutorialSetClicksBlocked(step, 1);
935 TutorialBubble(step, "It is very boring here.\nSo I invented this little game:", 10, 10, 600, boxHeight);
936 TutorialProceedOnClick(step);
937 step++;
938
939 TutorialSetClicksBlocked(step, 1);
940 TutorialBubble(step, "Please tap on the marked area ...", 10, 10, 600, boxHeight);
941 TutorialProceedOnClick(step);
942 TutorialHighlightCircle(step, GetScreenWidth() / 2, GetScreenHeight() - 50, 20);
943 step++;
944
945 TutorialSpawnBubble(step, GetScreenWidth() / 2, GetScreenHeight() - 50, 1, 0, 1, 0);
946 TutorialBubble(step, "Very good! Now do it again!", 10, 10, 600, boxHeight);
947 TutorialProceedOnClick(step);
948 TutorialHighlightCircle(step, GetScreenWidth() / 2, GetScreenHeight() - 50, 20);
949 TutorialSetClicksBlocked(step, 1);
950 step++;
951
952 TutorialSpawnBubble(step, GetScreenWidth() / 2, GetScreenHeight() - 50, 1, 0, 2, 1);
953 TutorialBubble(step,
954 "These two bubbles have the same size and\n"
955 "color. When they touch, they will merge.", 10, 10, 600, boxHeight);
956 TutorialProceedOnClick(step);
957 TutorialSetClicksBlocked(step, 1);
958 step++;
959
960 TutorialBubble(step, "But currently, they don't :(\nLet me help you a little.", 10, 10, 600, boxHeight);
961 TutorialProceedOnClick(step);
962 TutorialSetClicksBlocked(step, 1);
963 step++;
964 TutorialBubble(step, "...", 10, 10, 600, boxHeight);
965 if (step == playfield.tutorialStep) {
966 playfield.enableTutorialMerge = 1;
967 if (playfield.bubbles[0].bubbleLevel == 2)
968 {
969 playfield.nextTutorialStep = step + 1;
970 }
971 }
972 step++;
973 TutorialBubble(step, "See? The two bubbles merged and now\nit is bigger - look and the two dots it got.", 10, 10, 600, boxHeight);
974 TutorialProceedOnClick(step);
975 TutorialSetClicksBlocked(step, 1);
976 step++;
977 TutorialBubble(step, "Oh, the next bubble has a different color.\nTap again!", 10, 10, 600, boxHeight);
978 TutorialProceedOnClick(step);
979 TutorialHighlightCircle(step, GetScreenWidth() / 2, GetScreenHeight() - 50, 20);
980 TutorialSetClicksBlocked(step, 1);
981
982 step++;
983 TutorialSpawnBubble(step, GetScreenWidth() / 2, GetScreenHeight() - 50, 2, 1, 2, 1);
984 TutorialBubble(step, "It won't merge with the bright bubble.\nI don't know why.", 10, 10, 600, boxHeight);
985 TutorialProceedOnClick(step);
986
987 step++;
988 TutorialBubble(step, "But look! We have another bubble of \nthe same color! Release it!", 10, 10, 600, boxHeight);
989 TutorialProceedOnClick(step);
990 TutorialHighlightCircle(step, GetScreenWidth() / 2, GetScreenHeight() - 50, 20);
991 step++;
992 TutorialSpawnBubble(step, GetScreenWidth() / 2, GetScreenHeight() - 50, 2, 1, 2, 1);
993 TutorialBubble(step, "Wait for it...", 10, 10, 600, boxHeight);
994 if (step == playfield.tutorialStep) {
995 playfield.enableTutorialMerge = 1;
996 if (playfield.bubbles[1].bubbleLevel == 3)
997 {
998 playfield.nextTutorialStep = step + 1;
999 }
1000 }
1001
1002 step++;
1003 TutorialBubble(step, "Just watch out: When the bubbles go too high\nor low, they all pop :(", 10, 10, 600, boxHeight);
1004 TutorialProceedOnClick(step);
1005
1006 step++;
1007 TutorialBubble(step, "I love these bubbles!\nGo and try to see how many you can get!", 10, 10, 600, boxHeight);
1008 TutorialProceedOnClick(step);
1009 if (step == playfield.tutorialStep) {
1010 playfield.enableTutorialMerge = 0;
1011 }
1012
1013 step++;
1014 TutorialSetClicksBlocked(step, 0);
1015
1016 if (playfield.strikes > 0 && playfield.tutorialStep == step)
1017 {
1018 playfield.nextTutorialStep = step + 1;
1019 }
1020 step++;
1021
1022 TutorialBubble(step, "Oh no! The bubbles are about to pop!\nCareful, you have 2 more tries!", 10, 10, 600, boxHeight);
1023 TutorialProceedOnClick(step);
1024 TutorialSetClicksBlocked(step, 1);
1025 step++;
1026 TutorialSetClicksBlocked(step, 0);
1027 }
1028
1029 void UpdateTutorialSystem()
1030 {
1031 if (currentScene == GAME_SCENE_PLAY && gameDifficulty == GAME_DIFFICULTY_TUTORIAL)
1032 {
1033 UpdateTutorialSystem_PlayTutorial();
1034 }
1035
1036 if (playfield.nextTutorialStep != playfield.tutorialStep)
1037 {
1038 playfield.tutorialStepStart = 1;
1039 }
1040 else
1041 {
1042 playfield.tutorialStepStart = 0;
1043 }
1044 playfield.tutorialStep = playfield.nextTutorialStep;
1045 }
1046
1047 // out of time
1048 // void DrawWaterPlant(float x, float y, float height, Color color)
1049 // {
1050 // Vector2 lineStrip[90];
1051 // float yStep = height / 30;
1052 // for (int i = 0; i < 45; i++)
1053 // {
1054 // float t = i / 45.0f;
1055 // float w = powf(t, 0.6f) * height / 90.0f;
1056 // float offset = sinf(gameTime.time * 2.0f + i * 0.1f) * 2.0f;
1057 // lineStrip[i * 2] = (Vector2){x + w + offset, y - i * yStep};
1058 // lineStrip[i * 2 + 1] = (Vector2){x - w + offset, y - i * yStep};
1059 // }
1060 // DrawTriangleStrip(lineStrip, 90, color);
1061 // }
1062
1063 int main(void)
1064 {
1065 // Initialization
1066 //--------------------------------------------------------------------------------------
1067 const int screenWidth = 800;
1068 const int screenHeight = 450;
1069
1070 InitWindow(screenWidth, screenHeight, "Shrimpy Bubble Merge");
1071 InitAudioDevice();
1072
1073 SetTargetFPS(60); // Set our game to run at 60 frames-per-second
1074 //--------------------------------------------------------------------------------------
1075
1076 TraceLog(LOG_INFO, "loading shaders");
1077 bubbleOutlineShader = LoadShader(0, "data/bubble_outline.fs");
1078 // Get shader locations
1079 outlineSizeLoc = GetShaderLocation(bubbleOutlineShader, "outlineSize");
1080 outlineColorLoc = GetShaderLocation(bubbleOutlineShader, "outlineColor");
1081 textureSizeLoc = GetShaderLocation(bubbleOutlineShader, "textureSize");
1082
1083 LoadStorage();
1084 LogStorage();
1085
1086 texShrimpy = LoadTexture("data/shrimpy.png");
1087 texLeftCorner = LoadTexture("data/left-corner.png");
1088 texUiAtlas = LoadTexture("data/ui-atlas.png");
1089 texGround = LoadTexture("data/ground.png");
1090 fontMainMedium = LoadFontEx("data/IndieFlower-Regular.ttf", 40, 0, 0);
1091 fontMainLarge = LoadFontEx("data/IndieFlower-Regular.ttf", 70, 0, 0);
1092 sfxPop = LoadSound("data/pop1.wav");
1093 bubbleModel = LoadModelFromMesh(GenMeshSphere(1.0f, 4, 24));
1094
1095 Vector2 floatingBubbles[32];
1096 Vector2 floatingBubblesVelocities[32];
1097 for (int i = 0; i < 32; i++)
1098 {
1099 floatingBubbles[i] = (Vector2){GetRandomValue(0, screenWidth), GetRandomValue(0, screenHeight)};
1100 floatingBubblesVelocities[i] = (Vector2){GetRandomValue(-100, 100) * 0.1f, GetRandomValue(-100, 100) * 0.1f};
1101 }
1102 // Main game loop
1103 while (!WindowShouldClose()) // Detect window close button or ESC key
1104 {
1105 if (bubbleFieldTexture.texture.width != GetScreenWidth() || bubbleFieldTexture.texture.height != GetScreenHeight())
1106 {
1107 UnloadRenderTexture(bubbleFieldTexture);
1108 bubbleFieldTexture = LoadRenderTexture(GetScreenWidth(), GetScreenHeight());
1109 }
1110
1111 float dt = GetFrameTime();
1112 // clamp dt to prevent large time steps, e.g. when browser tab is inactive
1113 if (dt > 0.2f) dt = 0.2f;
1114 gameTime.time += dt;
1115 gameTime.deltaTime = dt;
1116
1117 BeginDrawing();
1118
1119 DrawTexture(texLeftCorner, 0, GetScreenHeight() - texLeftCorner.height - 16, WHITE);
1120
1121 float decay = expf(-dt * 2.0f);
1122 for (int i=0; i < 32; i++)
1123 {
1124 DrawCircle(floatingBubbles[i].x, floatingBubbles[i].y, i %4 + 2, ColorLerp(WHITE, COLOR_FROM_HEX(0x4488cc), sinf(gameTime.time + i * 0.1f) * 0.5f + 0.5f));
1125 floatingBubbles[i] = Vector2Add(floatingBubbles[i], Vector2Scale(floatingBubblesVelocities[i], dt));
1126 floatingBubblesVelocities[i] = Vector2Add(floatingBubblesVelocities[i], (Vector2){GetRandomValue(-100,100) * 0.05f, -10.0f * dt});
1127 floatingBubblesVelocities[i] = Vector2Scale(floatingBubblesVelocities[i], decay);
1128 if (floatingBubbles[i].x < 0 || floatingBubbles[i].x > GetScreenWidth()
1129 || floatingBubbles[i].y < 0 || floatingBubbles[i].y > GetScreenHeight())
1130 {
1131 floatingBubbles[i].x = GetRandomValue(0, GetScreenWidth());
1132 floatingBubbles[i].y = GetRandomValue(0, GetScreenHeight());
1133 floatingBubblesVelocities[i].x = GetRandomValue(-100, 100) * 0.1f;
1134 floatingBubblesVelocities[i].y = GetRandomValue(-100, 100) * 0.1f;
1135 }
1136 }
1137
1138 int y = GetScreenHeight() - texGround.height;
1139 for (int x=0; x<screenWidth; x+=texGround.width)
1140 {
1141 DrawTexture(texGround, x, y, WHITE);
1142 }
1143
1144 // draw wavy water line at top
1145 Vector2 waterLine[80];
1146 float waterXStep = GetScreenWidth() / 38;
1147 for (int i = 0; i < 40; i++)
1148 {
1149 waterLine[i * 2] = (Vector2){i * waterXStep, 0};
1150 waterLine[i * 2 + 1] = (Vector2){i * waterXStep, 15
1151 + sinf(gameTime.time * 2.9f + waterLine[i * 2 + 1].x * 0.008f) * 2.0f
1152 + sinf(gameTime.time * 4.3f + waterLine[i * 2 + 1].x * 0.015f) * 1.0f
1153 + sinf(gameTime.time * 6.9f + waterLine[i * 2 + 1].x * 0.032f) * 0.5f
1154 };
1155 }
1156 DrawTriangleStrip(waterLine, 80, COLOR_FROM_HEX(0x88ccff));
1157
1158 for (int i = 0; i < 40; i++)
1159 {
1160 float x = i * waterXStep;
1161 waterLine[i * 2 + 1] = (Vector2){x, 16
1162 + sinf(gameTime.time * 2.9f + x * 0.008f) * 2.0f
1163 + sinf(gameTime.time * 4.3f + x * 0.015f) * 1.0f
1164 + sinf(gameTime.time * 6.9f + x * 0.032f) * 0.5f
1165 };
1166 waterLine[i * 2] = (Vector2){x, waterLine[i * 2 + 1].y - 2.0f};
1167 }
1168 DrawTriangleStrip(waterLine, 80, COLOR_FROM_HEX(0xeef8ff));
1169
1170 // DrawWaterPlant(50, GetScreenHeight() - 20, 130, COLOR_FROM_HEX(0x449955));
1171
1172
1173
1174 switch (currentScene)
1175 {
1176 default: UpdateSceneMenu(); break;
1177 case GAME_SCENE_SETTINGS: UpdateSceneSettings(); break;
1178 case GAME_SCENE_PLAY: UpdateScenePlay(); break;
1179 case GAME_SCENE_GAMEOVER: UpdateSceneGameOver(); break;
1180 }
1181
1182 UpdateTutorialSystem();
1183
1184 switch (nextScene)
1185 {
1186 case GAME_SCENE_NONE: break;
1187 default: currentScene = nextScene; break;
1188 case GAME_SCENE_GAMEOVER:
1189 StoreHighScore(CalculatePlayfieldScores(&playfield));
1190 currentScene = GAME_SCENE_GAMEOVER;
1191 break;
1192 case GAME_SCENE_PLAY:
1193 playfield = (Playfield){
1194 .fieldSize = {230, 300},
1195 .waterLevel = 200.0f,
1196 .minHeight = 90.0f,
1197 .maxHeight = 280.0f,
1198 };
1199 if (gameDifficulty == GAME_DIFFICULTY_TUTORIAL)
1200 {
1201 playfield.waterLevel = 180.0f;
1202 }
1203 currentScene = GAME_SCENE_PLAY; break;
1204 }
1205
1206 if (nextScene != GAME_SCENE_NONE)
1207 {
1208 gameTime.fixedTime = gameTime.time;
1209 isClickActionBlocked = 0;
1210 playfield.tutorialStep = 0;
1211 playfield.nextTutorialStep = 0;
1212 }
1213
1214 nextScene = GAME_SCENE_NONE;
1215 EndDrawing();
1216 }
1217
1218 // De-Initialization
1219 //--------------------------------------------------------------------------------------
1220 CloseWindow(); // Close window and OpenGL context
1221 //--------------------------------------------------------------------------------------
1222
1223 return 0;
1224 }
1 #ifndef __BPOP_MAIN_H__
2 #define __BPOP_MAIN_H__
3
4 #include "raylib.h"
5 #include "raymath.h"
6 #include "rlgl.h"
7 #include <stdint.h>
8 #include <math.h>
9
10 typedef enum GameDifficulty
11 {
12 GAME_DIFFICULTY_TUTORIAL,
13 GAME_DIFFICULTY_EASY,
14 GAME_DIFFICULTY_NORMAL,
15 GAME_DIFFICULTY_HARD,
16 } GameDifficulty;
17
18 typedef struct HighscoreEntry
19 {
20 uint32_t score;
21 char name[16];
22 char date[16];
23 } HighscoreEntry;
24
25 typedef struct DifficultyScores
26 {
27 HighscoreEntry highscores[8];
28 } DifficultyScores;
29
30
31 typedef struct Storage
32 {
33 uint32_t version;
34 uint32_t startups;
35 DifficultyScores scores[4];
36 uint32_t tutorialStep;
37 uint32_t tutorialStepCount;
38 uint32_t tutorialCompleted;
39 } Storage;
40
41 typedef enum GameScene
42 {
43 GAME_SCENE_NONE,
44 GAME_SCENE_MENU,
45 GAME_SCENE_DIFFICULTY_SELECT,
46 GAME_SCENE_PLAY,
47 GAME_SCENE_GAMEOVER,
48 GAME_SCENE_HIGHSCORES,
49 GAME_SCENE_SETTINGS,
50 } GameScene;
51
52
53 #define MAX_BUBBLE_TYPES 3
54
55 typedef struct Bubble
56 {
57 uint8_t flagIsActive:1;
58 uint8_t sameLevelContact:1;
59 uint8_t isOutsideZone:1;
60 uint8_t bubbleType:3;
61 uint8_t bubbleLevel;
62
63 Vector2 position;
64 Vector2 velocity;
65 float bubbleMergeCooldown;
66 float radius;
67 float lifeTime;
68 } Bubble;
69
70 #define MAX_BUBBLES 64
71 #define MAX_STRIKES 3
72 #define GAMEOVER_COOLDOWN 2.0f
73
74 typedef struct PlayfieldScores
75 {
76 uint8_t bubbleCount;
77 uint8_t outsideBubbleCount;
78 uint32_t score;
79 } PlayfieldScores;
80
81 typedef struct Playfield
82 {
83 Bubble bubbles[MAX_BUBBLES];
84 Bubble poppingBubbles[MAX_BUBBLES];
85 Matrix transform;
86 Vector2 fieldSize;
87 float waterLevel;
88 float maxHeight;
89 float minHeight;
90 uint8_t comboPop;
91 uint8_t comboPopPoints;
92 uint8_t nextBubbleLevel;
93 uint8_t nextBubbleType;
94 // every placed bubble when a bubble is outside the zone counts
95 // as a strike, 3 strikes and it's game over. Strikes are
96 // reset when all bubbles are inside the zone.
97 uint8_t strikes;
98 uint8_t tutorialStepStart;
99 uint8_t enableTutorialMerge;
100 float gameoverCooldown;
101 uint32_t spawnedBubbleCount;
102 uint32_t tutorialStep;
103 uint32_t nextTutorialStep;
104 } Playfield;
105
106 typedef struct GameTime
107 {
108 float time;
109 float deltaTime;
110 float fixedTime;
111 float fixedDeltaTime;
112 } GameTime;
113
114 #define COLOR_FROM_HEX(hexValue) (Color){((hexValue) >> 16) & 0xFF, ((hexValue) >> 8) & 0xFF, (hexValue) & 0xFF, 0xFF}
115 #endif
Todos:
- [x] Game Over Screen popping bubbles
- [x] Bubble Pop Sound
After the game jam: Retrospective
As always, the final hours were quite hectic. I got a couple of things polished. The game was received very well and in the final votes, I got a 1st place in the category "Theme" and 2nd place in the category gameplay. What I am most proud of is however was how many people kept playing the game for quite a while - and moreover, that there were no bugs, besides issues on mobile devices and a problem with mapping mouse/touch inputs to the screen when the game runs in fullscreen mode. This is however a raylib issue. Maybe I should investigate this and see if I can fix it.
I added the game to the projects section, you can find it here.
The global game jam link is here.
Of all the game jams I have participated in, I think this was the most finished game and coming closest to the theme's topic.
Using raylib worked pretty well, just like the blog system I have set up. Being able to pass a link to other participants is great. I think it also helped me to focus on the game play and not on technical or graphical aspects early on. In the final hours, I would have loved to have some authoring tools for integrating effects, but I think I still managed to get a lot in and the limitations forced me to be creative.
What I found interesting: I used copilot and in most cases its contributions were rather minor. But in some cases, the suggestions were quite useful nonetheless.
When I didn't get audio to work, I asked for help and got the hint that I need to call the InitAudioDevice function, which I forgot about.
Bugwise there was not really much, not even during development: The initial physics implementation for the bubbles kept me busy for a few hours, mostly because signs were wrong. Another trouble giver was the shader for the bubble outlines. It didn't work correctly on mobile and the reason was the precision mode of the shader. After setting it to highp, it worked.
The structure of the game is following a common pattern that works pretty well:
- A game state id that is used to like a scene id (main menu, game play, settings, etc.)
- Update handlers for each state, that are called from the main loop.
- A "nextGameState" variable, that is used to queue which state to switch to. it is evaluated at the end of the loop.
- A tutorial stepping system
- No dynamic memory allocation. Pointers only point to static memory addresses.
- Fixed size arrays for everything
In total, the entire source and header file is only 1.3k lines of code. That is not really much.
Another thing I've learned: I can use my Android tablet as an external monitor and even use its touch and pen input. I drew the artworks this way and it was pretty nice since the pen for that tablet is more precise than the stylus I have for my notebook.
What was also nice: I had all game states and scoring system in the game by yesterday afternoon. That gave me a lot of time to polish and add test the game.
Surprising: I started out with a plain copy of Cosmic Collapse and the idea to just retheming it. But over time, I changed some behavior of the physics, so by now, I think it differs quite a bit from my inspiration game. The waterline where the buyancy changes makes the game feel quite different in my opinion. Usually I am very reluctant to copy mechanics from other games during a game jam, but maybe it is actually useful to have a good starting point. I think I would try this again in the future.
As for what to do with the result: I think the game is fairly complete. There are several things I can think of that would be nice to have:
- Gaining a special action every 100 points that allows to pop a bubble of any size and color anywhere on the screen.
- When getting combos, a visual effect could show that the merger was a combo ("Combo x2", "Combo x3", etc.)
- A preview of the next bubble that will be spawned
- Suggestion by a player: Earn wearables for the shrimp
I think once these are in, I could upload it to browser gaming sites and see how it performs.