view color_game/main.c @ 71:75de5903355c

Giagantic changes that update Dowa library to be more align with stb style array and hashmap. Updated Seobeo to be caching on server side instead of file level caching. Deleted bunch of things I don't really use.
author June Park <parkjune1995@gmail.com>
date Sun, 28 Dec 2025 20:34:22 -0800
parents fff1b048dda6
children 35b1abc37969
line wrap: on
line source

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <math.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 PLAYER_MAX_HEALTH 100.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 MONSTER_WEIGHT 10.0f
#define MONSTER_CONTACT_DAMAGE 5.0f

#define BASE_DAMAGE 10.0f
#define BOSS_HEALTH_MULTIPLIER 5.0f
#define BOSS_MONSTER_WEIGHT 10.0f
#define BOSS_SPAWN_TIME 10.0f

#define COLOR_UNLOCK_TIME 5.0f
#define MAX_DAMAGE_NUMBERS 100
#define DAMAGE_NUMBER_LIFETIME 1.0f
#define EXP_PER_MONSTER 10.0f
#define EXP_PER_BOSS 50.0f
#define EXP_TO_LEVEL 100.0f

typedef struct Player {
  Vector2 position;
  float health;
  float maxHealth;
  float invulnerabilityTimer;
  int unlockedBulletTypes[MAX_PASSIVE_NODES];
  int unlockedCount;
  int passivePoints;
  int level;
  float experience;
  float expToNextLevel;
  Color color;
} Player;

typedef struct DamageNumber {
  Vector2 position;
  float damage;
  float lifetime;
  bool active;
  Color color;
} DamageNumber;

typedef struct Map {
  float width;
  float height;
} Map;

