comparison color_game/main.c @ 60:d64a8c189a77

Merged
author June Park <me@mrjunejune.com>
date Sat, 20 Dec 2025 13:56:01 -0500
parents e06bc03d9618
children 9df5587cf23b
comparison
equal deleted inserted replaced
50:983769fba767 60:d64a8c189a77
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <time.h>
4 #include "dowa/dowa.h"
5 #include "third_party/raylib/include/raylib.h"
6
7 #define INIT_SCREEN_WIDTH 1200
8 #define INIT_SCREEN_HEIGHT 700
9 #define PLAYER_SPEED 200.0f
10 #define PLAYER_RADIUS 20.0f
11 #define MONSTER_SPEED 100.0f
12 #define MONSTER_RADIUS 15.0f
13 #define BULLET_SPEED 300.0f
14 #define BULLET_RADIUS 5.0f
15 #define BULLET_LIFETIME 3.0f
16 #define SHOOT_INTERVAL 0.5f
17 #define HIT_PROBABILITY 0.7f // 70% aim accuracy (0.0 = random, 1.0 = perfect)
18 #define MAX_PASSIVE_NODES 12
19 #define PASSIVE_TREE_RADIUS 250.0f
20 #define PASSIVE_NODE_RADIUS 20.0f
21 #define BASE_DAMAGE 10.0f
22 #define PLAYER_MAX_HEALTH 100.0f
23 #define MONSTER_CONTACT_DAMAGE 5.0f
24 #define BOSS_HEALTH_MULTIPLIER 5.0f
25 #define COLOR_UNLOCK_TIME 30.0f
26 #define BOSS_SPAWN_TIME 60.0f
27
28 typedef struct Player {
29 Vector2 position;
30 Color color;
31 float health;
32 float maxHealth;
33 int unlockedBulletTypes[MAX_PASSIVE_NODES];
34 int unlockedCount;
35 int passivePoints;
36 float invulnerabilityTimer;
37 } Player;
38
39 typedef struct Map {
40 float width;
41 float height;
42 } Map;
43
44 typedef struct Monster {
45 Vector2 position;
46 float hue;
47 float saturation;
48 float health;
49 float maxHealth;
50 bool alive;
51 bool hasCollision;
52 bool isBoss;
53 } Monster;
54
55 typedef struct Bullet {
56 Vector2 position;
57 Vector2 velocity;
58 float lifetime;
59 float hue;
60 float damage;
61 bool active;
62 } Bullet;
63
64 typedef struct PassiveNode {
65 float angle;
66 float hue;
67 char description[64];
68 float damageBonus;
69 bool unlocked;
70 } PassiveNode;
71
72 float RandomFloat(float min, float max)
73 {
74 return min + ((float)rand() / (float)RAND_MAX) * (max - min);
75 }
76
77 float CalculateColorDamage(float bulletHue, float monsterHue, float baseDamage)
78 {
79 // Calculate hue difference (0-180 degrees)
80 float diff = fabs(bulletHue - monsterHue);
81 if (diff > 180.0f) diff = 360.0f - diff;
82
83 // Opposite colors (180 degrees) = 2x damage
84 // Same color (0 degrees) = 0.5x damage
85 // Linear scale between them
86 float damageMultiplier = 0.5f + (diff / 180.0f) * 1.5f;
87
88 return baseDamage * damageMultiplier;
89 }
90
91 void InitializePassiveTree(PassiveNode* nodes)
92 {
93 const char* nodeDescriptions[MAX_PASSIVE_NODES] = {
94 "Red Bullet", // 0°
95 "Orange Bullet", // 30°
96 "Yellow Bullet", // 60°
97 "Chartreuse", // 90°
98 "Green Bullet", // 120°
99 "Spring Green", // 150°
100 "Cyan Bullet", // 180°
101 "Azure Bullet", // 210°
102 "Blue Bullet", // 240°
103 "Violet Bullet", // 270°
104 "Magenta Bullet", // 300°
105 "Rose Bullet" // 330°
106 };
107
108 for (int i = 0; i < MAX_PASSIVE_NODES; i++)
109 {
110 nodes[i].angle = (360.0f / MAX_PASSIVE_NODES) * i;
111 nodes[i].hue = nodes[i].angle;
112 snprintf(nodes[i].description, sizeof(nodes[i].description), "%s", nodeDescriptions[i]);
113 nodes[i].damageBonus = 5.0f + (i * 2.0f); // Scaling damage bonus
114 nodes[i].unlocked = false;
115 }
116 }
117
118 void HandlePlayerMovement(Player* player, Map* map, float deltaTime)
119 {
120 Vector2 movement = {0};
121
122 if (IsKeyDown(KEY_W) || IsKeyDown(KEY_UP)) movement.y -= 1.0f;
123 if (IsKeyDown(KEY_S) || IsKeyDown(KEY_DOWN)) movement.y += 1.0f;
124 if (IsKeyDown(KEY_A) || IsKeyDown(KEY_LEFT)) movement.x -= 1.0f;
125 if (IsKeyDown(KEY_D) || IsKeyDown(KEY_RIGHT)) movement.x += 1.0f;
126
127 // Normalize diagonal movement
128 float length = sqrtf(movement.x * movement.x + movement.y * movement.y);
129 if (length > 0)
130 {
131 movement.x /= length;
132 movement.y /= length;
133 }
134
135 // Apply movement
136 player->position.x += movement.x * PLAYER_SPEED * deltaTime;
137 player->position.y += movement.y * PLAYER_SPEED * deltaTime;
138
139 // Keep player within map bounds
140 if (player->position.x < PLAYER_RADIUS) player->position.x = PLAYER_RADIUS;
141 if (player->position.x > map->width - PLAYER_RADIUS)
142 player->position.x = map->width - PLAYER_RADIUS;
143 if (player->position.y < PLAYER_RADIUS) player->position.y = PLAYER_RADIUS;
144 if (player->position.y > map->height - PLAYER_RADIUS)
145 player->position.y = map->height - PLAYER_RADIUS;
146 }
147
148 void UpdateMonsters(Monster* monsters, int monsterCount, Vector2 playerPos, float deltaTime)
149 {
150 for (int i = 0; i < monsterCount; i++)
151 {
152 if (!monsters[i].alive) continue;
153
154 // Calculate direction to player
155 Vector2 direction = {
156 playerPos.x - monsters[i].position.x,
157 playerPos.y - monsters[i].position.y
158 };
159
160 // Normalize direction
161 float length = sqrtf(direction.x * direction.x + direction.y * direction.y);
162 if (length > 0)
163 {
164 direction.x /= length;
165 direction.y /= length;
166 }
167
168 // Move toward player
169 Vector2 newPos = {
170 monsters[i].position.x + direction.x * MONSTER_SPEED * deltaTime,
171 monsters[i].position.y + direction.y * MONSTER_SPEED * deltaTime
172 };
173
174 // Check collision with other monsters if this monster has collision enabled
175 if (monsters[i].hasCollision)
176 {
177 bool collision = false;
178 for (int j = 0; j < monsterCount; j++)
179 {
180 if (i == j || !monsters[j].alive || !monsters[j].hasCollision) continue;
181
182 float dx = newPos.x - monsters[j].position.x;
183 float dy = newPos.y - monsters[j].position.y;
184 float distance = sqrtf(dx * dx + dy * dy);
185
186 if (distance < MONSTER_RADIUS * 2)
187 {
188 collision = true;
189 break;
190 }
191 }
192
193 if (!collision)
194 {
195 monsters[i].position = newPos;
196 }
197 }
198 else
199 {
200 monsters[i].position = newPos;
201 }
202 }
203 }
204
205 void CheckPlayerMonsterCollision(Player* player, Monster* monsters, int monsterCount, float deltaTime)
206 {
207 if (player->invulnerabilityTimer > 0)
208 {
209 player->invulnerabilityTimer -= deltaTime;
210 return;
211 }
212
213 for (int i = 0; i < monsterCount; i++)
214 {
215 if (!monsters[i].alive) continue;
216
217 float dx = player->position.x - monsters[i].position.x;
218 float dy = player->position.y - monsters[i].position.y;
219 float distance = sqrtf(dx * dx + dy * dy);
220
221 if (distance < PLAYER_RADIUS + MONSTER_RADIUS)
222 {
223 player->health -= MONSTER_CONTACT_DAMAGE;
224 player->invulnerabilityTimer = 0.5f; // 0.5 second invulnerability
225 if (player->health < 0) player->health = 0;
226 break;
227 }
228 }
229 }
230
231 void UpdateBullets(Bullet* bullets, int bulletCount, float deltaTime)
232 {
233 for (int i = 0; i < bulletCount; i++)
234 {
235 if (!bullets[i].active) continue;
236
237 // Update position
238 bullets[i].position.x += bullets[i].velocity.x * deltaTime;
239 bullets[i].position.y += bullets[i].velocity.y * deltaTime;
240
241 // Update lifetime
242 bullets[i].lifetime -= deltaTime;
243 if (bullets[i].lifetime <= 0)
244 {
245 bullets[i].active = false;
246 }
247 }
248 }
249
250 int CheckCollisions(Bullet* bullets, int bulletCount, Monster* monsters, int monsterCount, Player* player)
251 {
252 int bossesKilled = 0;
253
254 for (int i = 0; i < bulletCount; i++)
255 {
256 if (!bullets[i].active) continue;
257
258 for (int j = 0; j < monsterCount; j++)
259 {
260 if (!monsters[j].alive) continue;
261
262 // Check distance between bullet and monster
263 float dx = bullets[i].position.x - monsters[j].position.x;
264 float dy = bullets[i].position.y - monsters[j].position.y;
265 float distance = sqrtf(dx * dx + dy * dy);
266
267 if (distance < BULLET_RADIUS + MONSTER_RADIUS)
268 {
269 // Calculate damage based on color difference
270 float damage = CalculateColorDamage(bullets[i].hue, monsters[j].hue, bullets[i].damage);
271 monsters[j].health -= damage;
272
273 // Check if monster died
274 if (monsters[j].health <= 0)
275 {
276 monsters[j].alive = false;
277
278 // Award passive point if boss was killed
279 if (monsters[j].isBoss)
280 {
281 player->passivePoints++;
282 bossesKilled++;
283 }
284 }
285
286 bullets[i].active = false;
287 break;
288 }
289 }
290 }
291
292 return bossesKilled;
293 }
294
295 Vector2 FindNearestMonster(Monster* monsters, int monsterCount, Vector2 playerPos)
296 {
297 float minDistance = INFINITY;
298 Vector2 nearestPos = playerPos;
299 bool found = false;
300
301 for (int i = 0; i < monsterCount; i++)
302 {
303 if (!monsters[i].alive) continue;
304
305 float dx = monsters[i].position.x - playerPos.x;
306 float dy = monsters[i].position.y - playerPos.y;
307 float distance = sqrtf(dx * dx + dy * dy);
308
309 if (distance < minDistance)
310 {
311 minDistance = distance;
312 nearestPos = monsters[i].position;
313 found = true;
314 }
315 }
316
317 if (!found)
318 {
319 // No monsters, return random direction
320 float angle = RandomFloat(0.0f, 2.0f * PI);
321 return (Vector2){
322 playerPos.x + cosf(angle) * 100.0f,
323 playerPos.y + sinf(angle) * 100.0f
324 };
325 }
326
327 return nearestPos;
328 }
329
330 int main()
331 {
332 InitWindow(INIT_SCREEN_WIDTH, INIT_SCREEN_HEIGHT, "color game");
333 SetTargetFPS(60);
334 srand(time(NULL));
335
336 // --- All Global Variables ---
337 Map map = {
338 .width = INIT_SCREEN_WIDTH * 3,
339 .height = INIT_SCREEN_HEIGHT * 3
340 };
341
342 // Initialize player at map center
343 Player player = {
344 .position = {map.width / 2, map.height / 2},
345 .color = BLUE,
346 .health = PLAYER_MAX_HEALTH,
347 .maxHealth = PLAYER_MAX_HEALTH,
348 .unlockedCount = 0,
349 .passivePoints = 0,
350 .invulnerabilityTimer = 0.0f
351 };
352
353 // Initialize passive tree
354 PassiveNode passiveTree[MAX_PASSIVE_NODES];
355 InitializePassiveTree(passiveTree);
356
357 // Unlock first node only (grayscale bullet to start)
358 passiveTree[0].unlocked = true;
359 player.unlockedBulletTypes[player.unlockedCount++] = 0;
360
361 // Initialize camera
362 Camera2D camera = {0};
363 camera.target = player.position;
364 camera.offset = (Vector2){INIT_SCREEN_WIDTH / 2.0f, INIT_SCREEN_HEIGHT / 2.0f};
365 camera.rotation = 0.0f;
366 camera.zoom = 1.0f;
367
368 bool menuOpen = false;
369 bool showMonsterColors = false;
370
371 Vector2 menuCenter = {
372 .x = INIT_SCREEN_WIDTH / 2,
373 .y = INIT_SCREEN_HEIGHT / 2
374 };
375
376 // Game progression variables
377 float gameTime = 0.0f;
378 float currentMonsterHue = 0.0f;
379 float currentMonsterSaturation = 0.0f; // Start grayscale
380 int bossesDefeated = 0;
381 float nextBossTime = BOSS_SPAWN_TIME;
382 bool colorUnlocked = false;
383
384 // Initialize monsters array
385 #define MAX_MONSTERS 100
386 Monster live_monsters[MAX_MONSTERS];
387 int monsterCount = 0;
388
389 // Spawn initial monsters (grayscale)
390 for (int i = 0; i < 10; i++)
391 {
392 live_monsters[monsterCount++] = (Monster){
393 .position = {RandomFloat(0, map.width), RandomFloat(0, map.height)},
394 .hue = currentMonsterHue,
395 .saturation = currentMonsterSaturation,
396 .health = 50.0f,
397 .maxHealth = 50.0f,
398 .alive = true,
399 .hasCollision = true,
400 .isBoss = false
401 };
402 }
403
404 // Initialize bullets array
405 #define MAX_BULLETS 200
406 Bullet bullets[MAX_BULLETS];
407 int bulletCount = 0;
408 for (int i = 0; i < MAX_BULLETS; i++)
409 {
410 bullets[i].active = false;
411 }
412
413 // Shooting timer
414 float shootTimer = 0.0f;
415 float hitProbability = HIT_PROBABILITY;
416
417 while (!WindowShouldClose())
418 {
419 float deltaTime = GetFrameTime();
420
421 if (IsKeyPressed(KEY_P))
422 {
423 menuOpen = !menuOpen;
424 }
425
426 if (IsKeyPressed(KEY_R))
427 {
428 showMonsterColors = !showMonsterColors;
429 }
430
431 // Handle passive tree menu interactions
432 if (menuOpen)
433 {
434 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
435 {
436 Vector2 mousePos = GetMousePosition();
437
438 // Check if clicked on a passive node
439 for (int i = 0; i < MAX_PASSIVE_NODES; i++)
440 {
441 float angleRad = passiveTree[i].angle * DEG2RAD;
442 Vector2 nodePos = {
443 menuCenter.x + cosf(angleRad) * PASSIVE_TREE_RADIUS,
444 menuCenter.y + sinf(angleRad) * PASSIVE_TREE_RADIUS
445 };
446
447 float dx = mousePos.x - nodePos.x;
448 float dy = mousePos.y - nodePos.y;
449 float distance = sqrtf(dx * dx + dy * dy);
450
451 if (distance < PASSIVE_NODE_RADIUS && !passiveTree[i].unlocked && player.passivePoints > 0)
452 {
453 // Unlock the node
454 passiveTree[i].unlocked = true;
455 player.unlockedBulletTypes[player.unlockedCount++] = i;
456 player.passivePoints--;
457 break;
458 }
459 }
460 }
461 }
462
463 // Only handle game logic when menu is closed
464 if (!menuOpen)
465 {
466 // Update game time and progression
467 gameTime += deltaTime;
468
469 // Unlock color after COLOR_UNLOCK_TIME
470 if (!colorUnlocked && gameTime >= COLOR_UNLOCK_TIME)
471 {
472 colorUnlocked = true;
473 currentMonsterSaturation = 1.0f;
474 }
475
476 // Spawn boss and unlock new color
477 if (gameTime >= nextBossTime && monsterCount < MAX_MONSTERS)
478 {
479 // Generate new color for this boss cycle
480 currentMonsterHue = RandomFloat(0, 360);
481 nextBossTime += BOSS_SPAWN_TIME;
482
483 // Spawn boss monster
484 live_monsters[monsterCount++] = (Monster){
485 .position = {RandomFloat(0, map.width), RandomFloat(0, map.height)},
486 .hue = currentMonsterHue,
487 .saturation = currentMonsterSaturation,
488 .health = 50.0f * BOSS_HEALTH_MULTIPLIER,
489 .maxHealth = 50.0f * BOSS_HEALTH_MULTIPLIER,
490 .alive = true,
491 .hasCollision = true,
492 .isBoss = true
493 };
494 }
495
496 // Spawn regular monsters periodically
497 static float monsterSpawnTimer = 0.0f;
498 monsterSpawnTimer += deltaTime;
499 if (monsterSpawnTimer >= 3.0f && monsterCount < MAX_MONSTERS)
500 {
501 monsterSpawnTimer = 0.0f;
502 live_monsters[monsterCount++] = (Monster){
503 .position = {RandomFloat(0, map.width), RandomFloat(0, map.height)},
504 .hue = currentMonsterHue,
505 .saturation = currentMonsterSaturation,
506 .health = 50.0f,
507 .maxHealth = 50.0f,
508 .alive = true,
509 .hasCollision = true,
510 .isBoss = false
511 };
512 }
513
514 HandlePlayerMovement(&player, &map, deltaTime);
515
516 // Check player-monster collision
517 CheckPlayerMonsterCollision(&player, live_monsters, monsterCount, deltaTime);
518
519 // Update shoot timer and shoot bullets
520 shootTimer += deltaTime;
521 if (shootTimer >= SHOOT_INTERVAL && player.unlockedCount > 0)
522 {
523 shootTimer = 0.0f;
524
525 // Find inactive bullet slot
526 for (int i = 0; i < MAX_BULLETS; i++)
527 {
528 if (!bullets[i].active)
529 {
530 // Choose random bullet type from unlocked types
531 int randomIndex = (int)RandomFloat(0, player.unlockedCount);
532 int nodeIndex = player.unlockedBulletTypes[randomIndex];
533 PassiveNode* selectedNode = &passiveTree[nodeIndex];
534
535 // Find nearest monster to aim at
536 Vector2 targetPos = FindNearestMonster(live_monsters, monsterCount, player.position);
537
538 // Calculate direction to target
539 Vector2 direction = {
540 targetPos.x - player.position.x,
541 targetPos.y - player.position.y
542 };
543
544 // Normalize
545 float length = sqrtf(direction.x * direction.x + direction.y * direction.y);
546 if (length > 0)
547 {
548 direction.x /= length;
549 direction.y /= length;
550 }
551
552 // Calculate base angle
553 float baseAngle = atan2f(direction.y, direction.x);
554
555 // Add random deviation based on accuracy
556 float maxDeviation = (1.0f - hitProbability) * PI;
557 float deviation = RandomFloat(-maxDeviation, maxDeviation);
558 float finalAngle = baseAngle + deviation;
559
560 bullets[i] = (Bullet){
561 .position = player.position,
562 .velocity = {
563 cosf(finalAngle) * BULLET_SPEED,
564 sinf(finalAngle) * BULLET_SPEED
565 },
566 .lifetime = BULLET_LIFETIME,
567 .hue = selectedNode->hue,
568 .damage = BASE_DAMAGE + selectedNode->damageBonus,
569 .active = true
570 };
571 break;
572 }
573 }
574 }
575
576 // Update monsters
577 UpdateMonsters(live_monsters, monsterCount, player.position, deltaTime);
578
579 // Update bullets
580 UpdateBullets(bullets, MAX_BULLETS, deltaTime);
581
582 // Check collisions
583 int bossKills = CheckCollisions(bullets, MAX_BULLETS, live_monsters, monsterCount, &player);
584 bossesDefeated += bossKills;
585
586 // Update camera to follow player
587 camera.target = player.position;
588
589 // Clamp camera to map edges
590 float minX = INIT_SCREEN_WIDTH / 2.0f;
591 float maxX = map.width - INIT_SCREEN_WIDTH / 2.0f;
592 float minY = INIT_SCREEN_HEIGHT / 2.0f;
593 float maxY = map.height - INIT_SCREEN_HEIGHT / 2.0f;
594
595 if (camera.target.x < minX) camera.target.x = minX;
596 if (camera.target.x > maxX) camera.target.x = maxX;
597 if (camera.target.y < minY) camera.target.y = minY;
598 if (camera.target.y > maxY) camera.target.y = maxY;
599 }
600
601 // --- Drawings ---
602 BeginDrawing();
603 ClearBackground(RAYWHITE);
604
605 if (menuOpen)
606 {
607 // Draw color wheel background
608 for (int i = 0; i < 360; i++)
609 {
610 Color color = ColorFromHSV((float)i, 1.0f, 1.0f);
611 DrawCircleSector(
612 menuCenter,
613 PASSIVE_TREE_RADIUS + 30.0f,
614 (float)i,
615 (float)(i + 1),
616 1,
617 color
618 );
619 }
620
621 // Draw passive tree nodes
622 for (int i = 0; i < MAX_PASSIVE_NODES; i++)
623 {
624 float angleRad = passiveTree[i].angle * DEG2RAD;
625 Vector2 nodePos = {
626 menuCenter.x + cosf(angleRad) * PASSIVE_TREE_RADIUS,
627 menuCenter.y + sinf(angleRad) * PASSIVE_TREE_RADIUS
628 };
629
630 Color nodeColor = ColorFromHSV(passiveTree[i].hue, 1.0f, 1.0f);
631
632 if (passiveTree[i].unlocked)
633 {
634 // Draw unlocked node
635 DrawCircleV(nodePos, PASSIVE_NODE_RADIUS, nodeColor);
636 DrawCircleV(nodePos, PASSIVE_NODE_RADIUS - 3, WHITE);
637 DrawCircleV(nodePos, PASSIVE_NODE_RADIUS - 6, nodeColor);
638 }
639 else
640 {
641 // Draw locked node
642 DrawCircleV(nodePos, PASSIVE_NODE_RADIUS, DARKGRAY);
643 DrawCircleV(nodePos, PASSIVE_NODE_RADIUS - 3, GRAY);
644 }
645
646 // Draw node info on hover
647 Vector2 mousePos = GetMousePosition();
648 float dx = mousePos.x - nodePos.x;
649 float dy = mousePos.y - nodePos.y;
650 float distance = sqrtf(dx * dx + dy * dy);
651
652 if (distance < PASSIVE_NODE_RADIUS)
653 {
654 DrawText(passiveTree[i].description, nodePos.x - 50, nodePos.y - 40, 12, BLACK);
655 DrawText(TextFormat("+%.0f dmg", passiveTree[i].damageBonus), nodePos.x - 30, nodePos.y - 28, 10, DARKGRAY);
656 }
657 }
658
659 // Draw menu instructions
660 DrawText("PASSIVE TREE MENU", menuCenter.x - 100, menuCenter.y - 350, 20, BLACK);
661 DrawText("Click nodes to unlock bullet types", menuCenter.x - 120, menuCenter.y - 330, 16, DARKGRAY);
662 DrawText(TextFormat("Passive Points Available: %d", player.passivePoints), menuCenter.x - 120, menuCenter.y - 310, 16, PURPLE);
663 DrawText("Defeat bosses to earn passive points!", menuCenter.x - 120, menuCenter.y - 290, 14, DARKGRAY);
664 DrawText("Press P to close", menuCenter.x - 70, menuCenter.y + 320, 18, BLACK);
665 }
666 else
667 {
668 // Draw game world (with camera)
669 BeginMode2D(camera);
670
671 // Draw map background
672 DrawRectangle(0, 0, map.width, map.height, (Color){240, 240, 240, 255});
673
674 // Draw map border
675 DrawRectangleLinesEx((Rectangle){0, 0, map.width, map.height}, 5.0f, DARKGRAY);
676
677 // Draw grid to show map scale
678 for (int i = 0; i <= map.width; i += 100)
679 {
680 DrawLine(i, 0, i, map.height, LIGHTGRAY);
681 }
682 for (int i = 0; i <= map.height; i += 100)
683 {
684 DrawLine(0, i, map.width, i, LIGHTGRAY);
685 }
686
687 // Draw monsters
688 for (int i = 0; i < monsterCount; i++)
689 {
690 if (live_monsters[i].alive)
691 {
692 Color monsterColor = ColorFromHSV(live_monsters[i].hue, live_monsters[i].saturation, 1.0f);
693 float radius = live_monsters[i].isBoss ? MONSTER_RADIUS * 2 : MONSTER_RADIUS;
694
695 DrawCircleV(live_monsters[i].position, radius, monsterColor);
696
697 // Draw boss crown indicator
698 if (live_monsters[i].isBoss)
699 {
700 DrawCircleV(live_monsters[i].position, radius - 5, YELLOW);
701 DrawCircleV(live_monsters[i].position, radius - 10, monsterColor);
702 }
703
704 // Draw health bar
705 float healthPercent = live_monsters[i].health / live_monsters[i].maxHealth;
706 Vector2 barPos = {live_monsters[i].position.x - 15, live_monsters[i].position.y - radius - 10};
707 DrawRectangle(barPos.x, barPos.y, 30, 4, DARKGRAY);
708 DrawRectangle(barPos.x, barPos.y, 30 * healthPercent, 4, GREEN);
709
710 // Draw color label if toggle is on
711 if (showMonsterColors)
712 {
713 char colorText[32];
714 snprintf(colorText, sizeof(colorText), "H:%.0f S:%.1f", live_monsters[i].hue, live_monsters[i].saturation);
715 DrawText(colorText, live_monsters[i].position.x - 25, live_monsters[i].position.y + radius + 5, 10, BLACK);
716 }
717 }
718 }
719
720 // Draw bullets
721 for (int i = 0; i < MAX_BULLETS; i++)
722 {
723 if (bullets[i].active)
724 {
725 Color bulletColor = ColorFromHSV(bullets[i].hue, 1.0f, 1.0f);
726 DrawCircleV(bullets[i].position, BULLET_RADIUS, bulletColor);
727 }
728 }
729
730 // Draw player (flash when invulnerable)
731 if (player.invulnerabilityTimer <= 0 || ((int)(player.invulnerabilityTimer * 10) % 2 == 0))
732 {
733 DrawCircleV(player.position, PLAYER_RADIUS, player.color);
734 }
735
736 EndMode2D();
737
738 // Draw UI (screen space)
739 // Timer at top center
740 int minutes = (int)(gameTime / 60);
741 int seconds = (int)gameTime % 60;
742 DrawText(TextFormat("Time: %02d:%02d", minutes, seconds), INIT_SCREEN_WIDTH / 2 - 60, 10, 30, BLACK);
743
744 // Player health bar
745 float healthPercent = player.health / player.maxHealth;
746 DrawRectangle(10, 10, 200, 20, DARKGRAY);
747 DrawRectangle(10, 10, 200 * healthPercent, 20, RED);
748 DrawText(TextFormat("HP: %.0f/%.0f", player.health, player.maxHealth), 15, 12, 16, WHITE);
749
750 // Passive points
751 DrawText(TextFormat("Passive Points: %d", player.passivePoints), 10, 40, 20, PURPLE);
752
753 // Controls
754 DrawText("WASD: Move | P: Menu | R: Toggle Colors", 10, 70, 16, DARKGRAY);
755
756 // Count alive monsters and bosses
757 int aliveCount = 0;
758 int bossCount = 0;
759 for (int i = 0; i < monsterCount; i++)
760 {
761 if (live_monsters[i].alive)
762 {
763 aliveCount++;
764 if (live_monsters[i].isBoss) bossCount++;
765 }
766 }
767 DrawText(TextFormat("Monsters: %d | Bosses: %d", aliveCount, bossCount), 10, 95, 18, DARKGRAY);
768 DrawText(TextFormat("Unlocked bullets: %d/%d", player.unlockedCount, MAX_PASSIVE_NODES), 10, 120, 16, DARKGRAY);
769 DrawText(TextFormat("Bosses defeated: %d", bossesDefeated), 10, 145, 16, DARKGRAY);
770 }
771
772 EndDrawing();
773 }
774 return 0;
775 }