Mercurial
view color_game/main.c @ 70:4bc56e88e1f3
Remove unnecessary files.
| author | June Park <parkjune1995@gmail.com> |
|---|---|
| date | Thu, 25 Dec 2025 20:10:46 -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; }