diff 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
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/color_game/main.c	Sat Dec 20 13:56:01 2025 -0500
@@ -0,0 +1,775 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <time.h>
+#include "dowa/dowa.h"
+#include "third_party/raylib/include/raylib.h"
+
+#define INIT_SCREEN_WIDTH 1200
+#define INIT_SCREEN_HEIGHT 700
+#define PLAYER_SPEED 200.0f
+#define PLAYER_RADIUS 20.0f
+#define MONSTER_SPEED 100.0f
+#define MONSTER_RADIUS 15.0f
+#define BULLET_SPEED 300.0f
+#define BULLET_RADIUS 5.0f
+#define BULLET_LIFETIME 3.0f
+#define SHOOT_INTERVAL 0.5f
+#define HIT_PROBABILITY 0.7f  // 70% aim accuracy (0.0 = random, 1.0 = perfect)
+#define MAX_PASSIVE_NODES 12
+#define PASSIVE_TREE_RADIUS 250.0f
+#define PASSIVE_NODE_RADIUS 20.0f
+#define BASE_DAMAGE 10.0f
+#define PLAYER_MAX_HEALTH 100.0f
+#define MONSTER_CONTACT_DAMAGE 5.0f
+#define BOSS_HEALTH_MULTIPLIER 5.0f
+#define COLOR_UNLOCK_TIME 30.0f
+#define BOSS_SPAWN_TIME 60.0f
+
+typedef struct Player {
+  Vector2 position;
+  Color color;
+  float health;
+  float maxHealth;
+  int unlockedBulletTypes[MAX_PASSIVE_NODES];
+  int unlockedCount;
+  int passivePoints;
+  float invulnerabilityTimer;
+} Player;
+
+typedef struct Map {
+  float width;
+  float height;
+} Map;
+
+typedef struct Monster {
+  Vector2 position;
+  float hue;
+  float saturation;
+  float health;
+  float maxHealth;
+  bool alive;
+  bool hasCollision;
+  bool isBoss;
+} Monster;
+
+typedef struct Bullet {
+  Vector2 position;
+  Vector2 velocity;
+  float lifetime;
+  float hue;
+  float damage;
+  bool active;
+} Bullet;
+
+typedef struct PassiveNode {
+  float angle;
+  float hue;
+  char description[64];
+  float damageBonus;
+  bool unlocked;
+} PassiveNode;
+
+float RandomFloat(float min, float max)
+{
+  return min + ((float)rand() / (float)RAND_MAX) * (max - min);
+}
+
+float CalculateColorDamage(float bulletHue, float monsterHue, float baseDamage)
+{
+  // Calculate hue difference (0-180 degrees)
+  float diff = fabs(bulletHue - monsterHue);
+  if (diff > 180.0f) diff = 360.0f - diff;
+
+  // Opposite colors (180 degrees) = 2x damage
+  // Same color (0 degrees) = 0.5x damage
+  // Linear scale between them
+  float damageMultiplier = 0.5f + (diff / 180.0f) * 1.5f;
+
+  return baseDamage * damageMultiplier;
+}
+
+void InitializePassiveTree(PassiveNode* nodes)
+{
+  const char* nodeDescriptions[MAX_PASSIVE_NODES] = {
+    "Red Bullet",      // 0°
+    "Orange Bullet",   // 30°
+    "Yellow Bullet",   // 60°
+    "Chartreuse",      // 90°
+    "Green Bullet",    // 120°
+    "Spring Green",    // 150°
+    "Cyan Bullet",     // 180°
+    "Azure Bullet",    // 210°
+    "Blue Bullet",     // 240°
+    "Violet Bullet",   // 270°
+    "Magenta Bullet",  // 300°
+    "Rose Bullet"      // 330°
+  };
+
+  for (int i = 0; i < MAX_PASSIVE_NODES; i++)
+  {
+    nodes[i].angle = (360.0f / MAX_PASSIVE_NODES) * i;
+    nodes[i].hue = nodes[i].angle;
+    snprintf(nodes[i].description, sizeof(nodes[i].description), "%s", nodeDescriptions[i]);
+    nodes[i].damageBonus = 5.0f + (i * 2.0f); // Scaling damage bonus
+    nodes[i].unlocked = false;
+  }
+}
+
+void HandlePlayerMovement(Player* player, Map* map, float deltaTime)
+{
+  Vector2 movement = {0};
+
+  if (IsKeyDown(KEY_W) || IsKeyDown(KEY_UP))    movement.y -= 1.0f;
+  if (IsKeyDown(KEY_S) || IsKeyDown(KEY_DOWN))  movement.y += 1.0f;
+  if (IsKeyDown(KEY_A) || IsKeyDown(KEY_LEFT))  movement.x -= 1.0f;
+  if (IsKeyDown(KEY_D) || IsKeyDown(KEY_RIGHT)) movement.x += 1.0f;
+
+  // Normalize diagonal movement
+  float length = sqrtf(movement.x * movement.x + movement.y * movement.y);
+  if (length > 0)
+  {
+    movement.x /= length;
+    movement.y /= length;
+  }
+
+  // Apply movement
+  player->position.x += movement.x * PLAYER_SPEED * deltaTime;
+  player->position.y += movement.y * PLAYER_SPEED * deltaTime;
+
+  // Keep player within map bounds
+  if (player->position.x < PLAYER_RADIUS) player->position.x = PLAYER_RADIUS;
+  if (player->position.x > map->width - PLAYER_RADIUS)
+    player->position.x = map->width - PLAYER_RADIUS;
+  if (player->position.y < PLAYER_RADIUS) player->position.y = PLAYER_RADIUS;
+  if (player->position.y > map->height - PLAYER_RADIUS)
+    player->position.y = map->height - PLAYER_RADIUS;
+}
+
+void UpdateMonsters(Monster* monsters, int monsterCount, Vector2 playerPos, float deltaTime)
+{
+  for (int i = 0; i < monsterCount; i++)
+  {
+    if (!monsters[i].alive) continue;
+
+    // Calculate direction to player
+    Vector2 direction = {
+      playerPos.x - monsters[i].position.x,
+      playerPos.y - monsters[i].position.y
+    };
+
+    // Normalize direction
+    float length = sqrtf(direction.x * direction.x + direction.y * direction.y);
+    if (length > 0)
+    {
+      direction.x /= length;
+      direction.y /= length;
+    }
+
+    // Move toward player
+    Vector2 newPos = {
+      monsters[i].position.x + direction.x * MONSTER_SPEED * deltaTime,
+      monsters[i].position.y + direction.y * MONSTER_SPEED * deltaTime
+    };
+
+    // Check collision with other monsters if this monster has collision enabled
+    if (monsters[i].hasCollision)
+    {
+      bool collision = false;
+      for (int j = 0; j < monsterCount; j++)
+      {
+        if (i == j || !monsters[j].alive || !monsters[j].hasCollision) continue;
+
+        float dx = newPos.x - monsters[j].position.x;
+        float dy = newPos.y - monsters[j].position.y;
+        float distance = sqrtf(dx * dx + dy * dy);
+
+        if (distance < MONSTER_RADIUS * 2)
+        {
+          collision = true;
+          break;
+        }
+      }
+
+      if (!collision)
+      {
+        monsters[i].position = newPos;
+      }
+    }
+    else
+    {
+      monsters[i].position = newPos;
+    }
+  }
+}
+
+void CheckPlayerMonsterCollision(Player* player, Monster* monsters, int monsterCount, float deltaTime)
+{
+  if (player->invulnerabilityTimer > 0)
+  {
+    player->invulnerabilityTimer -= deltaTime;
+    return;
+  }
+
+  for (int i = 0; i < monsterCount; i++)
+  {
+    if (!monsters[i].alive) continue;
+
+    float dx = player->position.x - monsters[i].position.x;
+    float dy = player->position.y - monsters[i].position.y;
+    float distance = sqrtf(dx * dx + dy * dy);
+
+    if (distance < PLAYER_RADIUS + MONSTER_RADIUS)
+    {
+      player->health -= MONSTER_CONTACT_DAMAGE;
+      player->invulnerabilityTimer = 0.5f; // 0.5 second invulnerability
+      if (player->health < 0) player->health = 0;
+      break;
+    }
+  }
+}
+
+void UpdateBullets(Bullet* bullets, int bulletCount, float deltaTime)
+{
+  for (int i = 0; i < bulletCount; i++)
+  {
+    if (!bullets[i].active) continue;
+
+    // Update position
+    bullets[i].position.x += bullets[i].velocity.x * deltaTime;
+    bullets[i].position.y += bullets[i].velocity.y * deltaTime;
+
+    // Update lifetime
+    bullets[i].lifetime -= deltaTime;
+    if (bullets[i].lifetime <= 0)
+    {
+      bullets[i].active = false;
+    }
+  }
+}
+
+int CheckCollisions(Bullet* bullets, int bulletCount, Monster* monsters, int monsterCount, Player* player)
+{
+  int bossesKilled = 0;
+
+  for (int i = 0; i < bulletCount; i++)
+  {
+    if (!bullets[i].active) continue;
+
+    for (int j = 0; j < monsterCount; j++)
+    {
+      if (!monsters[j].alive) continue;
+
+      // Check distance between bullet and monster
+      float dx = bullets[i].position.x - monsters[j].position.x;
+      float dy = bullets[i].position.y - monsters[j].position.y;
+      float distance = sqrtf(dx * dx + dy * dy);
+
+      if (distance < BULLET_RADIUS + MONSTER_RADIUS)
+      {
+        // Calculate damage based on color difference
+        float damage = CalculateColorDamage(bullets[i].hue, monsters[j].hue, bullets[i].damage);
+        monsters[j].health -= damage;
+
+        // Check if monster died
+        if (monsters[j].health <= 0)
+        {
+          monsters[j].alive = false;
+
+          // Award passive point if boss was killed
+          if (monsters[j].isBoss)
+          {
+            player->passivePoints++;
+            bossesKilled++;
+          }
+        }
+
+        bullets[i].active = false;
+        break;
+      }
+    }
+  }
+
+  return bossesKilled;
+}
+
+Vector2 FindNearestMonster(Monster* monsters, int monsterCount, Vector2 playerPos)
+{
+  float minDistance = INFINITY;
+  Vector2 nearestPos = playerPos;
+  bool found = false;
+
+  for (int i = 0; i < monsterCount; i++)
+  {
+    if (!monsters[i].alive) continue;
+
+    float dx = monsters[i].position.x - playerPos.x;
+    float dy = monsters[i].position.y - playerPos.y;
+    float distance = sqrtf(dx * dx + dy * dy);
+
+    if (distance < minDistance)
+    {
+      minDistance = distance;
+      nearestPos = monsters[i].position;
+      found = true;
+    }
+  }
+
+  if (!found)
+  {
+    // No monsters, return random direction
+    float angle = RandomFloat(0.0f, 2.0f * PI);
+    return (Vector2){
+      playerPos.x + cosf(angle) * 100.0f,
+      playerPos.y + sinf(angle) * 100.0f
+    };
+  }
+
+  return nearestPos;
+}
+
+int main()
+{
+  InitWindow(INIT_SCREEN_WIDTH, INIT_SCREEN_HEIGHT, "color game");
+  SetTargetFPS(60);
+  srand(time(NULL));
+
+  // --- All Global Variables ---
+  Map map = {
+    .width = INIT_SCREEN_WIDTH * 3,
+    .height = INIT_SCREEN_HEIGHT * 3
+  };
+
+  // Initialize player at map center
+  Player player = {
+    .position = {map.width / 2, map.height / 2},
+    .color = BLUE,
+    .health = PLAYER_MAX_HEALTH,
+    .maxHealth = PLAYER_MAX_HEALTH,
+    .unlockedCount = 0,
+    .passivePoints = 0,
+    .invulnerabilityTimer = 0.0f
+  };
+
+  // Initialize passive tree
+  PassiveNode passiveTree[MAX_PASSIVE_NODES];
+  InitializePassiveTree(passiveTree);
+
+  // Unlock first node only (grayscale bullet to start)
+  passiveTree[0].unlocked = true;
+  player.unlockedBulletTypes[player.unlockedCount++] = 0;
+
+  // Initialize camera
+  Camera2D camera = {0};
+  camera.target = player.position;
+  camera.offset = (Vector2){INIT_SCREEN_WIDTH / 2.0f, INIT_SCREEN_HEIGHT / 2.0f};
+  camera.rotation = 0.0f;
+  camera.zoom = 1.0f;
+
+  bool menuOpen = false;
+  bool showMonsterColors = false;
+
+  Vector2 menuCenter = {
+    .x = INIT_SCREEN_WIDTH / 2,
+    .y = INIT_SCREEN_HEIGHT / 2
+  };
+
+  // Game progression variables
+  float gameTime = 0.0f;
+  float currentMonsterHue = 0.0f;
+  float currentMonsterSaturation = 0.0f; // Start grayscale
+  int bossesDefeated = 0;
+  float nextBossTime = BOSS_SPAWN_TIME;
+  bool colorUnlocked = false;
+
+  // Initialize monsters array
+  #define MAX_MONSTERS 100
+  Monster live_monsters[MAX_MONSTERS];
+  int monsterCount = 0;
+
+  // Spawn initial monsters (grayscale)
+  for (int i = 0; i < 10; i++)
+  {
+    live_monsters[monsterCount++] = (Monster){
+      .position = {RandomFloat(0, map.width), RandomFloat(0, map.height)},
+      .hue = currentMonsterHue,
+      .saturation = currentMonsterSaturation,
+      .health = 50.0f,
+      .maxHealth = 50.0f,
+      .alive = true,
+      .hasCollision = true,
+      .isBoss = false
+    };
+  }
+
+  // Initialize bullets array
+  #define MAX_BULLETS 200
+  Bullet bullets[MAX_BULLETS];
+  int bulletCount = 0;
+  for (int i = 0; i < MAX_BULLETS; i++)
+  {
+    bullets[i].active = false;
+  }
+
+  // Shooting timer
+  float shootTimer = 0.0f;
+  float hitProbability = HIT_PROBABILITY;
+
+  while (!WindowShouldClose())
+  {
+    float deltaTime = GetFrameTime();
+
+    if (IsKeyPressed(KEY_P))
+    {
+      menuOpen = !menuOpen;
+    }
+
+    if (IsKeyPressed(KEY_R))
+    {
+      showMonsterColors = !showMonsterColors;
+    }
+
+    // Handle passive tree menu interactions
+    if (menuOpen)
+    {
+      if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
+      {
+        Vector2 mousePos = GetMousePosition();
+
+        // Check if clicked on a passive node
+        for (int i = 0; i < MAX_PASSIVE_NODES; i++)
+        {
+          float angleRad = passiveTree[i].angle * DEG2RAD;
+          Vector2 nodePos = {
+            menuCenter.x + cosf(angleRad) * PASSIVE_TREE_RADIUS,
+            menuCenter.y + sinf(angleRad) * PASSIVE_TREE_RADIUS
+          };
+
+          float dx = mousePos.x - nodePos.x;
+          float dy = mousePos.y - nodePos.y;
+          float distance = sqrtf(dx * dx + dy * dy);
+
+          if (distance < PASSIVE_NODE_RADIUS && !passiveTree[i].unlocked && player.passivePoints > 0)
+          {
+            // Unlock the node
+            passiveTree[i].unlocked = true;
+            player.unlockedBulletTypes[player.unlockedCount++] = i;
+            player.passivePoints--;
+            break;
+          }
+        }
+      }
+    }
+
+    // Only handle game logic when menu is closed
+    if (!menuOpen)
+    {
+      // Update game time and progression
+      gameTime += deltaTime;
+
+      // Unlock color after COLOR_UNLOCK_TIME
+      if (!colorUnlocked && gameTime >= COLOR_UNLOCK_TIME)
+      {
+        colorUnlocked = true;
+        currentMonsterSaturation = 1.0f;
+      }
+
+      // Spawn boss and unlock new color
+      if (gameTime >= nextBossTime && monsterCount < MAX_MONSTERS)
+      {
+        // Generate new color for this boss cycle
+        currentMonsterHue = RandomFloat(0, 360);
+        nextBossTime += BOSS_SPAWN_TIME;
+
+        // Spawn boss monster
+        live_monsters[monsterCount++] = (Monster){
+          .position = {RandomFloat(0, map.width), RandomFloat(0, map.height)},
+          .hue = currentMonsterHue,
+          .saturation = currentMonsterSaturation,
+          .health = 50.0f * BOSS_HEALTH_MULTIPLIER,
+          .maxHealth = 50.0f * BOSS_HEALTH_MULTIPLIER,
+          .alive = true,
+          .hasCollision = true,
+          .isBoss = true
+        };
+      }
+
+      // Spawn regular monsters periodically
+      static float monsterSpawnTimer = 0.0f;
+      monsterSpawnTimer += deltaTime;
+      if (monsterSpawnTimer >= 3.0f && monsterCount < MAX_MONSTERS)
+      {
+        monsterSpawnTimer = 0.0f;
+        live_monsters[monsterCount++] = (Monster){
+          .position = {RandomFloat(0, map.width), RandomFloat(0, map.height)},
+          .hue = currentMonsterHue,
+          .saturation = currentMonsterSaturation,
+          .health = 50.0f,
+          .maxHealth = 50.0f,
+          .alive = true,
+          .hasCollision = true,
+          .isBoss = false
+        };
+      }
+
+      HandlePlayerMovement(&player, &map, deltaTime);
+
+      // Check player-monster collision
+      CheckPlayerMonsterCollision(&player, live_monsters, monsterCount, deltaTime);
+
+      // Update shoot timer and shoot bullets
+      shootTimer += deltaTime;
+      if (shootTimer >= SHOOT_INTERVAL && player.unlockedCount > 0)
+      {
+        shootTimer = 0.0f;
+
+        // Find inactive bullet slot
+        for (int i = 0; i < MAX_BULLETS; i++)
+        {
+          if (!bullets[i].active)
+          {
+            // Choose random bullet type from unlocked types
+            int randomIndex = (int)RandomFloat(0, player.unlockedCount);
+            int nodeIndex = player.unlockedBulletTypes[randomIndex];
+            PassiveNode* selectedNode = &passiveTree[nodeIndex];
+
+            // Find nearest monster to aim at
+            Vector2 targetPos = FindNearestMonster(live_monsters, monsterCount, player.position);
+
+            // Calculate direction to target
+            Vector2 direction = {
+              targetPos.x - player.position.x,
+              targetPos.y - player.position.y
+            };
+
+            // Normalize
+            float length = sqrtf(direction.x * direction.x + direction.y * direction.y);
+            if (length > 0)
+            {
+              direction.x /= length;
+              direction.y /= length;
+            }
+
+            // Calculate base angle
+            float baseAngle = atan2f(direction.y, direction.x);
+
+            // Add random deviation based on accuracy
+            float maxDeviation = (1.0f - hitProbability) * PI;
+            float deviation = RandomFloat(-maxDeviation, maxDeviation);
+            float finalAngle = baseAngle + deviation;
+
+            bullets[i] = (Bullet){
+              .position = player.position,
+              .velocity = {
+                cosf(finalAngle) * BULLET_SPEED,
+                sinf(finalAngle) * BULLET_SPEED
+              },
+              .lifetime = BULLET_LIFETIME,
+              .hue = selectedNode->hue,
+              .damage = BASE_DAMAGE + selectedNode->damageBonus,
+              .active = true
+            };
+            break;
+          }
+        }
+      }
+
+      // Update monsters
+      UpdateMonsters(live_monsters, monsterCount, player.position, deltaTime);
+
+      // Update bullets
+      UpdateBullets(bullets, MAX_BULLETS, deltaTime);
+
+      // Check collisions
+      int bossKills = CheckCollisions(bullets, MAX_BULLETS, live_monsters, monsterCount, &player);
+      bossesDefeated += bossKills;
+
+      // Update camera to follow player
+      camera.target = player.position;
+
+      // Clamp camera to map edges
+      float minX = INIT_SCREEN_WIDTH / 2.0f;
+      float maxX = map.width - INIT_SCREEN_WIDTH / 2.0f;
+      float minY = INIT_SCREEN_HEIGHT / 2.0f;
+      float maxY = map.height - INIT_SCREEN_HEIGHT / 2.0f;
+
+      if (camera.target.x < minX) camera.target.x = minX;
+      if (camera.target.x > maxX) camera.target.x = maxX;
+      if (camera.target.y < minY) camera.target.y = minY;
+      if (camera.target.y > maxY) camera.target.y = maxY;
+    }
+
+    // --- Drawings ---
+    BeginDrawing();
+      ClearBackground(RAYWHITE);
+
+      if (menuOpen)
+      {
+        // Draw color wheel background
+        for (int i = 0; i < 360; i++)
+        {
+          Color color = ColorFromHSV((float)i, 1.0f, 1.0f);
+          DrawCircleSector(
+            menuCenter,
+            PASSIVE_TREE_RADIUS + 30.0f,
+            (float)i,
+            (float)(i + 1),
+            1,
+            color
+          );
+        }
+
+        // Draw passive tree nodes
+        for (int i = 0; i < MAX_PASSIVE_NODES; i++)
+        {
+          float angleRad = passiveTree[i].angle * DEG2RAD;
+          Vector2 nodePos = {
+            menuCenter.x + cosf(angleRad) * PASSIVE_TREE_RADIUS,
+            menuCenter.y + sinf(angleRad) * PASSIVE_TREE_RADIUS
+          };
+
+          Color nodeColor = ColorFromHSV(passiveTree[i].hue, 1.0f, 1.0f);
+
+          if (passiveTree[i].unlocked)
+          {
+            // Draw unlocked node
+            DrawCircleV(nodePos, PASSIVE_NODE_RADIUS, nodeColor);
+            DrawCircleV(nodePos, PASSIVE_NODE_RADIUS - 3, WHITE);
+            DrawCircleV(nodePos, PASSIVE_NODE_RADIUS - 6, nodeColor);
+          }
+          else
+          {
+            // Draw locked node
+            DrawCircleV(nodePos, PASSIVE_NODE_RADIUS, DARKGRAY);
+            DrawCircleV(nodePos, PASSIVE_NODE_RADIUS - 3, GRAY);
+          }
+
+          // Draw node info on hover
+          Vector2 mousePos = GetMousePosition();
+          float dx = mousePos.x - nodePos.x;
+          float dy = mousePos.y - nodePos.y;
+          float distance = sqrtf(dx * dx + dy * dy);
+
+          if (distance < PASSIVE_NODE_RADIUS)
+          {
+            DrawText(passiveTree[i].description, nodePos.x - 50, nodePos.y - 40, 12, BLACK);
+            DrawText(TextFormat("+%.0f dmg", passiveTree[i].damageBonus), nodePos.x - 30, nodePos.y - 28, 10, DARKGRAY);
+          }
+        }
+
+        // Draw menu instructions
+        DrawText("PASSIVE TREE MENU", menuCenter.x - 100, menuCenter.y - 350, 20, BLACK);
+        DrawText("Click nodes to unlock bullet types", menuCenter.x - 120, menuCenter.y - 330, 16, DARKGRAY);
+        DrawText(TextFormat("Passive Points Available: %d", player.passivePoints), menuCenter.x - 120, menuCenter.y - 310, 16, PURPLE);
+        DrawText("Defeat bosses to earn passive points!", menuCenter.x - 120, menuCenter.y - 290, 14, DARKGRAY);
+        DrawText("Press P to close", menuCenter.x - 70, menuCenter.y + 320, 18, BLACK);
+      }
+      else
+      {
+        // Draw game world (with camera)
+        BeginMode2D(camera);
+
+          // Draw map background
+          DrawRectangle(0, 0, map.width, map.height, (Color){240, 240, 240, 255});
+
+          // Draw map border
+          DrawRectangleLinesEx((Rectangle){0, 0, map.width, map.height}, 5.0f, DARKGRAY);
+
+          // Draw grid to show map scale
+          for (int i = 0; i <= map.width; i += 100)
+          {
+            DrawLine(i, 0, i, map.height, LIGHTGRAY);
+          }
+          for (int i = 0; i <= map.height; i += 100)
+          {
+            DrawLine(0, i, map.width, i, LIGHTGRAY);
+          }
+
+          // Draw monsters
+          for (int i = 0; i < monsterCount; i++)
+          {
+            if (live_monsters[i].alive)
+            {
+              Color monsterColor = ColorFromHSV(live_monsters[i].hue, live_monsters[i].saturation, 1.0f);
+              float radius = live_monsters[i].isBoss ? MONSTER_RADIUS * 2 : MONSTER_RADIUS;
+
+              DrawCircleV(live_monsters[i].position, radius, monsterColor);
+
+              // Draw boss crown indicator
+              if (live_monsters[i].isBoss)
+              {
+                DrawCircleV(live_monsters[i].position, radius - 5, YELLOW);
+                DrawCircleV(live_monsters[i].position, radius - 10, monsterColor);
+              }
+
+              // Draw health bar
+              float healthPercent = live_monsters[i].health / live_monsters[i].maxHealth;
+              Vector2 barPos = {live_monsters[i].position.x - 15, live_monsters[i].position.y - radius - 10};
+              DrawRectangle(barPos.x, barPos.y, 30, 4, DARKGRAY);
+              DrawRectangle(barPos.x, barPos.y, 30 * healthPercent, 4, GREEN);
+
+              // Draw color label if toggle is on
+              if (showMonsterColors)
+              {
+                char colorText[32];
+                snprintf(colorText, sizeof(colorText), "H:%.0f S:%.1f", live_monsters[i].hue, live_monsters[i].saturation);
+                DrawText(colorText, live_monsters[i].position.x - 25, live_monsters[i].position.y + radius + 5, 10, BLACK);
+              }
+            }
+          }
+
+          // Draw bullets
+          for (int i = 0; i < MAX_BULLETS; i++)
+          {
+            if (bullets[i].active)
+            {
+              Color bulletColor = ColorFromHSV(bullets[i].hue, 1.0f, 1.0f);
+              DrawCircleV(bullets[i].position, BULLET_RADIUS, bulletColor);
+            }
+          }
+
+          // Draw player (flash when invulnerable)
+          if (player.invulnerabilityTimer <= 0 || ((int)(player.invulnerabilityTimer * 10) % 2 == 0))
+          {
+            DrawCircleV(player.position, PLAYER_RADIUS, player.color);
+          }
+
+        EndMode2D();
+
+        // Draw UI (screen space)
+        // Timer at top center
+        int minutes = (int)(gameTime / 60);
+        int seconds = (int)gameTime % 60;
+        DrawText(TextFormat("Time: %02d:%02d", minutes, seconds), INIT_SCREEN_WIDTH / 2 - 60, 10, 30, BLACK);
+
+        // Player health bar
+        float healthPercent = player.health / player.maxHealth;
+        DrawRectangle(10, 10, 200, 20, DARKGRAY);
+        DrawRectangle(10, 10, 200 * healthPercent, 20, RED);
+        DrawText(TextFormat("HP: %.0f/%.0f", player.health, player.maxHealth), 15, 12, 16, WHITE);
+
+        // Passive points
+        DrawText(TextFormat("Passive Points: %d", player.passivePoints), 10, 40, 20, PURPLE);
+
+        // Controls
+        DrawText("WASD: Move | P: Menu | R: Toggle Colors", 10, 70, 16, DARKGRAY);
+
+        // Count alive monsters and bosses
+        int aliveCount = 0;
+        int bossCount = 0;
+        for (int i = 0; i < monsterCount; i++)
+        {
+          if (live_monsters[i].alive)
+          {
+            aliveCount++;
+            if (live_monsters[i].isBoss) bossCount++;
+          }
+        }
+        DrawText(TextFormat("Monsters: %d | Bosses: %d", aliveCount, bossCount), 10, 95, 18, DARKGRAY);
+        DrawText(TextFormat("Unlocked bullets: %d/%d", player.unlockedCount, MAX_PASSIVE_NODES), 10, 120, 16, DARKGRAY);
+        DrawText(TextFormat("Bosses defeated: %d", bossesDefeated), 10, 145, 16, DARKGRAY);
+      }
+
+    EndDrawing();
+  }
+  return 0;
+}