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