Mercurial
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 } |