comparison color_game/main.c @ 76:35b1abc37969

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