diff color_game/main.c @ 63:fff1b048dda6

[Postdog] Fixed a problem where string did not wrap.
author June Park <parkjune1995@gmail.com>
date Tue, 23 Dec 2025 14:00:37 -0800
parents 9df5587cf23b
children 35b1abc37969
line wrap: on
line diff
--- a/color_game/main.c	Tue Dec 23 11:48:11 2025 -0800
+++ b/color_game/main.c	Tue Dec 23 14:00:37 2025 -0800
@@ -9,34 +9,58 @@
 #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 BASE_DAMAGE 10.0f
-#define PLAYER_MAX_HEALTH 100.0f
+
+#define MONSTER_WEIGHT 10.0f
 #define MONSTER_CONTACT_DAMAGE 5.0f
+
+#define BASE_DAMAGE 10.0f
 #define BOSS_HEALTH_MULTIPLIER 5.0f
-#define COLOR_UNLOCK_TIME 30.0f
-#define BOSS_SPAWN_TIME 60.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;
-  Color color;
   float health;
   float maxHealth;
+  float invulnerabilityTimer;
   int unlockedBulletTypes[MAX_PASSIVE_NODES];
   int unlockedCount;
   int passivePoints;
-  float invulnerabilityTimer;
+  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;
@@ -48,6 +72,7 @@
   float saturation;
   float health;
   float maxHealth;
+  float weight;
   bool alive;
   bool hasCollision;
   bool isBoss;
@@ -89,6 +114,53 @@
   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] = {
@@ -175,7 +247,7 @@
     // Check collision with other monsters if this monster has collision enabled
     if (monsters[i].hasCollision)
     {
-      bool collision = false;
+      bool canMove = true;
       for (int j = 0; j < monsterCount; j++)
       {
         if (i == j || !monsters[j].alive || !monsters[j].hasCollision) continue;
@@ -186,12 +258,33 @@
 
         if (distance < MONSTER_RADIUS * 2)
         {
-          collision = true;
-          break;
+          // 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 (!collision)
+      if (canMove)
       {
         monsters[i].position = newPos;
       }
@@ -248,7 +341,7 @@
   }
 }
 
-int CheckCollisions(Bullet* bullets, int bulletCount, Monster* monsters, int monsterCount, Player* player)
+int CheckCollisions(Bullet* bullets, int bulletCount, Monster* monsters, int monsterCount, Player* player, DamageNumber* damageNumbers)
 {
   int bossesKilled = 0;
 
@@ -271,11 +364,19 @@
         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)
           {
@@ -313,6 +414,9 @@
       nearestPos = monsters[i].position;
       found = true;
     }
+
+    if (found)
+      break;
   }
 
   if (!found)
@@ -340,26 +444,31 @@
     .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
+    .passivePoints = 3,
+    .invulnerabilityTimer = 0.0f,
+    .level = 1,
+    .experience = 0.0f,
+    .expToNextLevel = EXP_TO_LEVEL
   };
 
-  // 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
+  // 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};
@@ -374,7 +483,6 @@
     .y = INIT_SCREEN_HEIGHT / 2
   };
 
-  // Game progression variables
   float gameTime = 0.0f;
   float currentMonsterHue = 0.0f;
   float currentMonsterSaturation = 0.0f; // Start grayscale
@@ -383,17 +491,18 @@
   bool colorUnlocked = false;
 
   // Initialize monsters array
-  #define MAX_MONSTERS 100
+  #define MAX_MONSTERS 1000
   Monster live_monsters[MAX_MONSTERS];
   int monsterCount = 0;
 
   // Spawn initial monsters (grayscale)
-  for (int i = 0; i < 10; i++)
+  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,
@@ -488,6 +597,7 @@
           .saturation = currentMonsterSaturation,
           .health = 50.0f * BOSS_HEALTH_MULTIPLIER,
           .maxHealth = 50.0f * BOSS_HEALTH_MULTIPLIER,
+          .weight = BOSS_MONSTER_WEIGHT,
           .alive = true,
           .hasCollision = true,
           .isBoss = true
@@ -580,8 +690,11 @@
       // 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);
+      int bossKills = CheckCollisions(bullets, MAX_BULLETS, live_monsters, monsterCount, &player, damageNumbers);
       bossesDefeated += bossKills;
 
       // Update camera to follow player
@@ -728,6 +841,23 @@
             }
           }
 
+          // 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))
           {
@@ -748,11 +878,18 @@
         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, 40, 20, PURPLE);
+        DrawText(TextFormat("Passive Points: %d", player.passivePoints), 10, 55, 18, PURPLE);
 
         // Controls
-        DrawText("WASD: Move | P: Menu | R: Toggle Colors", 10, 70, 16, DARKGRAY);
+        DrawText("WASD: Move | P: Menu | R: Toggle Colors", 10, 80, 16, DARKGRAY);
 
         // Count alive monsters and bosses
         int aliveCount = 0;
@@ -765,9 +902,9 @@
             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);
+        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();