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