typedef struct Monster {
  Vector2 position;
  float hue;
  float saturation;
  float health;
  float maxHealth;
  float weight;
  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 SpawnDamageNumber(DamageNumber* damageNumbers, int maxCount, Vector2 position, float damage, Color color)
{
  for (int i = 0; i < maxCount; i++)
  {
    if (!damageNumbers[i].active)
    {
      damageNumbers[i].position = position;
      damageNumbers[i].damage = damage;
      damageNumbers[i].lifetime = DAMAGE_NUMBER_LIFETIME;
      damageNumbers[i].active = true;
      damageNumbers[i].color = color;
      break;
    }
  }
}

void UpdateDamageNumbers(DamageNumber* damageNumbers, int count, float deltaTime)
{
  for (int i = 0; i < count; i++)
  {
    if (!damageNumbers[i].active) continue;

    damageNumbers[i].lifetime -= deltaTime;
    damageNumbers[i].position.y -= 30.0f * deltaTime; // Float upward

    if (damageNumbers[i].lifetime <= 0)
    {
      damageNumbers[i].active = false;
    }
  }
}

void AddExperience(Player* player, float exp)
{
  player->experience += exp;

  // Check for level up
  while (player->experience >= player->expToNextLevel)
  {
    player->experience -= player->expToNextLevel;
    player->level++;
    player->passivePoints++;
    // Increase exp requirement for next level
    player->expToNextLevel = EXP_TO_LEVEL * player->level;
  }
}

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 canMove = true;
      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)
        {
          // If this monster is heavier, push the other monster away
          if (monsters[i].weight > monsters[j].weight)
          {
            // Calculate push direction (away from current monster)
            Vector2 pushDir = {dx, dy};
            float pushLength = sqrtf(pushDir.x * pushDir.x + pushDir.y * pushDir.y);
            if (pushLength > 0)
            {
              pushDir.x /= pushLength;
              pushDir.y /= pushLength;
            }

            // Push the lighter monster away
            float pushForce = (monsters[i].weight - monsters[j].weight) * MONSTER_SPEED * deltaTime * 0.5f;
            monsters[j].position.x += pushDir.x * pushForce;
            monsters[j].position.y += pushDir.y * pushForce;
          }
          else
          {
            // This monster is lighter or equal weight, can't move through
            canMove = false;
            break;
          }
        }
      }

      if (canMove)
      {
        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, DamageNumber* damageNumbers)
{
  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;

        // Spawn damage number
        Color damageColor = damage > bullets[i].damage * 1.5f ? ORANGE : WHITE;
        SpawnDamageNumber(damageNumbers, MAX_DAMAGE_NUMBERS, monsters[j].position, damage, damageColor);

        // Check if monster died
        if (monsters[j].health <= 0)
        {
          monsters[j].alive = false;

          // Award experience
          float expGain = monsters[j].isBoss ? EXP_PER_BOSS : EXP_PER_MONSTER;
          AddExperience(player, expGain);

          // 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)
      break;
  }

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

  Player player = {
    .position = {map.width / 2, map.height / 2},
    .color = BLUE,
    .health = PLAYER_MAX_HEALTH,
    .maxHealth = PLAYER_MAX_HEALTH,
    .unlockedCount = 0,
    .passivePoints = 3,
    .invulnerabilityTimer = 0.0f,
    .level = 1,
    .experience = 0.0f,
    .expToNextLevel = EXP_TO_LEVEL
  };

  PassiveNode passiveTree[MAX_PASSIVE_NODES];
  InitializePassiveTree(passiveTree);
  passiveTree[0].unlocked = true;
  player.unlockedBulletTypes[player.unlockedCount++] = 0;

  // Initialize damage numbers array
  DamageNumber damageNumbers[MAX_DAMAGE_NUMBERS];
  for (int i = 0; i < MAX_DAMAGE_NUMBERS; i++)
  {
    damageNumbers[i].active = false;
  }

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

  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 1000
  Monster live_monsters[MAX_MONSTERS];
  int monsterCount = 0;

  // Spawn initial monsters (grayscale)
  for (int i = 0; i < 30; i++)
  {
    live_monsters[monsterCount++] = (Monster){
      .position = {RandomFloat(0, map.width), RandomFloat(0, map.height)},
      .hue = currentMonsterHue,
      .saturation = currentMonsterSaturation,
      .weight = MONSTER_WEIGHT,
      .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,
          .weight = BOSS_MONSTER_WEIGHT,
          .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);

      // Update damage numbers
      UpdateDamageNumbers(damageNumbers, MAX_DAMAGE_NUMBERS, deltaTime);

      // Check collisions
      int bossKills = CheckCollisions(bullets, MAX_BULLETS, live_monsters, monsterCount, &player, damageNumbers);
      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 damage numbers
          for (int i = 0; i < MAX_DAMAGE_NUMBERS; i++)
          {
            if (damageNumbers[i].active)
            {
              printf("True\n");
              float alpha = damageNumbers[i].lifetime / DAMAGE_NUMBER_LIFETIME;
              Color color = damageNumbers[i].color;
              color.a = (unsigned char)(255 * alpha);
              DrawText(TextFormat("%.0f", damageNumbers[i].damage),
                       damageNumbers[i].position.x - 10,
                       damageNumbers[i].position.y,
                       20,
                       color);
            }
          }

          // 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);

        // Experience bar
        float expPercent = player.experience / player.expToNextLevel;
        DrawRectangle(10, 35, 200, 15, DARKGRAY);
        DrawRectangle(10, 35, 200 * expPercent, 15, SKYBLUE);
        DrawText(TextFormat("Lvl %d", player.level), 15, 36, 12, WHITE);
        DrawText(TextFormat("%.0f/%.0f XP", player.experience, player.expToNextLevel), 70, 36, 12, WHITE);

        // Passive points
        DrawText(TextFormat("Passive Points: %d", player.passivePoints), 10, 55, 18, PURPLE);

        // Controls
        DrawText("WASD: Move | P: Menu | R: Toggle Colors", 10, 80, 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, 105, 18, DARKGRAY);
        DrawText(TextFormat("Unlocked bullets: %d/%d", player.unlockedCount, MAX_PASSIVE_NODES), 10, 130, 16, DARKGRAY);
        DrawText(TextFormat("Bosses defeated: %d", bossesDefeated), 10, 155, 16, DARKGRAY);
      }

    EndDrawing();
  }
  return 0;
}