comparison 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
comparison
equal deleted inserted replaced
62:ea9ef388ab97 63:fff1b048dda6
7 7
8 #define INIT_SCREEN_WIDTH 1200 8 #define INIT_SCREEN_WIDTH 1200
9 #define INIT_SCREEN_HEIGHT 700 9 #define INIT_SCREEN_HEIGHT 700
10 #define PLAYER_SPEED 200.0f 10 #define PLAYER_SPEED 200.0f
11 #define PLAYER_RADIUS 20.0f 11 #define PLAYER_RADIUS 20.0f
12 #define PLAYER_MAX_HEALTH 100.0f
13
12 #define MONSTER_SPEED 100.0f 14 #define MONSTER_SPEED 100.0f
13 #define MONSTER_RADIUS 15.0f 15 #define MONSTER_RADIUS 15.0f
16
14 #define BULLET_SPEED 300.0f 17 #define BULLET_SPEED 300.0f
15 #define BULLET_RADIUS 5.0f 18 #define BULLET_RADIUS 5.0f
16 #define BULLET_LIFETIME 3.0f 19 #define BULLET_LIFETIME 3.0f
17 #define SHOOT_INTERVAL 0.5f 20 #define SHOOT_INTERVAL 0.5f
18 #define HIT_PROBABILITY 0.7f // 70% aim accuracy (0.0 = random, 1.0 = perfect) 21 #define HIT_PROBABILITY 0.7f // 70% aim accuracy (0.0 = random, 1.0 = perfect)
22
19 #define MAX_PASSIVE_NODES 12 23 #define MAX_PASSIVE_NODES 12
20 #define PASSIVE_TREE_RADIUS 250.0f 24 #define PASSIVE_TREE_RADIUS 250.0f
21 #define PASSIVE_NODE_RADIUS 20.0f 25 #define PASSIVE_NODE_RADIUS 20.0f
26
27 #define MONSTER_WEIGHT 10.0f
28 #define MONSTER_CONTACT_DAMAGE 5.0f
29
22 #define BASE_DAMAGE 10.0f 30 #define BASE_DAMAGE 10.0f
23 #define PLAYER_MAX_HEALTH 100.0f
24 #define MONSTER_CONTACT_DAMAGE 5.0f
25 #define BOSS_HEALTH_MULTIPLIER 5.0f 31 #define BOSS_HEALTH_MULTIPLIER 5.0f
26 #define COLOR_UNLOCK_TIME 30.0f 32 #define BOSS_MONSTER_WEIGHT 10.0f
27 #define BOSS_SPAWN_TIME 60.0f 33 #define BOSS_SPAWN_TIME 10.0f
34
35 #define COLOR_UNLOCK_TIME 5.0f
36 #define MAX_DAMAGE_NUMBERS 100
37 #define DAMAGE_NUMBER_LIFETIME 1.0f
38 #define EXP_PER_MONSTER 10.0f
39 #define EXP_PER_BOSS 50.0f
40 #define EXP_TO_LEVEL 100.0f
28 41
29 typedef struct Player { 42 typedef struct Player {
30 Vector2 position; 43 Vector2 position;
31 Color color;
32 float health; 44 float health;
33 float maxHealth; 45 float maxHealth;
46 float invulnerabilityTimer;
34 int unlockedBulletTypes[MAX_PASSIVE_NODES]; 47 int unlockedBulletTypes[MAX_PASSIVE_NODES];
35 int unlockedCount; 48 int unlockedCount;
36 int passivePoints; 49 int passivePoints;
37 float invulnerabilityTimer; 50 int level;
51 float experience;
52 float expToNextLevel;
53 Color color;
38 } Player; 54 } Player;
55
56 typedef struct DamageNumber {
57 Vector2 position;
58 float damage;
59 float lifetime;
60 bool active;
61 Color color;
62 } DamageNumber;
39 63
40 typedef struct Map { 64 typedef struct Map {
41 float width; 65 float width;
42 float height; 66 float height;
43 } Map; 67 } Map;
46 Vector2 position; 70 Vector2 position;
47 float hue; 71 float hue;
48 float saturation; 72 float saturation;
49 float health; 73 float health;
50 float maxHealth; 74 float maxHealth;
75 float weight;
51 bool alive; 76 bool alive;
52 bool hasCollision; 77 bool hasCollision;
53 bool isBoss; 78 bool isBoss;
54 } Monster; 79 } Monster;
55 80
85 // Same color (0 degrees) = 0.5x damage 110 // Same color (0 degrees) = 0.5x damage
86 // Linear scale between them 111 // Linear scale between them
87 float damageMultiplier = 0.5f + (diff / 180.0f) * 1.5f; 112 float damageMultiplier = 0.5f + (diff / 180.0f) * 1.5f;
88 113
89 return baseDamage * damageMultiplier; 114 return baseDamage * damageMultiplier;
115 }
116
117 void SpawnDamageNumber(DamageNumber* damageNumbers, int maxCount, Vector2 position, float damage, Color color)
118 {
119 for (int i = 0; i < maxCount; i++)
120 {
121 if (!damageNumbers[i].active)
122 {
123 damageNumbers[i].position = position;
124 damageNumbers[i].damage = damage;
125 damageNumbers[i].lifetime = DAMAGE_NUMBER_LIFETIME;
126 damageNumbers[i].active = true;
127 damageNumbers[i].color = color;
128 break;
129 }
130 }
131 }
132
133 void UpdateDamageNumbers(DamageNumber* damageNumbers, int count, float deltaTime)
134 {
135 for (int i = 0; i < count; i++)
136 {
137 if (!damageNumbers[i].active) continue;
138
139 damageNumbers[i].lifetime -= deltaTime;
140 damageNumbers[i].position.y -= 30.0f * deltaTime; // Float upward
141
142 if (damageNumbers[i].lifetime <= 0)
143 {
144 damageNumbers[i].active = false;
145 }
146 }
147 }
148
149 void AddExperience(Player* player, float exp)
150 {
151 player->experience += exp;
152
153 // Check for level up
154 while (player->experience >= player->expToNextLevel)
155 {
156 player->experience -= player->expToNextLevel;
157 player->level++;
158 player->passivePoints++;
159 // Increase exp requirement for next level
160 player->expToNextLevel = EXP_TO_LEVEL * player->level;
161 }
90 } 162 }
91 163
92 void InitializePassiveTree(PassiveNode* nodes) 164 void InitializePassiveTree(PassiveNode* nodes)
93 { 165 {
94 const char* nodeDescriptions[MAX_PASSIVE_NODES] = { 166 const char* nodeDescriptions[MAX_PASSIVE_NODES] = {
173 }; 245 };
174 246
175 // Check collision with other monsters if this monster has collision enabled 247 // Check collision with other monsters if this monster has collision enabled
176 if (monsters[i].hasCollision) 248 if (monsters[i].hasCollision)
177 { 249 {
178 bool collision = false; 250 bool canMove = true;
179 for (int j = 0; j < monsterCount; j++) 251 for (int j = 0; j < monsterCount; j++)
180 { 252 {
181 if (i == j || !monsters[j].alive || !monsters[j].hasCollision) continue; 253 if (i == j || !monsters[j].alive || !monsters[j].hasCollision) continue;
182 254
183 float dx = newPos.x - monsters[j].position.x; 255 float dx = newPos.x - monsters[j].position.x;
184 float dy = newPos.y - monsters[j].position.y; 256 float dy = newPos.y - monsters[j].position.y;
185 float distance = sqrtf(dx * dx + dy * dy); 257 float distance = sqrtf(dx * dx + dy * dy);
186 258
187 if (distance < MONSTER_RADIUS * 2) 259 if (distance < MONSTER_RADIUS * 2)
188 { 260 {
189 collision = true; 261 // If this monster is heavier, push the other monster away
190 break; 262 if (monsters[i].weight > monsters[j].weight)
263 {
264 // Calculate push direction (away from current monster)
265 Vector2 pushDir = {dx, dy};
266 float pushLength = sqrtf(pushDir.x * pushDir.x + pushDir.y * pushDir.y);
267 if (pushLength > 0)
268 {
269 pushDir.x /= pushLength;
270 pushDir.y /= pushLength;
271 }
272
273 // Push the lighter monster away
274 float pushForce = (monsters[i].weight - monsters[j].weight) * MONSTER_SPEED * deltaTime * 0.5f;
275 monsters[j].position.x += pushDir.x * pushForce;
276 monsters[j].position.y += pushDir.y * pushForce;
277 }
278 else
279 {
280 // This monster is lighter or equal weight, can't move through
281 canMove = false;
282 break;
283 }
191 } 284 }
192 } 285 }
193 286
194 if (!collision) 287 if (canMove)
195 { 288 {
196 monsters[i].position = newPos; 289 monsters[i].position = newPos;
197 } 290 }
198 } 291 }
199 else 292 else
246 bullets[i].active = false; 339 bullets[i].active = false;
247 } 340 }
248 } 341 }
249 } 342 }
250 343
251 int CheckCollisions(Bullet* bullets, int bulletCount, Monster* monsters, int monsterCount, Player* player) 344 int CheckCollisions(Bullet* bullets, int bulletCount, Monster* monsters, int monsterCount, Player* player, DamageNumber* damageNumbers)
252 { 345 {
253 int bossesKilled = 0; 346 int bossesKilled = 0;
254 347
255 for (int i = 0; i < bulletCount; i++) 348 for (int i = 0; i < bulletCount; i++)
256 { 349 {
269 { 362 {
270 // Calculate damage based on color difference 363 // Calculate damage based on color difference
271 float damage = CalculateColorDamage(bullets[i].hue, monsters[j].hue, bullets[i].damage); 364 float damage = CalculateColorDamage(bullets[i].hue, monsters[j].hue, bullets[i].damage);
272 monsters[j].health -= damage; 365 monsters[j].health -= damage;
273 366
367 // Spawn damage number
368 Color damageColor = damage > bullets[i].damage * 1.5f ? ORANGE : WHITE;
369 SpawnDamageNumber(damageNumbers, MAX_DAMAGE_NUMBERS, monsters[j].position, damage, damageColor);
370
274 // Check if monster died 371 // Check if monster died
275 if (monsters[j].health <= 0) 372 if (monsters[j].health <= 0)
276 { 373 {
277 monsters[j].alive = false; 374 monsters[j].alive = false;
278 375
376 // Award experience
377 float expGain = monsters[j].isBoss ? EXP_PER_BOSS : EXP_PER_MONSTER;
378 AddExperience(player, expGain);
379
279 // Award passive point if boss was killed 380 // Award passive point if boss was killed
280 if (monsters[j].isBoss) 381 if (monsters[j].isBoss)
281 { 382 {
282 player->passivePoints++; 383 player->passivePoints++;
283 bossesKilled++; 384 bossesKilled++;
311 { 412 {
312 minDistance = distance; 413 minDistance = distance;
313 nearestPos = monsters[i].position; 414 nearestPos = monsters[i].position;
314 found = true; 415 found = true;
315 } 416 }
417
418 if (found)
419 break;
316 } 420 }
317 421
318 if (!found) 422 if (!found)
319 { 423 {
320 // No monsters, return random direction 424 // No monsters, return random direction
338 Map map = { 442 Map map = {
339 .width = INIT_SCREEN_WIDTH * 3, 443 .width = INIT_SCREEN_WIDTH * 3,
340 .height = INIT_SCREEN_HEIGHT * 3 444 .height = INIT_SCREEN_HEIGHT * 3
341 }; 445 };
342 446
343 // Initialize player at map center
344 Player player = { 447 Player player = {
345 .position = {map.width / 2, map.height / 2}, 448 .position = {map.width / 2, map.height / 2},
346 .color = BLUE, 449 .color = BLUE,
347 .health = PLAYER_MAX_HEALTH, 450 .health = PLAYER_MAX_HEALTH,
348 .maxHealth = PLAYER_MAX_HEALTH, 451 .maxHealth = PLAYER_MAX_HEALTH,
349 .unlockedCount = 0, 452 .unlockedCount = 0,
350 .passivePoints = 0, 453 .passivePoints = 3,
351 .invulnerabilityTimer = 0.0f 454 .invulnerabilityTimer = 0.0f,
455 .level = 1,
456 .experience = 0.0f,
457 .expToNextLevel = EXP_TO_LEVEL
352 }; 458 };
353 459
354 // Initialize passive tree
355 PassiveNode passiveTree[MAX_PASSIVE_NODES]; 460 PassiveNode passiveTree[MAX_PASSIVE_NODES];
356 InitializePassiveTree(passiveTree); 461 InitializePassiveTree(passiveTree);
357
358 // Unlock first node only (grayscale bullet to start)
359 passiveTree[0].unlocked = true; 462 passiveTree[0].unlocked = true;
360 player.unlockedBulletTypes[player.unlockedCount++] = 0; 463 player.unlockedBulletTypes[player.unlockedCount++] = 0;
361 464
362 // Initialize camera 465 // Initialize damage numbers array
466 DamageNumber damageNumbers[MAX_DAMAGE_NUMBERS];
467 for (int i = 0; i < MAX_DAMAGE_NUMBERS; i++)
468 {
469 damageNumbers[i].active = false;
470 }
471
363 Camera2D camera = {0}; 472 Camera2D camera = {0};
364 camera.target = player.position; 473 camera.target = player.position;
365 camera.offset = (Vector2){INIT_SCREEN_WIDTH / 2.0f, INIT_SCREEN_HEIGHT / 2.0f}; 474 camera.offset = (Vector2){INIT_SCREEN_WIDTH / 2.0f, INIT_SCREEN_HEIGHT / 2.0f};
366 camera.rotation = 0.0f; 475 camera.rotation = 0.0f;
367 camera.zoom = 1.0f; 476 camera.zoom = 1.0f;
372 Vector2 menuCenter = { 481 Vector2 menuCenter = {
373 .x = INIT_SCREEN_WIDTH / 2, 482 .x = INIT_SCREEN_WIDTH / 2,
374 .y = INIT_SCREEN_HEIGHT / 2 483 .y = INIT_SCREEN_HEIGHT / 2
375 }; 484 };
376 485
377 // Game progression variables
378 float gameTime = 0.0f; 486 float gameTime = 0.0f;
379 float currentMonsterHue = 0.0f; 487 float currentMonsterHue = 0.0f;
380 float currentMonsterSaturation = 0.0f; // Start grayscale 488 float currentMonsterSaturation = 0.0f; // Start grayscale
381 int bossesDefeated = 0; 489 int bossesDefeated = 0;
382 float nextBossTime = BOSS_SPAWN_TIME; 490 float nextBossTime = BOSS_SPAWN_TIME;
383 bool colorUnlocked = false; 491 bool colorUnlocked = false;
384 492
385 // Initialize monsters array 493 // Initialize monsters array
386 #define MAX_MONSTERS 100 494 #define MAX_MONSTERS 1000
387 Monster live_monsters[MAX_MONSTERS]; 495 Monster live_monsters[MAX_MONSTERS];
388 int monsterCount = 0; 496 int monsterCount = 0;
389 497
390 // Spawn initial monsters (grayscale) 498 // Spawn initial monsters (grayscale)
391 for (int i = 0; i < 10; i++) 499 for (int i = 0; i < 30; i++)
392 { 500 {
393 live_monsters[monsterCount++] = (Monster){ 501 live_monsters[monsterCount++] = (Monster){
394 .position = {RandomFloat(0, map.width), RandomFloat(0, map.height)}, 502 .position = {RandomFloat(0, map.width), RandomFloat(0, map.height)},
395 .hue = currentMonsterHue, 503 .hue = currentMonsterHue,
396 .saturation = currentMonsterSaturation, 504 .saturation = currentMonsterSaturation,
505 .weight = MONSTER_WEIGHT,
397 .health = 50.0f, 506 .health = 50.0f,
398 .maxHealth = 50.0f, 507 .maxHealth = 50.0f,
399 .alive = true, 508 .alive = true,
400 .hasCollision = true, 509 .hasCollision = true,
401 .isBoss = false 510 .isBoss = false
486 .position = {RandomFloat(0, map.width), RandomFloat(0, map.height)}, 595 .position = {RandomFloat(0, map.width), RandomFloat(0, map.height)},
487 .hue = currentMonsterHue, 596 .hue = currentMonsterHue,
488 .saturation = currentMonsterSaturation, 597 .saturation = currentMonsterSaturation,
489 .health = 50.0f * BOSS_HEALTH_MULTIPLIER, 598 .health = 50.0f * BOSS_HEALTH_MULTIPLIER,
490 .maxHealth = 50.0f * BOSS_HEALTH_MULTIPLIER, 599 .maxHealth = 50.0f * BOSS_HEALTH_MULTIPLIER,
600 .weight = BOSS_MONSTER_WEIGHT,
491 .alive = true, 601 .alive = true,
492 .hasCollision = true, 602 .hasCollision = true,
493 .isBoss = true 603 .isBoss = true
494 }; 604 };
495 } 605 }
578 UpdateMonsters(live_monsters, monsterCount, player.position, deltaTime); 688 UpdateMonsters(live_monsters, monsterCount, player.position, deltaTime);
579 689
580 // Update bullets 690 // Update bullets
581 UpdateBullets(bullets, MAX_BULLETS, deltaTime); 691 UpdateBullets(bullets, MAX_BULLETS, deltaTime);
582 692
693 // Update damage numbers
694 UpdateDamageNumbers(damageNumbers, MAX_DAMAGE_NUMBERS, deltaTime);
695
583 // Check collisions 696 // Check collisions
584 int bossKills = CheckCollisions(bullets, MAX_BULLETS, live_monsters, monsterCount, &player); 697 int bossKills = CheckCollisions(bullets, MAX_BULLETS, live_monsters, monsterCount, &player, damageNumbers);
585 bossesDefeated += bossKills; 698 bossesDefeated += bossKills;
586 699
587 // Update camera to follow player 700 // Update camera to follow player
588 camera.target = player.position; 701 camera.target = player.position;
589 702
726 Color bulletColor = ColorFromHSV(bullets[i].hue, 1.0f, 1.0f); 839 Color bulletColor = ColorFromHSV(bullets[i].hue, 1.0f, 1.0f);
727 DrawCircleV(bullets[i].position, BULLET_RADIUS, bulletColor); 840 DrawCircleV(bullets[i].position, BULLET_RADIUS, bulletColor);
728 } 841 }
729 } 842 }
730 843
844 // Draw damage numbers
845 for (int i = 0; i < MAX_DAMAGE_NUMBERS; i++)
846 {
847 if (damageNumbers[i].active)
848 {
849 printf("True\n");
850 float alpha = damageNumbers[i].lifetime / DAMAGE_NUMBER_LIFETIME;
851 Color color = damageNumbers[i].color;
852 color.a = (unsigned char)(255 * alpha);
853 DrawText(TextFormat("%.0f", damageNumbers[i].damage),
854 damageNumbers[i].position.x - 10,
855 damageNumbers[i].position.y,
856 20,
857 color);
858 }
859 }
860
731 // Draw player (flash when invulnerable) 861 // Draw player (flash when invulnerable)
732 if (player.invulnerabilityTimer <= 0 || ((int)(player.invulnerabilityTimer * 10) % 2 == 0)) 862 if (player.invulnerabilityTimer <= 0 || ((int)(player.invulnerabilityTimer * 10) % 2 == 0))
733 { 863 {
734 DrawCircleV(player.position, PLAYER_RADIUS, player.color); 864 DrawCircleV(player.position, PLAYER_RADIUS, player.color);
735 } 865 }
746 float healthPercent = player.health / player.maxHealth; 876 float healthPercent = player.health / player.maxHealth;
747 DrawRectangle(10, 10, 200, 20, DARKGRAY); 877 DrawRectangle(10, 10, 200, 20, DARKGRAY);
748 DrawRectangle(10, 10, 200 * healthPercent, 20, RED); 878 DrawRectangle(10, 10, 200 * healthPercent, 20, RED);
749 DrawText(TextFormat("HP: %.0f/%.0f", player.health, player.maxHealth), 15, 12, 16, WHITE); 879 DrawText(TextFormat("HP: %.0f/%.0f", player.health, player.maxHealth), 15, 12, 16, WHITE);
750 880
881 // Experience bar
882 float expPercent = player.experience / player.expToNextLevel;
883 DrawRectangle(10, 35, 200, 15, DARKGRAY);
884 DrawRectangle(10, 35, 200 * expPercent, 15, SKYBLUE);
885 DrawText(TextFormat("Lvl %d", player.level), 15, 36, 12, WHITE);
886 DrawText(TextFormat("%.0f/%.0f XP", player.experience, player.expToNextLevel), 70, 36, 12, WHITE);
887
751 // Passive points 888 // Passive points
752 DrawText(TextFormat("Passive Points: %d", player.passivePoints), 10, 40, 20, PURPLE); 889 DrawText(TextFormat("Passive Points: %d", player.passivePoints), 10, 55, 18, PURPLE);
753 890
754 // Controls 891 // Controls
755 DrawText("WASD: Move | P: Menu | R: Toggle Colors", 10, 70, 16, DARKGRAY); 892 DrawText("WASD: Move | P: Menu | R: Toggle Colors", 10, 80, 16, DARKGRAY);
756 893
757 // Count alive monsters and bosses 894 // Count alive monsters and bosses
758 int aliveCount = 0; 895 int aliveCount = 0;
759 int bossCount = 0; 896 int bossCount = 0;
760 for (int i = 0; i < monsterCount; i++) 897 for (int i = 0; i < monsterCount; i++)
763 { 900 {
764 aliveCount++; 901 aliveCount++;
765 if (live_monsters[i].isBoss) bossCount++; 902 if (live_monsters[i].isBoss) bossCount++;
766 } 903 }
767 } 904 }
768 DrawText(TextFormat("Monsters: %d | Bosses: %d", aliveCount, bossCount), 10, 95, 18, DARKGRAY); 905 DrawText(TextFormat("Monsters: %d | Bosses: %d", aliveCount, bossCount), 10, 105, 18, DARKGRAY);
769 DrawText(TextFormat("Unlocked bullets: %d/%d", player.unlockedCount, MAX_PASSIVE_NODES), 10, 120, 16, DARKGRAY); 906 DrawText(TextFormat("Unlocked bullets: %d/%d", player.unlockedCount, MAX_PASSIVE_NODES), 10, 130, 16, DARKGRAY);
770 DrawText(TextFormat("Bosses defeated: %d", bossesDefeated), 10, 145, 16, DARKGRAY); 907 DrawText(TextFormat("Bosses defeated: %d", bossesDefeated), 10, 155, 16, DARKGRAY);
771 } 908 }
772 909
773 EndDrawing(); 910 EndDrawing();
774 } 911 }
775 return 0; 912 return 0;