Simple tower defense tutorial, part 11: Enemy path visualizations

In the last part, we added two new towers to the game. At the moment these are, visually speaking, just static boxes, but are at least functional. But there is some fundamental issue with the gameplay: While things work on a mechanical level, the player currently can't anticipate how the enemies will move through the level as tower placement can change this a lot, making planning impossible.

So in this part, we should add a visualization of the path the enemies will take and allowing to preview how the path will change before placing new towers.

Path visualization

Our pathfinding system provides only the next cell the enemy will move to. But this way we can draw any path by simply starting with a point.

Let's just start drawing some cubes along the way the enemies will move, beginning from the spawn point, before thinking much about how to visualize the path:

  • 💾
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 #include <stdlib.h>
  4 #include <math.h>
  5 
  6 //# Variables
  7 GUIState guiState = {0};
  8 GameTime gameTime = {0};
  9 
 10 Model floorTileAModel = {0};
 11 Model floorTileBModel = {0};
 12 Model treeModel[2] = {0};
 13 Model firTreeModel[2] = {0};
 14 Model rockModels[5] = {0};
 15 Model grassPatchModel[1] = {0};
 16 
 17 Texture2D palette, spriteSheet;
 18 
 19 Level levels[] = {
 20   [0] = {
 21     .state = LEVEL_STATE_BUILDING,
 22     .initialGold = 20,
 23     .waves[0] = {
 24       .enemyType = ENEMY_TYPE_MINION,
 25       .wave = 0,
 26       .count = 10,
 27       .interval = 2.5f,
 28       .delay = 1.0f,
 29       .spawnPosition = {0, 6},
 30     },
 31     .waves[1] = {
 32       .enemyType = ENEMY_TYPE_MINION,
 33       .wave = 1,
 34       .count = 20,
 35       .interval = 1.5f,
 36       .delay = 1.0f,
 37       .spawnPosition = {0, 6},
 38     },
 39     .waves[2] = {
 40       .enemyType = ENEMY_TYPE_MINION,
 41       .wave = 2,
 42       .count = 30,
 43       .interval = 1.2f,
 44       .delay = 1.0f,
 45       .spawnPosition = {0, 6},
 46     }
 47   },
 48 };
 49 
 50 Level *currentLevel = levels;
 51 
 52 //# Game
 53 
 54 static Model LoadGLBModel(char *filename)
 55 {
 56   Model model = LoadModel(TextFormat("data/%s.glb",filename));
 57   if (model.materialCount > 1)
 58   {
 59     model.materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
 60   }
 61   return model;
 62 }
 63 
 64 void LoadAssets()
 65 {
 66   // load a sprite sheet that contains all units
 67   spriteSheet = LoadTexture("data/spritesheet.png");
 68   SetTextureFilter(spriteSheet, TEXTURE_FILTER_BILINEAR);
 69 
 70   // we'll use a palette texture to colorize the all buildings and environment art
 71   palette = LoadTexture("data/palette.png");
 72   // The texture uses gradients on very small space, so we'll enable bilinear filtering
 73   SetTextureFilter(palette, TEXTURE_FILTER_BILINEAR);
 74 
 75   floorTileAModel = LoadGLBModel("floor-tile-a");
 76   floorTileBModel = LoadGLBModel("floor-tile-b");
 77   treeModel[0] = LoadGLBModel("leaftree-large-1-a");
 78   treeModel[1] = LoadGLBModel("leaftree-large-1-b");
 79   firTreeModel[0] = LoadGLBModel("firtree-1-a");
 80   firTreeModel[1] = LoadGLBModel("firtree-1-b");
 81   rockModels[0] = LoadGLBModel("rock-1");
 82   rockModels[1] = LoadGLBModel("rock-2");
 83   rockModels[2] = LoadGLBModel("rock-3");
 84   rockModels[3] = LoadGLBModel("rock-4");
 85   rockModels[4] = LoadGLBModel("rock-5");
 86   grassPatchModel[0] = LoadGLBModel("grass-patch-1");
 87 }
 88 
 89 void InitLevel(Level *level)
 90 {
 91   level->seed = (int)(GetTime() * 100.0f);
 92 
 93   TowerInit();
 94   EnemyInit();
 95   ProjectileInit();
 96   ParticleInit();
 97   TowerTryAdd(TOWER_TYPE_BASE, 0, 0);
 98 
 99   level->placementMode = 0;
100   level->state = LEVEL_STATE_BUILDING;
101   level->nextState = LEVEL_STATE_NONE;
102   level->playerGold = level->initialGold;
103   level->currentWave = 0;
104 
105   Camera *camera = &level->camera;
106   camera->position = (Vector3){4.0f, 8.0f, 8.0f};
107   camera->target = (Vector3){0.0f, 0.0f, 0.0f};
108   camera->up = (Vector3){0.0f, 1.0f, 0.0f};
109   camera->fovy = 10.0f;
110   camera->projection = CAMERA_ORTHOGRAPHIC;
111 }
112 
113 void DrawLevelHud(Level *level)
114 {
115   const char *text = TextFormat("Gold: %d", level->playerGold);
116   Font font = GetFontDefault();
117   DrawTextEx(font, text, (Vector2){GetScreenWidth() - 120, 10}, font.baseSize * 2.0f, 2.0f, BLACK);
118   DrawTextEx(font, text, (Vector2){GetScreenWidth() - 122, 8}, font.baseSize * 2.0f, 2.0f, YELLOW);
119 }
120 
121 void DrawLevelReportLostWave(Level *level)
122 {
123   BeginMode3D(level->camera);
124   DrawLevelGround(level);
125   TowerDraw();
126   EnemyDraw();
127   ProjectileDraw();
128   ParticleDraw();
129   guiState.isBlocked = 0;
130   EndMode3D();
131 
132   TowerDrawHealthBars(level->camera);
133 
134   const char *text = "Wave lost";
135   int textWidth = MeasureText(text, 20);
136   DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
137 
138   if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
139   {
140     level->nextState = LEVEL_STATE_RESET;
141   }
142 }
143 
144 int HasLevelNextWave(Level *level)
145 {
146   for (int i = 0; i < 10; i++)
147   {
148     EnemyWave *wave = &level->waves[i];
149     if (wave->wave == level->currentWave)
150     {
151       return 1;
152     }
153   }
154   return 0;
155 }
156 
157 void DrawLevelReportWonWave(Level *level)
158 {
159   BeginMode3D(level->camera);
160   DrawLevelGround(level);
161   TowerDraw();
162   EnemyDraw();
163   ProjectileDraw();
164   ParticleDraw();
165   guiState.isBlocked = 0;
166   EndMode3D();
167 
168   TowerDrawHealthBars(level->camera);
169 
170   const char *text = "Wave won";
171   int textWidth = MeasureText(text, 20);
172   DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
173 
174 
175   if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
176   {
177     level->nextState = LEVEL_STATE_RESET;
178   }
179 
180   if (HasLevelNextWave(level))
181   {
182     if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
183     {
184       level->nextState = LEVEL_STATE_BUILDING;
185     }
186   }
187   else {
188     if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
189     {
190       level->nextState = LEVEL_STATE_WON_LEVEL;
191     }
192   }
193 }
194 
195 void DrawBuildingBuildButton(Level *level, int x, int y, int width, int height, uint8_t towerType, const char *name)
196 {
197   static ButtonState buttonStates[8] = {0};
198   int cost = GetTowerCosts(towerType);
199   const char *text = TextFormat("%s: %d", name, cost);
200   buttonStates[towerType].isSelected = level->placementMode == towerType;
201   buttonStates[towerType].isDisabled = level->playerGold < cost;
202   if (Button(text, x, y, width, height, &buttonStates[towerType]))
203   {
204     level->placementMode = buttonStates[towerType].isSelected ? 0 : towerType;
205   }
206 }
207 
208 float GetRandomFloat(float min, float max)
209 {
210   int random = GetRandomValue(0, 0xfffffff);
211   return ((float)random / (float)0xfffffff) * (max - min) + min;
212 }
213 
214 void DrawLevelGround(Level *level)
215 {
216   // draw checkerboard ground pattern
217   for (int x = -5; x <= 5; x += 1)
218   {
219     for (int y = -5; y <= 5; y += 1)
220     {
221       Model *model = (x + y) % 2 == 0 ? &floorTileAModel : &floorTileBModel;
222       DrawModel(*model, (Vector3){x, 0.0f, y}, 1.0f, WHITE);
223     }
224   }
225 
226   int oldSeed = GetRandomValue(0, 0xfffffff);
227   SetRandomSeed(level->seed);
228   // increase probability for trees via duplicated entries
229   Model borderModels[64];
230   int maxRockCount = GetRandomValue(2, 6);
231   int maxTreeCount = GetRandomValue(10, 20);
232   int maxFirTreeCount = GetRandomValue(5, 10);
233   int maxLeafTreeCount = maxTreeCount - maxFirTreeCount;
234   int grassPatchCount = GetRandomValue(5, 30);
235 
236   int modelCount = 0;
237   for (int i = 0; i < maxRockCount && modelCount < 63; i++)
238   {
239     borderModels[modelCount++] = rockModels[GetRandomValue(0, 5)];
240   }
241   for (int i = 0; i < maxLeafTreeCount && modelCount < 63; i++)
242   {
243     borderModels[modelCount++] = treeModel[GetRandomValue(0, 1)];
244   }
245   for (int i = 0; i < maxFirTreeCount && modelCount < 63; i++)
246   {
247     borderModels[modelCount++] = firTreeModel[GetRandomValue(0, 1)];
248   }
249   for (int i = 0; i < grassPatchCount && modelCount < 63; i++)
250   {
251     borderModels[modelCount++] = grassPatchModel[0];
252   }
253 
254   // draw some objects around the border of the map
255   Vector3 up = {0, 1, 0};
256   // a pseudo random number generator to get the same result every time
257   const float wiggle = 0.75f;
258   const int layerCount = 3;
259   for (int layer = 0; layer < layerCount; layer++)
260   {
261     int layerPos = 6 + layer;
262     for (int x = -6 + layer; x <= 6 + layer; x += 1)
263     {
264       DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)], 
265         (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, -layerPos + GetRandomFloat(0.0f, wiggle)}, 
266         up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
267       DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)], 
268         (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, layerPos + GetRandomFloat(0.0f, wiggle)}, 
269         up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
270     }
271 
272     for (int z = -5 + layer; z <= 5 + layer; z += 1)
273     {
274       DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)], 
275         (Vector3){-layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)}, 
276         up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
277       DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)], 
278         (Vector3){layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)}, 
279         up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
280     }
281   }
282 
283   SetRandomSeed(oldSeed);
284 }
285 
286 void DrawEnemyPath(Level *level) 287 { 288 for (int i = 0; i < ENEMY_MAX_WAVE_COUNT; i++) 289 { 290 EnemyWave *wave = &level->waves[i]; 291 if (wave->wave != level->currentWave) 292 { 293 continue; 294 } 295 Vector3 position = (Vector3){wave->spawnPosition.x, 0.0f, wave->spawnPosition.y}; 296 DrawCube(position, 0.25f, 2.0f, 0.25f, RED); 297 for (int j = 0; j < 100; j++) 298 { 299 Vector2 gradient = PathFindingGetGradient(position); 300 if (gradient.x == 0 && gradient.y == 0) 301 { 302 break; 303 } 304 position.x += gradient.x; 305 position.z += gradient.y; 306 DrawCube(position, 0.2f, 1.25f, 0.2f, ORANGE); 307 } 308 } 309 } 310
311 void DrawLevelBuildingState(Level *level) 312 { 313 BeginMode3D(level->camera);
314 DrawLevelGround(level); 315 DrawEnemyPath(level);
316 TowerDraw(); 317 EnemyDraw(); 318 ProjectileDraw(); 319 ParticleDraw(); 320 321 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera); 322 float planeDistance = ray.position.y / -ray.direction.y; 323 float planeX = ray.direction.x * planeDistance + ray.position.x; 324 float planeY = ray.direction.z * planeDistance + ray.position.z; 325 int16_t mapX = (int16_t)floorf(planeX + 0.5f); 326 int16_t mapY = (int16_t)floorf(planeY + 0.5f); 327 if (level->placementMode && !guiState.isBlocked && mapX >= -5 && mapX <= 5 && mapY >= -5 && mapY <= 5) 328 { 329 DrawCubeWires((Vector3){mapX, 0.2f, mapY}, 1.0f, 0.4f, 1.0f, RED); 330 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) 331 { 332 if (TowerTryAdd(level->placementMode, mapX, mapY)) 333 { 334 level->playerGold -= GetTowerCosts(level->placementMode); 335 level->placementMode = TOWER_TYPE_NONE; 336 } 337 } 338 } 339 340 guiState.isBlocked = 0; 341 342 EndMode3D(); 343 344 TowerDrawHealthBars(level->camera); 345 346 static ButtonState buildWallButtonState = {0}; 347 static ButtonState buildGunButtonState = {0}; 348 buildWallButtonState.isSelected = level->placementMode == TOWER_TYPE_WALL; 349 buildGunButtonState.isSelected = level->placementMode == TOWER_TYPE_ARCHER; 350 351 DrawBuildingBuildButton(level, 10, 10, 110, 30, TOWER_TYPE_WALL, "Wall"); 352 DrawBuildingBuildButton(level, 10, 50, 110, 30, TOWER_TYPE_ARCHER, "Archer"); 353 DrawBuildingBuildButton(level, 10, 90, 110, 30, TOWER_TYPE_BALLISTA, "Ballista"); 354 DrawBuildingBuildButton(level, 10, 130, 110, 30, TOWER_TYPE_CATAPULT, "Catapult"); 355 356 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 357 { 358 level->nextState = LEVEL_STATE_RESET; 359 } 360 361 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0)) 362 { 363 level->nextState = LEVEL_STATE_BATTLE; 364 } 365 366 const char *text = "Building phase"; 367 int textWidth = MeasureText(text, 20); 368 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 369 } 370 371 void InitBattleStateConditions(Level *level) 372 { 373 level->state = LEVEL_STATE_BATTLE; 374 level->nextState = LEVEL_STATE_NONE; 375 level->waveEndTimer = 0.0f; 376 for (int i = 0; i < 10; i++) 377 { 378 EnemyWave *wave = &level->waves[i]; 379 wave->spawned = 0; 380 wave->timeToSpawnNext = wave->delay; 381 } 382 } 383 384 void DrawLevelBattleState(Level *level) 385 { 386 BeginMode3D(level->camera); 387 DrawLevelGround(level); 388 TowerDraw(); 389 EnemyDraw(); 390 ProjectileDraw(); 391 ParticleDraw(); 392 guiState.isBlocked = 0; 393 EndMode3D(); 394 395 EnemyDrawHealthbars(level->camera); 396 TowerDrawHealthBars(level->camera); 397 398 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 399 { 400 level->nextState = LEVEL_STATE_RESET; 401 } 402 403 int maxCount = 0; 404 int remainingCount = 0; 405 for (int i = 0; i < 10; i++) 406 { 407 EnemyWave *wave = &level->waves[i]; 408 if (wave->wave != level->currentWave) 409 { 410 continue; 411 } 412 maxCount += wave->count; 413 remainingCount += wave->count - wave->spawned; 414 } 415 int aliveCount = EnemyCount(); 416 remainingCount += aliveCount; 417 418 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount); 419 int textWidth = MeasureText(text, 20); 420 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 421 } 422 423 void DrawLevel(Level *level) 424 { 425 switch (level->state) 426 { 427 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break; 428 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break; 429 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break; 430 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break; 431 default: break; 432 } 433 434 DrawLevelHud(level); 435 } 436 437 void UpdateLevel(Level *level) 438 { 439 if (level->state == LEVEL_STATE_BATTLE) 440 { 441 int activeWaves = 0; 442 for (int i = 0; i < 10; i++) 443 { 444 EnemyWave *wave = &level->waves[i]; 445 if (wave->spawned >= wave->count || wave->wave != level->currentWave) 446 { 447 continue; 448 } 449 activeWaves++; 450 wave->timeToSpawnNext -= gameTime.deltaTime; 451 if (wave->timeToSpawnNext <= 0.0f) 452 { 453 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y); 454 if (enemy) 455 { 456 wave->timeToSpawnNext = wave->interval; 457 wave->spawned++; 458 } 459 } 460 } 461 if (GetTowerByType(TOWER_TYPE_BASE) == 0) { 462 level->waveEndTimer += gameTime.deltaTime; 463 if (level->waveEndTimer >= 2.0f) 464 { 465 level->nextState = LEVEL_STATE_LOST_WAVE; 466 } 467 } 468 else if (activeWaves == 0 && EnemyCount() == 0) 469 { 470 level->waveEndTimer += gameTime.deltaTime; 471 if (level->waveEndTimer >= 2.0f) 472 { 473 level->nextState = LEVEL_STATE_WON_WAVE; 474 } 475 } 476 } 477 478 PathFindingMapUpdate(); 479 EnemyUpdate(); 480 TowerUpdate(); 481 ProjectileUpdate(); 482 ParticleUpdate(); 483 484 if (level->nextState == LEVEL_STATE_RESET) 485 { 486 InitLevel(level); 487 } 488 489 if (level->nextState == LEVEL_STATE_BATTLE) 490 { 491 InitBattleStateConditions(level); 492 } 493 494 if (level->nextState == LEVEL_STATE_WON_WAVE) 495 { 496 level->currentWave++; 497 level->state = LEVEL_STATE_WON_WAVE; 498 } 499 500 if (level->nextState == LEVEL_STATE_LOST_WAVE) 501 { 502 level->state = LEVEL_STATE_LOST_WAVE; 503 } 504 505 if (level->nextState == LEVEL_STATE_BUILDING) 506 { 507 level->state = LEVEL_STATE_BUILDING; 508 } 509 510 if (level->nextState == LEVEL_STATE_WON_LEVEL) 511 { 512 // make something of this later 513 InitLevel(level); 514 } 515 516 level->nextState = LEVEL_STATE_NONE; 517 } 518 519 float nextSpawnTime = 0.0f; 520 521 void ResetGame() 522 { 523 InitLevel(currentLevel); 524 } 525 526 void InitGame() 527 { 528 TowerInit(); 529 EnemyInit(); 530 ProjectileInit(); 531 ParticleInit(); 532 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f); 533 534 currentLevel = levels; 535 InitLevel(currentLevel); 536 } 537 538 //# Immediate GUI functions 539 540 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth) 541 { 542 const float healthBarHeight = 6.0f; 543 const float healthBarOffset = 15.0f; 544 const float inset = 2.0f; 545 const float innerWidth = healthBarWidth - inset * 2; 546 const float innerHeight = healthBarHeight - inset * 2; 547 548 Vector2 screenPos = GetWorldToScreen(position, camera); 549 float centerX = screenPos.x - healthBarWidth * 0.5f; 550 float topY = screenPos.y - healthBarOffset; 551 DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK); 552 float healthWidth = innerWidth * healthRatio; 553 DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor); 554 } 555 556 int Button(const char *text, int x, int y, int width, int height, ButtonState *state) 557 { 558 Rectangle bounds = {x, y, width, height}; 559 int isPressed = 0; 560 int isSelected = state && state->isSelected; 561 int isDisabled = state && state->isDisabled; 562 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !isDisabled) 563 { 564 Color color = isSelected ? DARKGRAY : GRAY; 565 DrawRectangle(x, y, width, height, color); 566 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) 567 { 568 isPressed = 1; 569 } 570 guiState.isBlocked = 1; 571 } 572 else 573 { 574 Color color = isSelected ? WHITE : LIGHTGRAY; 575 DrawRectangle(x, y, width, height, color); 576 } 577 Font font = GetFontDefault(); 578 Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1); 579 Color textColor = isDisabled ? GRAY : BLACK; 580 DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, textColor); 581 return isPressed; 582 } 583 584 //# Main game loop 585 586 void GameUpdate() 587 { 588 float dt = GetFrameTime(); 589 // cap maximum delta time to 0.1 seconds to prevent large time steps 590 if (dt > 0.1f) dt = 0.1f; 591 gameTime.time += dt; 592 gameTime.deltaTime = dt; 593 594 UpdateLevel(currentLevel); 595 } 596 597 int main(void) 598 { 599 int screenWidth, screenHeight; 600 GetPreferredSize(&screenWidth, &screenHeight); 601 InitWindow(screenWidth, screenHeight, "Tower defense"); 602 SetTargetFPS(30); 603 604 LoadAssets(); 605 InitGame(); 606 607 while (!WindowShouldClose()) 608 { 609 if (IsPaused()) { 610 // canvas is not visible in browser - do nothing 611 continue; 612 } 613 614 BeginDrawing(); 615 ClearBackground((Color){0x4E, 0x63, 0x26, 0xFF}); 616 617 GameUpdate(); 618 DrawLevel(currentLevel); 619 620 EndDrawing(); 621 } 622 623 CloseWindow(); 624 625 return 0; 626 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 
  4 static TowerTypeConfig towerTypeConfigs[TOWER_TYPE_COUNT] = {
  5     [TOWER_TYPE_BASE] = {
  6         .maxHealth = 10,
  7     },
  8     [TOWER_TYPE_ARCHER] = {
  9         .cooldown = 0.5f,
 10         .range = 3.0f,
 11         .cost = 6,
 12         .maxHealth = 10,
 13         .projectileSpeed = 4.0f,
 14         .projectileType = PROJECTILE_TYPE_ARROW,
 15         .hitEffect = {
 16           .damage = 3.0f,
 17         }
 18     },
 19     [TOWER_TYPE_BALLISTA] = {
 20         .cooldown = 1.5f,
 21         .range = 6.0f,
 22         .cost = 9,
 23         .maxHealth = 10,
 24         .projectileSpeed = 10.0f,
 25         .projectileType = PROJECTILE_TYPE_BALLISTA,
 26         .hitEffect = {
 27           .damage = 6.0f,
 28           .pushbackPowerDistance = 0.25f,
 29         }
 30     },
 31     [TOWER_TYPE_CATAPULT] = {
 32         .cooldown = 1.7f,
 33         .range = 5.0f,
 34         .cost = 10,
 35         .maxHealth = 10,
 36         .projectileSpeed = 3.0f,
 37         .projectileType = PROJECTILE_TYPE_CATAPULT,
 38         .hitEffect = {
 39           .damage = 2.0f,
 40           .areaDamageRadius = 1.75f,
 41         }
 42     },
 43     [TOWER_TYPE_WALL] = {
 44         .cost = 2,
 45         .maxHealth = 10,
 46     },
 47 };
 48 
 49 Tower towers[TOWER_MAX_COUNT];
 50 int towerCount = 0;
 51 
 52 Model towerModels[TOWER_TYPE_COUNT];
 53 
 54 // definition of our archer unit
 55 SpriteUnit archerUnit = {
 56     .srcRect = {0, 0, 16, 16},
 57     .offset = {7, 1},
 58     .frameCount = 1,
 59     .frameDuration = 0.0f,
 60     .srcWeaponIdleRect = {16, 0, 6, 16},
 61     .srcWeaponIdleOffset = {8, 0},
 62     .srcWeaponCooldownRect = {22, 0, 11, 16},
 63     .srcWeaponCooldownOffset = {10, 0},
 64 };
 65 
 66 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase)
 67 {
 68   float xScale = flip ? -1.0f : 1.0f;
 69   Camera3D camera = currentLevel->camera;
 70   float size = 0.5f;
 71   Vector2 offset = (Vector2){ unit.offset.x / 16.0f * size, unit.offset.y / 16.0f * size * xScale };
 72   Vector2 scale = (Vector2){ unit.srcRect.width / 16.0f * size, unit.srcRect.height / 16.0f * size };
 73   // we want the sprite to face the camera, so we need to calculate the up vector
 74   Vector3 forward = Vector3Subtract(camera.target, camera.position);
 75   Vector3 up = {0, 1, 0};
 76   Vector3 right = Vector3CrossProduct(forward, up);
 77   up = Vector3Normalize(Vector3CrossProduct(right, forward));
 78 
 79   Rectangle srcRect = unit.srcRect;
 80   if (unit.frameCount > 1)
 81   {
 82     srcRect.x += (int)(t / unit.frameDuration) % unit.frameCount * srcRect.width;
 83   }
 84   if (flip)
 85   {
 86     srcRect.x += srcRect.width;
 87     srcRect.width = -srcRect.width;
 88   }
 89   DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
 90 
 91   if (phase == SPRITE_UNIT_PHASE_WEAPON_COOLDOWN && unit.srcWeaponCooldownRect.width > 0)
 92   {
 93     offset = (Vector2){ unit.srcWeaponCooldownOffset.x / 16.0f * size, unit.srcWeaponCooldownOffset.y / 16.0f * size };
 94     scale = (Vector2){ unit.srcWeaponCooldownRect.width / 16.0f * size, unit.srcWeaponCooldownRect.height / 16.0f * size };
 95     srcRect = unit.srcWeaponCooldownRect;
 96     if (flip)
 97     {
 98       // position.x = flip * scale.x * 0.5f;
 99       srcRect.x += srcRect.width;
100       srcRect.width = -srcRect.width;
101       offset.x = scale.x - offset.x;
102     }
103     DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
104   }
105   else if (phase == SPRITE_UNIT_PHASE_WEAPON_IDLE && unit.srcWeaponIdleRect.width > 0)
106   {
107     offset = (Vector2){ unit.srcWeaponIdleOffset.x / 16.0f * size, unit.srcWeaponIdleOffset.y / 16.0f * size };
108     scale = (Vector2){ unit.srcWeaponIdleRect.width / 16.0f * size, unit.srcWeaponIdleRect.height / 16.0f * size };
109     srcRect = unit.srcWeaponIdleRect;
110     if (flip)
111     {
112       // position.x = flip * scale.x * 0.5f;
113       srcRect.x += srcRect.width;
114       srcRect.width = -srcRect.width;
115       offset.x = scale.x - offset.x;
116     }
117     DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
118   }
119 }
120 
121 void TowerInit()
122 {
123   for (int i = 0; i < TOWER_MAX_COUNT; i++)
124   {
125     towers[i] = (Tower){0};
126   }
127   towerCount = 0;
128 
129   towerModels[TOWER_TYPE_BASE] = LoadModel("data/keep.glb");
130   towerModels[TOWER_TYPE_WALL] = LoadModel("data/wall-0000.glb");
131 
132   for (int i = 0; i < TOWER_TYPE_COUNT; i++)
133   {
134     if (towerModels[i].materials)
135     {
136       // assign the palette texture to the material of the model (0 is not used afaik)
137       towerModels[i].materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
138     }
139   }
140 }
141 
142 static void TowerGunUpdate(Tower *tower)
143 {
144   TowerTypeConfig config = towerTypeConfigs[tower->towerType];
145   if (tower->cooldown <= 0.0f)
146   {
147     Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, config.range);
148     if (enemy)
149     {
150       tower->cooldown = config.cooldown;
151       // shoot the enemy; determine future position of the enemy
152       float bulletSpeed = config.projectileSpeed;
153       Vector2 velocity = enemy->simVelocity;
154       Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0);
155       Vector2 towerPosition = {tower->x, tower->y};
156       float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed;
157       for (int i = 0; i < 8; i++) {
158         velocity = enemy->simVelocity;
159         futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0);
160         float distance = Vector2Distance(towerPosition, futurePosition);
161         float eta2 = distance / bulletSpeed;
162         if (fabs(eta - eta2) < 0.01f) {
163           break;
164         }
165         eta = (eta2 + eta) * 0.5f;
166       }
167 
168       ProjectileTryAdd(config.projectileType, enemy, 
169         (Vector3){towerPosition.x, 1.33f, towerPosition.y}, 
170         (Vector3){futurePosition.x, 0.25f, futurePosition.y},
171         bulletSpeed, config.hitEffect);
172       enemy->futureDamage += config.hitEffect.damage;
173       tower->lastTargetPosition = futurePosition;
174     }
175   }
176   else
177   {
178     tower->cooldown -= gameTime.deltaTime;
179   }
180 }
181 
182 Tower *TowerGetAt(int16_t x, int16_t y)
183 {
184   for (int i = 0; i < towerCount; i++)
185   {
186     if (towers[i].x == x && towers[i].y == y && towers[i].towerType != TOWER_TYPE_NONE)
187     {
188       return &towers[i];
189     }
190   }
191   return 0;
192 }
193 
194 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y)
195 {
196   if (towerCount >= TOWER_MAX_COUNT)
197   {
198     return 0;
199   }
200 
201   Tower *tower = TowerGetAt(x, y);
202   if (tower)
203   {
204     return 0;
205   }
206 
207   tower = &towers[towerCount++];
208   tower->x = x;
209   tower->y = y;
210   tower->towerType = towerType;
211   tower->cooldown = 0.0f;
212   tower->damage = 0.0f;
213   return tower;
214 }
215 
216 Tower *GetTowerByType(uint8_t towerType)
217 {
218   for (int i = 0; i < towerCount; i++)
219   {
220     if (towers[i].towerType == towerType)
221     {
222       return &towers[i];
223     }
224   }
225   return 0;
226 }
227 
228 int GetTowerCosts(uint8_t towerType)
229 {
230   return towerTypeConfigs[towerType].cost;
231 }
232 
233 float TowerGetMaxHealth(Tower *tower)
234 {
235   return towerTypeConfigs[tower->towerType].maxHealth;
236 }
237 
238 void TowerDraw()
239 {
240   for (int i = 0; i < towerCount; i++)
241   {
242     Tower tower = towers[i];
243     if (tower.towerType == TOWER_TYPE_NONE)
244     {
245       continue;
246     }
247 
248     switch (tower.towerType)
249     {
250     case TOWER_TYPE_ARCHER:
251       {
252         Vector2 screenPosTower = GetWorldToScreen((Vector3){tower.x, 0.0f, tower.y}, currentLevel->camera);
253         Vector2 screenPosTarget = GetWorldToScreen((Vector3){tower.lastTargetPosition.x, 0.0f, tower.lastTargetPosition.y}, currentLevel->camera);
254         DrawModel(towerModels[TOWER_TYPE_WALL], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
255         DrawSpriteUnit(archerUnit, (Vector3){tower.x, 1.0f, tower.y}, 0, screenPosTarget.x > screenPosTower.x, 
256           tower.cooldown > 0.2f ? SPRITE_UNIT_PHASE_WEAPON_COOLDOWN : SPRITE_UNIT_PHASE_WEAPON_IDLE);
257       }
258       break;
259     case TOWER_TYPE_BALLISTA:
260       DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, BROWN);
261       break;
262     case TOWER_TYPE_CATAPULT:
263       DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, DARKGRAY);
264       break;
265     default:
266       if (towerModels[tower.towerType].materials)
267       {
268         DrawModel(towerModels[tower.towerType], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
269       } else {
270         DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY);
271       }
272       break;
273     }
274   }
275 }
276 
277 void TowerUpdate()
278 {
279   for (int i = 0; i < towerCount; i++)
280   {
281     Tower *tower = &towers[i];
282     switch (tower->towerType)
283     {
284     case TOWER_TYPE_CATAPULT:
285     case TOWER_TYPE_BALLISTA:
286     case TOWER_TYPE_ARCHER:
287       TowerGunUpdate(tower);
288       break;
289     }
290   }
291 }
292 
293 void TowerDrawHealthBars(Camera3D camera)
294 {
295   for (int i = 0; i < towerCount; i++)
296   {
297     Tower *tower = &towers[i];
298     if (tower->towerType == TOWER_TYPE_NONE || tower->damage <= 0.0f)
299     {
300       continue;
301     }
302     
303     Vector3 position = (Vector3){tower->x, 0.5f, tower->y};
304     float maxHealth = TowerGetMaxHealth(tower);
305     float health = maxHealth - tower->damage;
306     float healthRatio = health / maxHealth;
307     
308     DrawHealthBar(camera, position, healthRatio, GREEN, 35.0f);
309   }
310 }
  1 #ifndef TD_TUT_2_MAIN_H
  2 #define TD_TUT_2_MAIN_H
  3 
  4 #include <inttypes.h>
  5 
  6 #include "raylib.h"
  7 #include "preferred_size.h"
  8 
  9 //# Declarations
 10 
 11 #define ENEMY_MAX_PATH_COUNT 8
 12 #define ENEMY_MAX_COUNT 400
 13 #define ENEMY_TYPE_NONE 0
 14 #define ENEMY_TYPE_MINION 1
 15 
 16 #define PARTICLE_MAX_COUNT 400
 17 #define PARTICLE_TYPE_NONE 0
 18 #define PARTICLE_TYPE_EXPLOSION 1
 19 
 20 typedef struct Particle
 21 {
 22   uint8_t particleType;
 23   float spawnTime;
 24   float lifetime;
 25   Vector3 position;
 26   Vector3 velocity;
 27   Vector3 scale;
 28 } Particle;
 29 
 30 #define TOWER_MAX_COUNT 400
 31 enum TowerType
 32 {
 33   TOWER_TYPE_NONE,
 34   TOWER_TYPE_BASE,
 35   TOWER_TYPE_ARCHER,
 36   TOWER_TYPE_BALLISTA,
 37   TOWER_TYPE_CATAPULT,
 38   TOWER_TYPE_WALL,
 39   TOWER_TYPE_COUNT
 40 };
 41 
 42 typedef struct HitEffectConfig
 43 {
 44   float damage;
 45   float areaDamageRadius;
 46   float pushbackPowerDistance;
 47 } HitEffectConfig;
 48 
 49 typedef struct TowerTypeConfig
 50 {
 51   float cooldown;
 52   float range;
 53   float projectileSpeed;
 54   
 55   uint8_t cost;
 56   uint8_t projectileType;
 57   uint16_t maxHealth;
 58 
 59   HitEffectConfig hitEffect;
 60 } TowerTypeConfig;
 61 
 62 typedef struct Tower
 63 {
 64   int16_t x, y;
 65   uint8_t towerType;
 66   Vector2 lastTargetPosition;
 67   float cooldown;
 68   float damage;
 69 } Tower;
 70 
 71 typedef struct GameTime
 72 {
 73   float time;
 74   float deltaTime;
 75 } GameTime;
 76 
 77 typedef struct ButtonState {
 78   char isSelected;
 79   char isDisabled;
 80 } ButtonState;
 81 
 82 typedef struct GUIState {
 83   int isBlocked;
 84 } GUIState;
 85 
 86 typedef enum LevelState
 87 {
 88   LEVEL_STATE_NONE,
 89   LEVEL_STATE_BUILDING,
 90   LEVEL_STATE_BATTLE,
 91   LEVEL_STATE_WON_WAVE,
 92   LEVEL_STATE_LOST_WAVE,
 93   LEVEL_STATE_WON_LEVEL,
 94   LEVEL_STATE_RESET,
 95 } LevelState;
 96 
 97 typedef struct EnemyWave {
 98   uint8_t enemyType;
 99   uint8_t wave;
100   uint16_t count;
101   float interval;
102   float delay;
103   Vector2 spawnPosition;
104 
105   uint16_t spawned;
106   float timeToSpawnNext;
107 } EnemyWave;
108 
109 #define ENEMY_MAX_WAVE_COUNT 10 110
111 typedef struct Level 112 { 113 int seed; 114 LevelState state; 115 LevelState nextState; 116 Camera3D camera; 117 int placementMode; 118 119 int initialGold; 120 int playerGold; 121
122 EnemyWave waves[ENEMY_MAX_WAVE_COUNT];
123 int currentWave; 124 float waveEndTimer; 125 } Level; 126 127 typedef struct DeltaSrc 128 { 129 char x, y; 130 } DeltaSrc; 131 132 typedef struct PathfindingMap 133 { 134 int width, height; 135 float scale; 136 float *distances; 137 long *towerIndex; 138 DeltaSrc *deltaSrc; 139 float maxDistance; 140 Matrix toMapSpace; 141 Matrix toWorldSpace; 142 } PathfindingMap; 143 144 // when we execute the pathfinding algorithm, we need to store the active nodes 145 // in a queue. Each node has a position, a distance from the start, and the 146 // position of the node that we came from. 147 typedef struct PathfindingNode 148 { 149 int16_t x, y, fromX, fromY; 150 float distance; 151 } PathfindingNode; 152 153 typedef struct EnemyId 154 { 155 uint16_t index; 156 uint16_t generation; 157 } EnemyId; 158 159 typedef struct EnemyClassConfig 160 { 161 float speed; 162 float health; 163 float radius; 164 float maxAcceleration; 165 float requiredContactTime; 166 float explosionDamage; 167 float explosionRange; 168 float explosionPushbackPower; 169 int goldValue; 170 } EnemyClassConfig; 171 172 typedef struct Enemy 173 { 174 int16_t currentX, currentY; 175 int16_t nextX, nextY; 176 Vector2 simPosition; 177 Vector2 simVelocity; 178 uint16_t generation; 179 float walkedDistance; 180 float startMovingTime; 181 float damage, futureDamage; 182 float contactTime; 183 uint8_t enemyType; 184 uint8_t movePathCount; 185 Vector2 movePath[ENEMY_MAX_PATH_COUNT]; 186 } Enemy; 187 188 // a unit that uses sprites to be drawn 189 #define SPRITE_UNIT_PHASE_WEAPON_IDLE 0 190 #define SPRITE_UNIT_PHASE_WEAPON_COOLDOWN 1 191 typedef struct SpriteUnit 192 { 193 Rectangle srcRect; 194 Vector2 offset; 195 int frameCount; 196 float frameDuration; 197 Rectangle srcWeaponIdleRect; 198 Vector2 srcWeaponIdleOffset; 199 Rectangle srcWeaponCooldownRect; 200 Vector2 srcWeaponCooldownOffset; 201 } SpriteUnit; 202 203 #define PROJECTILE_MAX_COUNT 1200 204 #define PROJECTILE_TYPE_NONE 0 205 #define PROJECTILE_TYPE_ARROW 1 206 #define PROJECTILE_TYPE_CATAPULT 2 207 #define PROJECTILE_TYPE_BALLISTA 3 208 209 typedef struct Projectile 210 { 211 uint8_t projectileType; 212 float shootTime; 213 float arrivalTime; 214 float distance; 215 Vector3 position; 216 Vector3 target; 217 Vector3 directionNormal; 218 EnemyId targetEnemy; 219 HitEffectConfig hitEffectConfig; 220 } Projectile; 221 222 //# Function declarations 223 float TowerGetMaxHealth(Tower *tower); 224 int Button(const char *text, int x, int y, int width, int height, ButtonState *state); 225 int EnemyAddDamageRange(Vector2 position, float range, float damage); 226 int EnemyAddDamage(Enemy *enemy, float damage); 227 228 //# Enemy functions 229 void EnemyInit(); 230 void EnemyDraw(); 231 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource); 232 void EnemyUpdate(); 233 float EnemyGetCurrentMaxSpeed(Enemy *enemy); 234 float EnemyGetMaxHealth(Enemy *enemy); 235 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY); 236 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount); 237 EnemyId EnemyGetId(Enemy *enemy); 238 Enemy *EnemyTryResolve(EnemyId enemyId); 239 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY); 240 int EnemyAddDamage(Enemy *enemy, float damage); 241 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range); 242 int EnemyCount(); 243 void EnemyDrawHealthbars(Camera3D camera); 244 245 //# Tower functions 246 void TowerInit(); 247 Tower *TowerGetAt(int16_t x, int16_t y); 248 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y); 249 Tower *GetTowerByType(uint8_t towerType); 250 int GetTowerCosts(uint8_t towerType); 251 float TowerGetMaxHealth(Tower *tower); 252 void TowerDraw(); 253 void TowerUpdate(); 254 void TowerDrawHealthBars(Camera3D camera); 255 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase); 256 257 //# Particles 258 void ParticleInit(); 259 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime); 260 void ParticleUpdate(); 261 void ParticleDraw(); 262 263 //# Projectiles 264 void ProjectileInit(); 265 void ProjectileDraw(); 266 void ProjectileUpdate(); 267 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig); 268 269 //# Pathfinding map 270 void PathfindingMapInit(int width, int height, Vector3 translate, float scale); 271 float PathFindingGetDistance(int mapX, int mapY); 272 Vector2 PathFindingGetGradient(Vector3 world); 273 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY); 274 void PathFindingMapUpdate(); 275 void PathFindingMapDraw(); 276 277 //# UI 278 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth); 279 280 //# Level
281 void DrawLevelGround(Level *level); 282 void DrawEnemyPath(Level *level);
283 284 //# variables 285 extern Level *currentLevel; 286 extern Enemy enemies[ENEMY_MAX_COUNT]; 287 extern int enemyCount; 288 extern EnemyClassConfig enemyClassConfigs[]; 289 290 extern GUIState guiState; 291 extern GameTime gameTime; 292 extern Tower towers[TOWER_MAX_COUNT]; 293 extern int towerCount; 294 295 extern Texture2D palette, spriteSheet; 296 297 #endif
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 #include <stdlib.h>
  4 #include <math.h>
  5 
  6 EnemyClassConfig enemyClassConfigs[] = {
  7     [ENEMY_TYPE_MINION] = {
  8       .health = 10.0f, 
  9       .speed = 0.6f, 
 10       .radius = 0.25f, 
 11       .maxAcceleration = 1.0f,
 12       .explosionDamage = 1.0f,
 13       .requiredContactTime = 0.5f,
 14       .explosionRange = 1.0f,
 15       .explosionPushbackPower = 0.25f,
 16       .goldValue = 1,
 17     },
 18 };
 19 
 20 Enemy enemies[ENEMY_MAX_COUNT];
 21 int enemyCount = 0;
 22 
 23 SpriteUnit enemySprites[] = {
 24     [ENEMY_TYPE_MINION] = {
 25       .srcRect = {0, 16, 16, 16},
 26       .offset = {8.0f, 0.0f},
 27       .frameCount = 6,
 28       .frameDuration = 0.1f,
 29     },
 30 };
 31 
 32 void EnemyInit()
 33 {
 34   for (int i = 0; i < ENEMY_MAX_COUNT; i++)
 35   {
 36     enemies[i] = (Enemy){0};
 37   }
 38   enemyCount = 0;
 39 }
 40 
 41 float EnemyGetCurrentMaxSpeed(Enemy *enemy)
 42 {
 43   return enemyClassConfigs[enemy->enemyType].speed;
 44 }
 45 
 46 float EnemyGetMaxHealth(Enemy *enemy)
 47 {
 48   return enemyClassConfigs[enemy->enemyType].health;
 49 }
 50 
 51 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY)
 52 {
 53   int16_t castleX = 0;
 54   int16_t castleY = 0;
 55   int16_t dx = castleX - currentX;
 56   int16_t dy = castleY - currentY;
 57   if (dx == 0 && dy == 0)
 58   {
 59     *nextX = currentX;
 60     *nextY = currentY;
 61     return 1;
 62   }
 63   Vector2 gradient = PathFindingGetGradient((Vector3){currentX, 0, currentY});
 64 
 65   if (gradient.x == 0 && gradient.y == 0)
 66   {
 67     *nextX = currentX;
 68     *nextY = currentY;
 69     return 1;
 70   }
 71 
 72   if (fabsf(gradient.x) > fabsf(gradient.y))
 73   {
 74     *nextX = currentX + (int16_t)(gradient.x > 0.0f ? 1 : -1);
 75     *nextY = currentY;
 76     return 0;
 77   }
 78   *nextX = currentX;
 79   *nextY = currentY + (int16_t)(gradient.y > 0.0f ? 1 : -1);
 80   return 0;
 81 }
 82 
 83 
 84 // this function predicts the movement of the unit for the next deltaT seconds
 85 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount)
 86 {
 87   const float pointReachedDistance = 0.25f;
 88   const float pointReachedDistance2 = pointReachedDistance * pointReachedDistance;
 89   const float maxSimStepTime = 0.015625f;
 90   
 91   float maxAcceleration = enemyClassConfigs[enemy->enemyType].maxAcceleration;
 92   float maxSpeed = EnemyGetCurrentMaxSpeed(enemy);
 93   int16_t nextX = enemy->nextX;
 94   int16_t nextY = enemy->nextY;
 95   Vector2 position = enemy->simPosition;
 96   int passedCount = 0;
 97   for (float t = 0.0f; t < deltaT; t += maxSimStepTime)
 98   {
 99     float stepTime = fminf(deltaT - t, maxSimStepTime);
100     Vector2 target = (Vector2){nextX, nextY};
101     float speed = Vector2Length(*velocity);
102     // draw the target position for debugging
103     DrawCubeWires((Vector3){target.x, 0.2f, target.y}, 0.1f, 0.4f, 0.1f, RED);
104     Vector2 lookForwardPos = Vector2Add(position, Vector2Scale(*velocity, speed));
105     if (Vector2DistanceSqr(target, lookForwardPos) <= pointReachedDistance2)
106     {
107       // we reached the target position, let's move to the next waypoint
108       EnemyGetNextPosition(nextX, nextY, &nextX, &nextY);
109       target = (Vector2){nextX, nextY};
110       // track how many waypoints we passed
111       passedCount++;
112     }
113     
114     // acceleration towards the target
115     Vector2 unitDirection = Vector2Normalize(Vector2Subtract(target, lookForwardPos));
116     Vector2 acceleration = Vector2Scale(unitDirection, maxAcceleration * stepTime);
117     *velocity = Vector2Add(*velocity, acceleration);
118 
119     // limit the speed to the maximum speed
120     if (speed > maxSpeed)
121     {
122       *velocity = Vector2Scale(*velocity, maxSpeed / speed);
123     }
124 
125     // move the enemy
126     position = Vector2Add(position, Vector2Scale(*velocity, stepTime));
127   }
128 
129   if (waypointPassedCount)
130   {
131     (*waypointPassedCount) = passedCount;
132   }
133 
134   return position;
135 }
136 
137 void EnemyDraw()
138 {
139   for (int i = 0; i < enemyCount; i++)
140   {
141     Enemy enemy = enemies[i];
142     if (enemy.enemyType == ENEMY_TYPE_NONE)
143     {
144       continue;
145     }
146 
147     Vector2 position = EnemyGetPosition(&enemy, gameTime.time - enemy.startMovingTime, &enemy.simVelocity, 0);
148     
149     // don't draw any trails for now; might replace this with footprints later
150     // if (enemy.movePathCount > 0)
151     // {
152     //   Vector3 p = {enemy.movePath[0].x, 0.2f, enemy.movePath[0].y};
153     //   DrawLine3D(p, (Vector3){position.x, 0.2f, position.y}, GREEN);
154     // }
155     // for (int j = 1; j < enemy.movePathCount; j++)
156     // {
157     //   Vector3 p = {enemy.movePath[j - 1].x, 0.2f, enemy.movePath[j - 1].y};
158     //   Vector3 q = {enemy.movePath[j].x, 0.2f, enemy.movePath[j].y};
159     //   DrawLine3D(p, q, GREEN);
160     // }
161 
162     switch (enemy.enemyType)
163     {
164     case ENEMY_TYPE_MINION:
165       DrawSpriteUnit(enemySprites[ENEMY_TYPE_MINION], (Vector3){position.x, 0.0f, position.y}, 
166         enemy.walkedDistance, 0, 0);
167       break;
168     }
169   }
170 }
171 
172 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource)
173 {
174   // damage the tower
175   float explosionDamge = enemyClassConfigs[enemy->enemyType].explosionDamage;
176   float explosionRange = enemyClassConfigs[enemy->enemyType].explosionRange;
177   float explosionPushbackPower = enemyClassConfigs[enemy->enemyType].explosionPushbackPower;
178   float explosionRange2 = explosionRange * explosionRange;
179   tower->damage += enemyClassConfigs[enemy->enemyType].explosionDamage;
180   // explode the enemy
181   if (tower->damage >= TowerGetMaxHealth(tower))
182   {
183     tower->towerType = TOWER_TYPE_NONE;
184   }
185 
186   ParticleAdd(PARTICLE_TYPE_EXPLOSION, 
187     explosionSource, 
188     (Vector3){0, 0.1f, 0}, (Vector3){1.0f, 1.0f, 1.0f}, 1.0f);
189 
190   enemy->enemyType = ENEMY_TYPE_NONE;
191 
192   // push back enemies & dealing damage
193   for (int i = 0; i < enemyCount; i++)
194   {
195     Enemy *other = &enemies[i];
196     if (other->enemyType == ENEMY_TYPE_NONE)
197     {
198       continue;
199     }
200     float distanceSqr = Vector2DistanceSqr(enemy->simPosition, other->simPosition);
201     if (distanceSqr > 0 && distanceSqr < explosionRange2)
202     {
203       Vector2 direction = Vector2Normalize(Vector2Subtract(other->simPosition, enemy->simPosition));
204       other->simPosition = Vector2Add(other->simPosition, Vector2Scale(direction, explosionPushbackPower));
205       EnemyAddDamage(other, explosionDamge);
206     }
207   }
208 }
209 
210 void EnemyUpdate()
211 {
212   const float castleX = 0;
213   const float castleY = 0;
214   const float maxPathDistance2 = 0.25f * 0.25f;
215   
216   for (int i = 0; i < enemyCount; i++)
217   {
218     Enemy *enemy = &enemies[i];
219     if (enemy->enemyType == ENEMY_TYPE_NONE)
220     {
221       continue;
222     }
223 
224     int waypointPassedCount = 0;
225     Vector2 prevPosition = enemy->simPosition;
226     enemy->simPosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &enemy->simVelocity, &waypointPassedCount);
227     enemy->startMovingTime = gameTime.time;
228     enemy->walkedDistance += Vector2Distance(prevPosition, enemy->simPosition);
229     // track path of unit
230     if (enemy->movePathCount == 0 || Vector2DistanceSqr(enemy->simPosition, enemy->movePath[0]) > maxPathDistance2)
231     {
232       for (int j = ENEMY_MAX_PATH_COUNT - 1; j > 0; j--)
233       {
234         enemy->movePath[j] = enemy->movePath[j - 1];
235       }
236       enemy->movePath[0] = enemy->simPosition;
237       if (++enemy->movePathCount > ENEMY_MAX_PATH_COUNT)
238       {
239         enemy->movePathCount = ENEMY_MAX_PATH_COUNT;
240       }
241     }
242 
243     if (waypointPassedCount > 0)
244     {
245       enemy->currentX = enemy->nextX;
246       enemy->currentY = enemy->nextY;
247       if (EnemyGetNextPosition(enemy->currentX, enemy->currentY, &enemy->nextX, &enemy->nextY) &&
248         Vector2DistanceSqr(enemy->simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
249       {
250         // enemy reached the castle; remove it
251         enemy->enemyType = ENEMY_TYPE_NONE;
252         continue;
253       }
254     }
255   }
256 
257   // handle collisions between enemies
258   for (int i = 0; i < enemyCount - 1; i++)
259   {
260     Enemy *enemyA = &enemies[i];
261     if (enemyA->enemyType == ENEMY_TYPE_NONE)
262     {
263       continue;
264     }
265     for (int j = i + 1; j < enemyCount; j++)
266     {
267       Enemy *enemyB = &enemies[j];
268       if (enemyB->enemyType == ENEMY_TYPE_NONE)
269       {
270         continue;
271       }
272       float distanceSqr = Vector2DistanceSqr(enemyA->simPosition, enemyB->simPosition);
273       float radiusA = enemyClassConfigs[enemyA->enemyType].radius;
274       float radiusB = enemyClassConfigs[enemyB->enemyType].radius;
275       float radiusSum = radiusA + radiusB;
276       if (distanceSqr < radiusSum * radiusSum && distanceSqr > 0.001f)
277       {
278         // collision
279         float distance = sqrtf(distanceSqr);
280         float overlap = radiusSum - distance;
281         // move the enemies apart, but softly; if we have a clog of enemies,
282         // moving them perfectly apart can cause them to jitter
283         float positionCorrection = overlap / 5.0f;
284         Vector2 direction = (Vector2){
285             (enemyB->simPosition.x - enemyA->simPosition.x) / distance * positionCorrection,
286             (enemyB->simPosition.y - enemyA->simPosition.y) / distance * positionCorrection};
287         enemyA->simPosition = Vector2Subtract(enemyA->simPosition, direction);
288         enemyB->simPosition = Vector2Add(enemyB->simPosition, direction);
289       }
290     }
291   }
292 
293   // handle collisions between enemies and towers
294   for (int i = 0; i < enemyCount; i++)
295   {
296     Enemy *enemy = &enemies[i];
297     if (enemy->enemyType == ENEMY_TYPE_NONE)
298     {
299       continue;
300     }
301     enemy->contactTime -= gameTime.deltaTime;
302     if (enemy->contactTime < 0.0f)
303     {
304       enemy->contactTime = 0.0f;
305     }
306 
307     float enemyRadius = enemyClassConfigs[enemy->enemyType].radius;
308     // linear search over towers; could be optimized by using path finding tower map,
309     // but for now, we keep it simple
310     for (int j = 0; j < towerCount; j++)
311     {
312       Tower *tower = &towers[j];
313       if (tower->towerType == TOWER_TYPE_NONE)
314       {
315         continue;
316       }
317       float distanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){tower->x, tower->y});
318       float combinedRadius = enemyRadius + 0.708; // sqrt(0.5^2 + 0.5^2), corner-center distance of square with side length 1
319       if (distanceSqr > combinedRadius * combinedRadius)
320       {
321         continue;
322       }
323       // potential collision; square / circle intersection
324       float dx = tower->x - enemy->simPosition.x;
325       float dy = tower->y - enemy->simPosition.y;
326       float absDx = fabsf(dx);
327       float absDy = fabsf(dy);
328       Vector3 contactPoint = {0};
329       if (absDx <= 0.5f && absDx <= absDy) {
330         // vertical collision; push the enemy out horizontally
331         float overlap = enemyRadius + 0.5f - absDy;
332         if (overlap < 0.0f)
333         {
334           continue;
335         }
336         float direction = dy > 0.0f ? -1.0f : 1.0f;
337         enemy->simPosition.y += direction * overlap;
338         contactPoint = (Vector3){enemy->simPosition.x, 0.2f, tower->y + direction * 0.5f};
339       }
340       else if (absDy <= 0.5f && absDy <= absDx)
341       {
342         // horizontal collision; push the enemy out vertically
343         float overlap = enemyRadius + 0.5f - absDx;
344         if (overlap < 0.0f)
345         {
346           continue;
347         }
348         float direction = dx > 0.0f ? -1.0f : 1.0f;
349         enemy->simPosition.x += direction * overlap;
350         contactPoint = (Vector3){tower->x + direction * 0.5f, 0.2f, enemy->simPosition.y};
351       }
352       else
353       {
354         // possible collision with a corner
355         float cornerDX = dx > 0.0f ? -0.5f : 0.5f;
356         float cornerDY = dy > 0.0f ? -0.5f : 0.5f;
357         float cornerX = tower->x + cornerDX;
358         float cornerY = tower->y + cornerDY;
359         float cornerDistanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){cornerX, cornerY});
360         if (cornerDistanceSqr > enemyRadius * enemyRadius)
361         {
362           continue;
363         }
364         // push the enemy out along the diagonal
365         float cornerDistance = sqrtf(cornerDistanceSqr);
366         float overlap = enemyRadius - cornerDistance;
367         float directionX = cornerDistance > 0.0f ? (cornerX - enemy->simPosition.x) / cornerDistance : -cornerDX;
368         float directionY = cornerDistance > 0.0f ? (cornerY - enemy->simPosition.y) / cornerDistance : -cornerDY;
369         enemy->simPosition.x -= directionX * overlap;
370         enemy->simPosition.y -= directionY * overlap;
371         contactPoint = (Vector3){cornerX, 0.2f, cornerY};
372       }
373 
374       if (enemyClassConfigs[enemy->enemyType].explosionDamage > 0.0f)
375       {
376         enemy->contactTime += gameTime.deltaTime * 2.0f; // * 2 to undo the subtraction above
377         if (enemy->contactTime >= enemyClassConfigs[enemy->enemyType].requiredContactTime)
378         {
379           EnemyTriggerExplode(enemy, tower, contactPoint);
380         }
381       }
382     }
383   }
384 }
385 
386 EnemyId EnemyGetId(Enemy *enemy)
387 {
388   return (EnemyId){enemy - enemies, enemy->generation};
389 }
390 
391 Enemy *EnemyTryResolve(EnemyId enemyId)
392 {
393   if (enemyId.index >= ENEMY_MAX_COUNT)
394   {
395     return 0;
396   }
397   Enemy *enemy = &enemies[enemyId.index];
398   if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE)
399   {
400     return 0;
401   }
402   return enemy;
403 }
404 
405 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY)
406 {
407   Enemy *spawn = 0;
408   for (int i = 0; i < enemyCount; i++)
409   {
410     Enemy *enemy = &enemies[i];
411     if (enemy->enemyType == ENEMY_TYPE_NONE)
412     {
413       spawn = enemy;
414       break;
415     }
416   }
417 
418   if (enemyCount < ENEMY_MAX_COUNT && !spawn)
419   {
420     spawn = &enemies[enemyCount++];
421   }
422 
423   if (spawn)
424   {
425     spawn->currentX = currentX;
426     spawn->currentY = currentY;
427     spawn->nextX = currentX;
428     spawn->nextY = currentY;
429     spawn->simPosition = (Vector2){currentX, currentY};
430     spawn->simVelocity = (Vector2){0, 0};
431     spawn->enemyType = enemyType;
432     spawn->startMovingTime = gameTime.time;
433     spawn->damage = 0.0f;
434     spawn->futureDamage = 0.0f;
435     spawn->generation++;
436     spawn->movePathCount = 0;
437     spawn->walkedDistance = 0.0f;
438   }
439 
440   return spawn;
441 }
442 
443 int EnemyAddDamageRange(Vector2 position, float range, float damage)
444 {
445   int count = 0;
446   float range2 = range * range;
447   for (int i = 0; i < enemyCount; i++)
448   {
449     Enemy *enemy = &enemies[i];
450     if (enemy->enemyType == ENEMY_TYPE_NONE)
451     {
452       continue;
453     }
454     float distance2 = Vector2DistanceSqr(position, enemy->simPosition);
455     if (distance2 <= range2)
456     {
457       EnemyAddDamage(enemy, damage);
458       count++;
459     }
460   }
461   return count;
462 }
463 
464 int EnemyAddDamage(Enemy *enemy, float damage)
465 {
466   enemy->damage += damage;
467   if (enemy->damage >= EnemyGetMaxHealth(enemy))
468   {
469     currentLevel->playerGold += enemyClassConfigs[enemy->enemyType].goldValue;
470     enemy->enemyType = ENEMY_TYPE_NONE;
471     return 1;
472   }
473 
474   return 0;
475 }
476 
477 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
478 {
479   int16_t castleX = 0;
480   int16_t castleY = 0;
481   Enemy* closest = 0;
482   int16_t closestDistance = 0;
483   float range2 = range * range;
484   for (int i = 0; i < enemyCount; i++)
485   {
486     Enemy* enemy = &enemies[i];
487     if (enemy->enemyType == ENEMY_TYPE_NONE)
488     {
489       continue;
490     }
491     float maxHealth = EnemyGetMaxHealth(enemy);
492     if (enemy->futureDamage >= maxHealth)
493     {
494       // ignore enemies that will die soon
495       continue;
496     }
497     int16_t dx = castleX - enemy->currentX;
498     int16_t dy = castleY - enemy->currentY;
499     int16_t distance = abs(dx) + abs(dy);
500     if (!closest || distance < closestDistance)
501     {
502       float tdx = towerX - enemy->currentX;
503       float tdy = towerY - enemy->currentY;
504       float tdistance2 = tdx * tdx + tdy * tdy;
505       if (tdistance2 <= range2)
506       {
507         closest = enemy;
508         closestDistance = distance;
509       }
510     }
511   }
512   return closest;
513 }
514 
515 int EnemyCount()
516 {
517   int count = 0;
518   for (int i = 0; i < enemyCount; i++)
519   {
520     if (enemies[i].enemyType != ENEMY_TYPE_NONE)
521     {
522       count++;
523     }
524   }
525   return count;
526 }
527 
528 void EnemyDrawHealthbars(Camera3D camera)
529 {
530   for (int i = 0; i < enemyCount; i++)
531   {
532     Enemy *enemy = &enemies[i];
533     if (enemy->enemyType == ENEMY_TYPE_NONE || enemy->damage == 0.0f)
534     {
535       continue;
536     }
537     Vector3 position = (Vector3){enemy->simPosition.x, 0.5f, enemy->simPosition.y};
538     float maxHealth = EnemyGetMaxHealth(enemy);
539     float health = maxHealth - enemy->damage;
540     float healthRatio = health / maxHealth;
541     
542     DrawHealthBar(camera, position, healthRatio, GREEN, 15.0f);
543   }
544 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 
  4 // The queue is a simple array of nodes, we add nodes to the end and remove
  5 // nodes from the front. We keep the array around to avoid unnecessary allocations
  6 static PathfindingNode *pathfindingNodeQueue = 0;
  7 static int pathfindingNodeQueueCount = 0;
  8 static int pathfindingNodeQueueCapacity = 0;
  9 
 10 // The pathfinding map stores the distances from the castle to each cell in the map.
 11 static PathfindingMap pathfindingMap = {0};
 12 
 13 void PathfindingMapInit(int width, int height, Vector3 translate, float scale)
 14 {
 15   // transforming between map space and world space allows us to adapt 
 16   // position and scale of the map without changing the pathfinding data
 17   pathfindingMap.toWorldSpace = MatrixTranslate(translate.x, translate.y, translate.z);
 18   pathfindingMap.toWorldSpace = MatrixMultiply(pathfindingMap.toWorldSpace, MatrixScale(scale, scale, scale));
 19   pathfindingMap.toMapSpace = MatrixInvert(pathfindingMap.toWorldSpace);
 20   pathfindingMap.width = width;
 21   pathfindingMap.height = height;
 22   pathfindingMap.scale = scale;
 23   pathfindingMap.distances = (float *)MemAlloc(width * height * sizeof(float));
 24   for (int i = 0; i < width * height; i++)
 25   {
 26     pathfindingMap.distances[i] = -1.0f;
 27   }
 28 
 29   pathfindingMap.towerIndex = (long *)MemAlloc(width * height * sizeof(long));
 30   pathfindingMap.deltaSrc = (DeltaSrc *)MemAlloc(width * height * sizeof(DeltaSrc));
 31 }
 32 
 33 static void PathFindingNodePush(int16_t x, int16_t y, int16_t fromX, int16_t fromY, float distance)
 34 {
 35   if (pathfindingNodeQueueCount >= pathfindingNodeQueueCapacity)
 36   {
 37     pathfindingNodeQueueCapacity = pathfindingNodeQueueCapacity == 0 ? 256 : pathfindingNodeQueueCapacity * 2;
 38     // we use MemAlloc/MemRealloc to allocate memory for the queue
 39     // I am not entirely sure if MemRealloc allows passing a null pointer
 40     // so we check if the pointer is null and use MemAlloc in that case
 41     if (pathfindingNodeQueue == 0)
 42     {
 43       pathfindingNodeQueue = (PathfindingNode *)MemAlloc(pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
 44     }
 45     else
 46     {
 47       pathfindingNodeQueue = (PathfindingNode *)MemRealloc(pathfindingNodeQueue, pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
 48     }
 49   }
 50 
 51   PathfindingNode *node = &pathfindingNodeQueue[pathfindingNodeQueueCount++];
 52   node->x = x;
 53   node->y = y;
 54   node->fromX = fromX;
 55   node->fromY = fromY;
 56   node->distance = distance;
 57 }
 58 
 59 static PathfindingNode *PathFindingNodePop()
 60 {
 61   if (pathfindingNodeQueueCount == 0)
 62   {
 63     return 0;
 64   }
 65   // we return the first node in the queue; we want to return a pointer to the node
 66   // so we can return 0 if the queue is empty. 
 67   // We should _not_ return a pointer to the element in the list, because the list
 68   // may be reallocated and the pointer would become invalid. Or the 
 69   // popped element is overwritten by the next push operation.
 70   // Using static here means that the variable is permanently allocated.
 71   static PathfindingNode node;
 72   node = pathfindingNodeQueue[0];
 73   // we shift all nodes one position to the front
 74   for (int i = 1; i < pathfindingNodeQueueCount; i++)
 75   {
 76     pathfindingNodeQueue[i - 1] = pathfindingNodeQueue[i];
 77   }
 78   --pathfindingNodeQueueCount;
 79   return &node;
 80 }
 81 
 82 float PathFindingGetDistance(int mapX, int mapY)
 83 {
 84   if (mapX < 0 || mapX >= pathfindingMap.width || mapY < 0 || mapY >= pathfindingMap.height)
 85   {
 86     // when outside the map, we return the manhattan distance to the castle (0,0)
 87     return fabsf((float)mapX) + fabsf((float)mapY);
 88   }
 89 
 90   return pathfindingMap.distances[mapY * pathfindingMap.width + mapX];
 91 }
 92 
 93 // transform a world position to a map position in the array; 
 94 // returns true if the position is inside the map
 95 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY)
 96 {
 97   Vector3 mapPosition = Vector3Transform(worldPosition, pathfindingMap.toMapSpace);
 98   *mapX = (int16_t)mapPosition.x;
 99   *mapY = (int16_t)mapPosition.z;
100   return *mapX >= 0 && *mapX < pathfindingMap.width && *mapY >= 0 && *mapY < pathfindingMap.height;
101 }
102 
103 void PathFindingMapUpdate()
104 {
105   const int castleX = 0, castleY = 0;
106   int16_t castleMapX, castleMapY;
107   if (!PathFindingFromWorldToMapPosition((Vector3){castleX, 0.0f, castleY}, &castleMapX, &castleMapY))
108   {
109     return;
110   }
111   int width = pathfindingMap.width, height = pathfindingMap.height;
112 
113   // reset the distances to -1
114   for (int i = 0; i < width * height; i++)
115   {
116     pathfindingMap.distances[i] = -1.0f;
117   }
118   // reset the tower indices
119   for (int i = 0; i < width * height; i++)
120   {
121     pathfindingMap.towerIndex[i] = -1;
122   }
123   // reset the delta src
124   for (int i = 0; i < width * height; i++)
125   {
126     pathfindingMap.deltaSrc[i].x = 0;
127     pathfindingMap.deltaSrc[i].y = 0;
128   }
129 
130   for (int i = 0; i < towerCount; i++)
131   {
132     Tower *tower = &towers[i];
133     if (tower->towerType == TOWER_TYPE_NONE || tower->towerType == TOWER_TYPE_BASE)
134     {
135       continue;
136     }
137     int16_t mapX, mapY;
138     // technically, if the tower cell scale is not in sync with the pathfinding map scale,
139     // this would not work correctly and needs to be refined to allow towers covering multiple cells
140     // or having multiple towers in one cell; for simplicity, we assume that the tower covers exactly
141     // one cell. For now.
142     if (!PathFindingFromWorldToMapPosition((Vector3){tower->x, 0.0f, tower->y}, &mapX, &mapY))
143     {
144       continue;
145     }
146     int index = mapY * width + mapX;
147     pathfindingMap.towerIndex[index] = i;
148   }
149 
150   // we start at the castle and add the castle to the queue
151   pathfindingMap.maxDistance = 0.0f;
152   pathfindingNodeQueueCount = 0;
153   PathFindingNodePush(castleMapX, castleMapY, castleMapX, castleMapY, 0.0f);
154   PathfindingNode *node = 0;
155   while ((node = PathFindingNodePop()))
156   {
157     if (node->x < 0 || node->x >= width || node->y < 0 || node->y >= height)
158     {
159       continue;
160     }
161     int index = node->y * width + node->x;
162     if (pathfindingMap.distances[index] >= 0 && pathfindingMap.distances[index] <= node->distance)
163     {
164       continue;
165     }
166 
167     int deltaX = node->x - node->fromX;
168     int deltaY = node->y - node->fromY;
169     // even if the cell is blocked by a tower, we still may want to store the direction
170     // (though this might not be needed, IDK right now)
171     pathfindingMap.deltaSrc[index].x = (char) deltaX;
172     pathfindingMap.deltaSrc[index].y = (char) deltaY;
173 
174     // we skip nodes that are blocked by towers
175     if (pathfindingMap.towerIndex[index] >= 0)
176     {
177       node->distance += 8.0f;
178     }
179     pathfindingMap.distances[index] = node->distance;
180     pathfindingMap.maxDistance = fmaxf(pathfindingMap.maxDistance, node->distance);
181     PathFindingNodePush(node->x, node->y + 1, node->x, node->y, node->distance + 1.0f);
182     PathFindingNodePush(node->x, node->y - 1, node->x, node->y, node->distance + 1.0f);
183     PathFindingNodePush(node->x + 1, node->y, node->x, node->y, node->distance + 1.0f);
184     PathFindingNodePush(node->x - 1, node->y, node->x, node->y, node->distance + 1.0f);
185   }
186 }
187 
188 void PathFindingMapDraw()
189 {
190   float cellSize = pathfindingMap.scale * 0.9f;
191   float highlightDistance = fmodf(GetTime() * 4.0f, pathfindingMap.maxDistance);
192   for (int x = 0; x < pathfindingMap.width; x++)
193   {
194     for (int y = 0; y < pathfindingMap.height; y++)
195     {
196       float distance = pathfindingMap.distances[y * pathfindingMap.width + x];
197       float colorV = distance < 0 ? 0 : fminf(distance / pathfindingMap.maxDistance, 1.0f);
198       Color color = distance < 0 ? BLUE : (Color){fminf(colorV, 1.0f) * 255, 0, 0, 255};
199       Vector3 position = Vector3Transform((Vector3){x, -0.25f, y}, pathfindingMap.toWorldSpace);
200       // animate the distance "wave" to show how the pathfinding algorithm expands
201       // from the castle
202       if (distance + 0.5f > highlightDistance && distance - 0.5f < highlightDistance)
203       {
204         color = BLACK;
205       }
206       DrawCube(position, cellSize, 0.1f, cellSize, color);
207     }
208   }
209 }
210 
211 Vector2 PathFindingGetGradient(Vector3 world)
212 {
213   int16_t mapX, mapY;
214   if (PathFindingFromWorldToMapPosition(world, &mapX, &mapY))
215   {
216     DeltaSrc delta = pathfindingMap.deltaSrc[mapY * pathfindingMap.width + mapX];
217     return (Vector2){(float)-delta.x, (float)-delta.y};
218   }
219   // fallback to a simple gradient calculation
220   float n = PathFindingGetDistance(mapX, mapY - 1);
221   float s = PathFindingGetDistance(mapX, mapY + 1);
222   float w = PathFindingGetDistance(mapX - 1, mapY);
223   float e = PathFindingGetDistance(mapX + 1, mapY);
224   return (Vector2){w - e + 0.25f, n - s + 0.125f};
225 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 
  4 static Projectile projectiles[PROJECTILE_MAX_COUNT];
  5 static int projectileCount = 0;
  6 
  7 typedef struct ProjectileConfig
  8 {
  9   float arcFactor;
 10   Color color;
 11   Color trailColor;
 12 } ProjectileConfig;
 13 
 14 ProjectileConfig projectileConfigs[] = {
 15     [PROJECTILE_TYPE_ARROW] = {
 16         .arcFactor = 0.15f,
 17         .color = RED,
 18         .trailColor = BROWN,
 19     },
 20     [PROJECTILE_TYPE_CATAPULT] = {
 21         .arcFactor = 0.5f,
 22         .color = RED,
 23         .trailColor = GRAY,
 24     },
 25     [PROJECTILE_TYPE_BALLISTA] = {
 26         .arcFactor = 0.025f,
 27         .color = RED,
 28         .trailColor = BROWN,
 29     },
 30 };
 31 
 32 void ProjectileInit()
 33 {
 34   for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
 35   {
 36     projectiles[i] = (Projectile){0};
 37   }
 38 }
 39 
 40 void ProjectileDraw()
 41 {
 42   for (int i = 0; i < projectileCount; i++)
 43   {
 44     Projectile projectile = projectiles[i];
 45     if (projectile.projectileType == PROJECTILE_TYPE_NONE)
 46     {
 47       continue;
 48     }
 49     float transition = (gameTime.time - projectile.shootTime) / (projectile.arrivalTime - projectile.shootTime);
 50     if (transition >= 1.0f)
 51     {
 52       continue;
 53     }
 54 
 55     ProjectileConfig config = projectileConfigs[projectile.projectileType];
 56     for (float transitionOffset = 0.0f; transitionOffset < 1.0f; transitionOffset += 0.1f)
 57     {
 58       float t = transition + transitionOffset * 0.3f;
 59       if (t > 1.0f)
 60       {
 61         break;
 62       }
 63       Vector3 position = Vector3Lerp(projectile.position, projectile.target, t);
 64       Color color = config.color;
 65       color = ColorLerp(config.trailColor, config.color, transitionOffset * transitionOffset);
 66       // fake a ballista flight path using parabola equation
 67       float parabolaT = t - 0.5f;
 68       parabolaT = 1.0f - 4.0f * parabolaT * parabolaT;
 69       position.y += config.arcFactor * parabolaT * projectile.distance;
 70       
 71       float size = 0.06f * (transitionOffset + 0.25f);
 72       DrawCube(position, size, size, size, color);
 73     }
 74   }
 75 }
 76 
 77 void ProjectileUpdate()
 78 {
 79   for (int i = 0; i < projectileCount; i++)
 80   {
 81     Projectile *projectile = &projectiles[i];
 82     if (projectile->projectileType == PROJECTILE_TYPE_NONE)
 83     {
 84       continue;
 85     }
 86     float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime);
 87     if (transition >= 1.0f)
 88     {
 89       projectile->projectileType = PROJECTILE_TYPE_NONE;
 90       Enemy *enemy = EnemyTryResolve(projectile->targetEnemy);
 91       if (enemy && projectile->hitEffectConfig.pushbackPowerDistance > 0.0f)
 92       {
 93           Vector2 direction = Vector2Normalize(Vector2Subtract((Vector2){projectile->target.x, projectile->target.z}, enemy->simPosition));
 94           enemy->simPosition = Vector2Add(enemy->simPosition, Vector2Scale(direction, projectile->hitEffectConfig.pushbackPowerDistance));
 95       }
 96       
 97       if (projectile->hitEffectConfig.areaDamageRadius > 0.0f)
 98       {
 99         EnemyAddDamageRange((Vector2){projectile->target.x, projectile->target.z}, projectile->hitEffectConfig.areaDamageRadius, projectile->hitEffectConfig.damage);
100         // pancaked sphere explosion
101         float r = projectile->hitEffectConfig.areaDamageRadius;
102         ParticleAdd(PARTICLE_TYPE_EXPLOSION, projectile->target, (Vector3){0}, (Vector3){r, r * 0.2f, r}, 0.33f);
103       }
104       else if (projectile->hitEffectConfig.damage > 0.0f && enemy)
105       {
106         EnemyAddDamage(enemy, projectile->hitEffectConfig.damage);
107       }
108       continue;
109     }
110   }
111 }
112 
113 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig)
114 {
115   for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
116   {
117     Projectile *projectile = &projectiles[i];
118     if (projectile->projectileType == PROJECTILE_TYPE_NONE)
119     {
120       projectile->projectileType = projectileType;
121       projectile->shootTime = gameTime.time;
122       float distance = Vector3Distance(position, target);
123       projectile->arrivalTime = gameTime.time + distance / speed;
124       projectile->position = position;
125       projectile->target = target;
126       projectile->directionNormal = Vector3Scale(Vector3Subtract(target, position), 1.0f / distance);
127       projectile->distance = distance;
128       projectile->targetEnemy = EnemyGetId(enemy);
129       projectileCount = projectileCount <= i ? i + 1 : projectileCount;
130       projectile->hitEffectConfig = hitEffectConfig;
131       return projectile;
132     }
133   }
134   return 0;
135 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 #include <rlgl.h>
  4 
  5 static Particle particles[PARTICLE_MAX_COUNT];
  6 static int particleCount = 0;
  7 
  8 void ParticleInit()
  9 {
 10   for (int i = 0; i < PARTICLE_MAX_COUNT; i++)
 11   {
 12     particles[i] = (Particle){0};
 13   }
 14   particleCount = 0;
 15 }
 16 
 17 static void DrawExplosionParticle(Particle *particle, float transition)
 18 {
 19   Vector3 scale = particle->scale;
 20   float size = 1.0f * (1.0f - transition);
 21   Color startColor = WHITE;
 22   Color endColor = RED;
 23   Color color = ColorLerp(startColor, endColor, transition);
 24 
 25   rlPushMatrix();
 26   rlTranslatef(particle->position.x, particle->position.y, particle->position.z);
 27   rlScalef(scale.x, scale.y, scale.z);
 28   DrawSphere(Vector3Zero(), size, color);
 29   rlPopMatrix();
 30 }
 31 
 32 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime)
 33 {
 34   if (particleCount >= PARTICLE_MAX_COUNT)
 35   {
 36     return;
 37   }
 38 
 39   int index = -1;
 40   for (int i = 0; i < particleCount; i++)
 41   {
 42     if (particles[i].particleType == PARTICLE_TYPE_NONE)
 43     {
 44       index = i;
 45       break;
 46     }
 47   }
 48 
 49   if (index == -1)
 50   {
 51     index = particleCount++;
 52   }
 53 
 54   Particle *particle = &particles[index];
 55   particle->particleType = particleType;
 56   particle->spawnTime = gameTime.time;
 57   particle->lifetime = lifetime;
 58   particle->position = position;
 59   particle->velocity = velocity;
 60   particle->scale = scale;
 61 }
 62 
 63 void ParticleUpdate()
 64 {
 65   for (int i = 0; i < particleCount; i++)
 66   {
 67     Particle *particle = &particles[i];
 68     if (particle->particleType == PARTICLE_TYPE_NONE)
 69     {
 70       continue;
 71     }
 72 
 73     float age = gameTime.time - particle->spawnTime;
 74 
 75     if (particle->lifetime > age)
 76     {
 77       particle->position = Vector3Add(particle->position, Vector3Scale(particle->velocity, gameTime.deltaTime));
 78     }
 79     else {
 80       particle->particleType = PARTICLE_TYPE_NONE;
 81     }
 82   }
 83 }
 84 
 85 void ParticleDraw()
 86 {
 87   for (int i = 0; i < particleCount; i++)
 88   {
 89     Particle particle = particles[i];
 90     if (particle.particleType == PARTICLE_TYPE_NONE)
 91     {
 92       continue;
 93     }
 94 
 95     float age = gameTime.time - particle.spawnTime;
 96     float transition = age / particle.lifetime;
 97     switch (particle.particleType)
 98     {
 99     case PARTICLE_TYPE_EXPLOSION:
100       DrawExplosionParticle(&particle, transition);
101       break;
102     default:
103       DrawCube(particle.position, 0.3f, 0.5f, 0.3f, RED);
104       break;
105     }
106   }
107 }
  1 #include "raylib.h"
  2 #include "preferred_size.h"
  3 
  4 // Since the canvas size is not known at compile time, we need to query it at runtime;
  5 // the following platform specific code obtains the canvas size and we will use this
  6 // size as the preferred size for the window at init time. We're ignoring here the
  7 // possibility of the canvas size changing during runtime - this would require to
  8 // poll the canvas size in the game loop or establishing a callback to be notified
  9 
 10 #ifdef PLATFORM_WEB
 11 #include <emscripten.h>
 12 EMSCRIPTEN_RESULT emscripten_get_element_css_size(const char *target, double *width, double *height);
 13 
 14 void GetPreferredSize(int *screenWidth, int *screenHeight)
 15 {
 16   double canvasWidth, canvasHeight;
 17   emscripten_get_element_css_size("#" CANVAS_NAME, &canvasWidth, &canvasHeight);
 18   *screenWidth = (int)canvasWidth;
 19   *screenHeight = (int)canvasHeight;
 20   TraceLog(LOG_INFO, "preferred size for %s: %d %d", CANVAS_NAME, *screenWidth, *screenHeight);
 21 }
 22 
 23 int IsPaused()
 24 {
 25   const char *js = "(function(){\n"
 26   "  var canvas = document.getElementById(\"" CANVAS_NAME "\");\n"
 27   "  var rect = canvas.getBoundingClientRect();\n"
 28   "  var isVisible = (\n"
 29   "    rect.top >= 0 &&\n"
 30   "    rect.left >= 0 &&\n"
 31   "    rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&\n"
 32   "    rect.right <= (window.innerWidth || document.documentElement.clientWidth)\n"
 33   "  );\n"
 34   "  return isVisible ? 0 : 1;\n"
 35   "})()";
 36   return emscripten_run_script_int(js);
 37 }
 38 
 39 #else
 40 void GetPreferredSize(int *screenWidth, int *screenHeight)
 41 {
 42   *screenWidth = 600;
 43   *screenHeight = 240;
 44 }
 45 int IsPaused()
 46 {
 47   return 0;
 48 }
 49 #endif
  1 #ifndef PREFERRED_SIZE_H
  2 #define PREFERRED_SIZE_H
  3 
  4 void GetPreferredSize(int *screenWidth, int *screenHeight);
  5 int IsPaused();
  6 
  7 #endif
The orange boxes show the path the enemies will take.

When building a more complex level, it's now much clearer how the enemies will move through it:

path points visualized

However, it would be better if the path was updated when previewing a new building placement. For that, we have to look into how the pathfinding handles the placed towers during the distance map calculation:

  1 void PathFindingMapUpdate()
  2 {
  3   //...
  4   for (int i = 0; i < towerCount; i++)
  5   {
  6     Tower *tower = &towers[i];
  7     if (tower->towerType == TOWER_TYPE_NONE || tower->towerType == TOWER_TYPE_BASE)
  8     {
  9       continue;
 10     }
 11     int16_t mapX, mapY;
 12     if (!PathFindingFromWorldToMapPosition((Vector3){tower->x, 0.0f, tower->y}, &mapX, &mapY))
 13     {
 14       continue;
 15     }
 16     int index = mapY * width + mapX;
 17     pathfindingMap.towerIndex[index] = i;
 18   }
 19 }

What we want to do here is to block cells where buildings are about to be built. We can achieve this in 2 ways: We could place a building that is flagged as "preview" but that blocks the path already or we supply a list of cells that are blocked to the pathfinding system.

Both approaches are valid. I have already another feature in mind that I want to add soon that may play into the decision which approach to take: At a later point, we have to refine the building placement. It should show the tower we are about to place and this tower is following the mouse hover or can be moved via touch input on mobile.

The first approach (placing a preview building) could make this easier to implement - at first. Because when moving the tower around, it should hover and move smoothly, not just jump from cell to cell. This again would be difficult to implement with the first approach.

So I will go with the second approach: Supplying a list of blocked cells to the pathfinding system and keeping the grid and the temporary cell blocking separate:

  • 💾
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 #include <stdlib.h>
  4 #include <math.h>
  5 
  6 //# Variables
  7 GUIState guiState = {0};
  8 GameTime gameTime = {0};
  9 
 10 Model floorTileAModel = {0};
 11 Model floorTileBModel = {0};
 12 Model treeModel[2] = {0};
 13 Model firTreeModel[2] = {0};
 14 Model rockModels[5] = {0};
 15 Model grassPatchModel[1] = {0};
 16 
 17 Texture2D palette, spriteSheet;
 18 
 19 Level levels[] = {
 20   [0] = {
 21     .state = LEVEL_STATE_BUILDING,
 22     .initialGold = 20,
 23     .waves[0] = {
 24       .enemyType = ENEMY_TYPE_MINION,
 25       .wave = 0,
 26       .count = 10,
 27       .interval = 2.5f,
 28       .delay = 1.0f,
 29       .spawnPosition = {0, 6},
 30     },
 31     .waves[1] = {
 32       .enemyType = ENEMY_TYPE_MINION,
 33       .wave = 1,
 34       .count = 20,
 35       .interval = 1.5f,
 36       .delay = 1.0f,
 37       .spawnPosition = {0, 6},
 38     },
 39     .waves[2] = {
 40       .enemyType = ENEMY_TYPE_MINION,
 41       .wave = 2,
 42       .count = 30,
 43       .interval = 1.2f,
 44       .delay = 1.0f,
 45       .spawnPosition = {0, 6},
 46     }
 47   },
 48 };
 49 
 50 Level *currentLevel = levels;
 51 
 52 //# Game
 53 
 54 static Model LoadGLBModel(char *filename)
 55 {
 56   Model model = LoadModel(TextFormat("data/%s.glb",filename));
 57   if (model.materialCount > 1)
 58   {
 59     model.materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
 60   }
 61   return model;
 62 }
 63 
 64 void LoadAssets()
 65 {
 66   // load a sprite sheet that contains all units
 67   spriteSheet = LoadTexture("data/spritesheet.png");
 68   SetTextureFilter(spriteSheet, TEXTURE_FILTER_BILINEAR);
 69 
 70   // we'll use a palette texture to colorize the all buildings and environment art
 71   palette = LoadTexture("data/palette.png");
 72   // The texture uses gradients on very small space, so we'll enable bilinear filtering
 73   SetTextureFilter(palette, TEXTURE_FILTER_BILINEAR);
 74 
 75   floorTileAModel = LoadGLBModel("floor-tile-a");
 76   floorTileBModel = LoadGLBModel("floor-tile-b");
 77   treeModel[0] = LoadGLBModel("leaftree-large-1-a");
 78   treeModel[1] = LoadGLBModel("leaftree-large-1-b");
 79   firTreeModel[0] = LoadGLBModel("firtree-1-a");
 80   firTreeModel[1] = LoadGLBModel("firtree-1-b");
 81   rockModels[0] = LoadGLBModel("rock-1");
 82   rockModels[1] = LoadGLBModel("rock-2");
 83   rockModels[2] = LoadGLBModel("rock-3");
 84   rockModels[3] = LoadGLBModel("rock-4");
 85   rockModels[4] = LoadGLBModel("rock-5");
 86   grassPatchModel[0] = LoadGLBModel("grass-patch-1");
 87 }
 88 
 89 void InitLevel(Level *level)
 90 {
 91   level->seed = (int)(GetTime() * 100.0f);
 92 
 93   TowerInit();
 94   EnemyInit();
 95   ProjectileInit();
 96   ParticleInit();
 97   TowerTryAdd(TOWER_TYPE_BASE, 0, 0);
 98 
 99   level->placementMode = 0;
100   level->state = LEVEL_STATE_BUILDING;
101   level->nextState = LEVEL_STATE_NONE;
102   level->playerGold = level->initialGold;
103   level->currentWave = 0;
104 
105   Camera *camera = &level->camera;
106   camera->position = (Vector3){4.0f, 8.0f, 8.0f};
107   camera->target = (Vector3){0.0f, 0.0f, 0.0f};
108   camera->up = (Vector3){0.0f, 1.0f, 0.0f};
109   camera->fovy = 10.0f;
110   camera->projection = CAMERA_ORTHOGRAPHIC;
111 }
112 
113 void DrawLevelHud(Level *level)
114 {
115   const char *text = TextFormat("Gold: %d", level->playerGold);
116   Font font = GetFontDefault();
117   DrawTextEx(font, text, (Vector2){GetScreenWidth() - 120, 10}, font.baseSize * 2.0f, 2.0f, BLACK);
118   DrawTextEx(font, text, (Vector2){GetScreenWidth() - 122, 8}, font.baseSize * 2.0f, 2.0f, YELLOW);
119 }
120 
121 void DrawLevelReportLostWave(Level *level)
122 {
123   BeginMode3D(level->camera);
124   DrawLevelGround(level);
125   TowerDraw();
126   EnemyDraw();
127   ProjectileDraw();
128   ParticleDraw();
129   guiState.isBlocked = 0;
130   EndMode3D();
131 
132   TowerDrawHealthBars(level->camera);
133 
134   const char *text = "Wave lost";
135   int textWidth = MeasureText(text, 20);
136   DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
137 
138   if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
139   {
140     level->nextState = LEVEL_STATE_RESET;
141   }
142 }
143 
144 int HasLevelNextWave(Level *level)
145 {
146   for (int i = 0; i < 10; i++)
147   {
148     EnemyWave *wave = &level->waves[i];
149     if (wave->wave == level->currentWave)
150     {
151       return 1;
152     }
153   }
154   return 0;
155 }
156 
157 void DrawLevelReportWonWave(Level *level)
158 {
159   BeginMode3D(level->camera);
160   DrawLevelGround(level);
161   TowerDraw();
162   EnemyDraw();
163   ProjectileDraw();
164   ParticleDraw();
165   guiState.isBlocked = 0;
166   EndMode3D();
167 
168   TowerDrawHealthBars(level->camera);
169 
170   const char *text = "Wave won";
171   int textWidth = MeasureText(text, 20);
172   DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
173 
174 
175   if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
176   {
177     level->nextState = LEVEL_STATE_RESET;
178   }
179 
180   if (HasLevelNextWave(level))
181   {
182     if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
183     {
184       level->nextState = LEVEL_STATE_BUILDING;
185     }
186   }
187   else {
188     if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
189     {
190       level->nextState = LEVEL_STATE_WON_LEVEL;
191     }
192   }
193 }
194 
195 void DrawBuildingBuildButton(Level *level, int x, int y, int width, int height, uint8_t towerType, const char *name)
196 {
197   static ButtonState buttonStates[8] = {0};
198   int cost = GetTowerCosts(towerType);
199   const char *text = TextFormat("%s: %d", name, cost);
200   buttonStates[towerType].isSelected = level->placementMode == towerType;
201   buttonStates[towerType].isDisabled = level->playerGold < cost;
202   if (Button(text, x, y, width, height, &buttonStates[towerType]))
203   {
204     level->placementMode = buttonStates[towerType].isSelected ? 0 : towerType;
205   }
206 }
207 
208 float GetRandomFloat(float min, float max)
209 {
210   int random = GetRandomValue(0, 0xfffffff);
211   return ((float)random / (float)0xfffffff) * (max - min) + min;
212 }
213 
214 void DrawLevelGround(Level *level)
215 {
216   // draw checkerboard ground pattern
217   for (int x = -5; x <= 5; x += 1)
218   {
219     for (int y = -5; y <= 5; y += 1)
220     {
221       Model *model = (x + y) % 2 == 0 ? &floorTileAModel : &floorTileBModel;
222       DrawModel(*model, (Vector3){x, 0.0f, y}, 1.0f, WHITE);
223     }
224   }
225 
226   int oldSeed = GetRandomValue(0, 0xfffffff);
227   SetRandomSeed(level->seed);
228   // increase probability for trees via duplicated entries
229   Model borderModels[64];
230   int maxRockCount = GetRandomValue(2, 6);
231   int maxTreeCount = GetRandomValue(10, 20);
232   int maxFirTreeCount = GetRandomValue(5, 10);
233   int maxLeafTreeCount = maxTreeCount - maxFirTreeCount;
234   int grassPatchCount = GetRandomValue(5, 30);
235 
236   int modelCount = 0;
237   for (int i = 0; i < maxRockCount && modelCount < 63; i++)
238   {
239     borderModels[modelCount++] = rockModels[GetRandomValue(0, 5)];
240   }
241   for (int i = 0; i < maxLeafTreeCount && modelCount < 63; i++)
242   {
243     borderModels[modelCount++] = treeModel[GetRandomValue(0, 1)];
244   }
245   for (int i = 0; i < maxFirTreeCount && modelCount < 63; i++)
246   {
247     borderModels[modelCount++] = firTreeModel[GetRandomValue(0, 1)];
248   }
249   for (int i = 0; i < grassPatchCount && modelCount < 63; i++)
250   {
251     borderModels[modelCount++] = grassPatchModel[0];
252   }
253 
254   // draw some objects around the border of the map
255   Vector3 up = {0, 1, 0};
256   // a pseudo random number generator to get the same result every time
257   const float wiggle = 0.75f;
258   const int layerCount = 3;
259   for (int layer = 0; layer < layerCount; layer++)
260   {
261     int layerPos = 6 + layer;
262     for (int x = -6 + layer; x <= 6 + layer; x += 1)
263     {
264       DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)], 
265         (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, -layerPos + GetRandomFloat(0.0f, wiggle)}, 
266         up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
267       DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)], 
268         (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, layerPos + GetRandomFloat(0.0f, wiggle)}, 
269         up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
270     }
271 
272     for (int z = -5 + layer; z <= 5 + layer; z += 1)
273     {
274       DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)], 
275         (Vector3){-layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)}, 
276         up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
277       DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)], 
278         (Vector3){layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)}, 
279         up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
280     }
281   }
282 
283   SetRandomSeed(oldSeed);
284 }
285 
286 void DrawEnemyPath(Level *level)
287 {
288   for (int i = 0; i < ENEMY_MAX_WAVE_COUNT; i++)
289   {
290     EnemyWave *wave = &level->waves[i];
291     if (wave->wave != level->currentWave)
292     {
293       continue;
294     }
295     Vector3 position = (Vector3){wave->spawnPosition.x, 0.0f, wave->spawnPosition.y};
296     DrawCube(position, 0.25f, 2.0f, 0.25f, RED);
297     for (int j = 0; j < 100; j++)
298     {
299       Vector2 gradient = PathFindingGetGradient(position);
300       if (gradient.x == 0 && gradient.y == 0)
301       {
302         break;
303       }
304       position.x += gradient.x;
305       position.z += gradient.y;
306       DrawCube(position, 0.2f, 1.25f, 0.2f, ORANGE);
307     }
308   }
309 }
310 
311 void DrawLevelBuildingState(Level *level)
312 {
313   BeginMode3D(level->camera);
314 DrawLevelGround(level); 315 316 int blockedCellCount = 0; 317 Vector2 blockedCells[1];
318 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera); 319 float planeDistance = ray.position.y / -ray.direction.y; 320 float planeX = ray.direction.x * planeDistance + ray.position.x; 321 float planeY = ray.direction.z * planeDistance + ray.position.z; 322 int16_t mapX = (int16_t)floorf(planeX + 0.5f);
323 int16_t mapY = (int16_t)floorf(planeY + 0.5f); 324 if (level->placementMode && !guiState.isBlocked && mapX >= -5 && mapX <= 5 && mapY >= -5 && mapY <= 5)
325 { 326 DrawCubeWires((Vector3){mapX, 0.2f, mapY}, 1.0f, 0.4f, 1.0f, RED); 327 blockedCells[blockedCellCount++] = (Vector2){mapX, mapY}; 328 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) 329 { 330 if (TowerTryAdd(level->placementMode, mapX, mapY)) 331 { 332 level->playerGold -= GetTowerCosts(level->placementMode);
333 level->placementMode = TOWER_TYPE_NONE; 334 } 335 } 336 } 337 338 PathFindingMapUpdate(blockedCellCount, blockedCells); 339 DrawEnemyPath(level); 340 TowerDraw();
341 EnemyDraw(); 342 ProjectileDraw(); 343 ParticleDraw(); 344 345 guiState.isBlocked = 0; 346 347 EndMode3D(); 348 349 TowerDrawHealthBars(level->camera); 350 351 static ButtonState buildWallButtonState = {0}; 352 static ButtonState buildGunButtonState = {0}; 353 buildWallButtonState.isSelected = level->placementMode == TOWER_TYPE_WALL; 354 buildGunButtonState.isSelected = level->placementMode == TOWER_TYPE_ARCHER; 355 356 DrawBuildingBuildButton(level, 10, 10, 110, 30, TOWER_TYPE_WALL, "Wall"); 357 DrawBuildingBuildButton(level, 10, 50, 110, 30, TOWER_TYPE_ARCHER, "Archer"); 358 DrawBuildingBuildButton(level, 10, 90, 110, 30, TOWER_TYPE_BALLISTA, "Ballista"); 359 DrawBuildingBuildButton(level, 10, 130, 110, 30, TOWER_TYPE_CATAPULT, "Catapult"); 360 361 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 362 { 363 level->nextState = LEVEL_STATE_RESET; 364 } 365 366 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0)) 367 { 368 level->nextState = LEVEL_STATE_BATTLE; 369 } 370 371 const char *text = "Building phase"; 372 int textWidth = MeasureText(text, 20); 373 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 374 } 375 376 void InitBattleStateConditions(Level *level) 377 { 378 level->state = LEVEL_STATE_BATTLE; 379 level->nextState = LEVEL_STATE_NONE; 380 level->waveEndTimer = 0.0f; 381 for (int i = 0; i < 10; i++) 382 { 383 EnemyWave *wave = &level->waves[i]; 384 wave->spawned = 0; 385 wave->timeToSpawnNext = wave->delay; 386 } 387 } 388 389 void DrawLevelBattleState(Level *level) 390 { 391 BeginMode3D(level->camera); 392 DrawLevelGround(level); 393 TowerDraw(); 394 EnemyDraw(); 395 ProjectileDraw(); 396 ParticleDraw(); 397 guiState.isBlocked = 0; 398 EndMode3D(); 399 400 EnemyDrawHealthbars(level->camera); 401 TowerDrawHealthBars(level->camera); 402 403 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 404 { 405 level->nextState = LEVEL_STATE_RESET; 406 } 407 408 int maxCount = 0; 409 int remainingCount = 0; 410 for (int i = 0; i < 10; i++) 411 { 412 EnemyWave *wave = &level->waves[i]; 413 if (wave->wave != level->currentWave) 414 { 415 continue; 416 } 417 maxCount += wave->count; 418 remainingCount += wave->count - wave->spawned; 419 } 420 int aliveCount = EnemyCount(); 421 remainingCount += aliveCount; 422 423 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount); 424 int textWidth = MeasureText(text, 20); 425 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 426 } 427 428 void DrawLevel(Level *level) 429 { 430 switch (level->state) 431 { 432 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break; 433 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break; 434 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break; 435 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break; 436 default: break; 437 } 438 439 DrawLevelHud(level); 440 } 441 442 void UpdateLevel(Level *level) 443 { 444 if (level->state == LEVEL_STATE_BATTLE) 445 { 446 int activeWaves = 0; 447 for (int i = 0; i < 10; i++) 448 { 449 EnemyWave *wave = &level->waves[i]; 450 if (wave->spawned >= wave->count || wave->wave != level->currentWave) 451 { 452 continue; 453 } 454 activeWaves++; 455 wave->timeToSpawnNext -= gameTime.deltaTime; 456 if (wave->timeToSpawnNext <= 0.0f) 457 { 458 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y); 459 if (enemy) 460 { 461 wave->timeToSpawnNext = wave->interval; 462 wave->spawned++; 463 } 464 } 465 } 466 if (GetTowerByType(TOWER_TYPE_BASE) == 0) { 467 level->waveEndTimer += gameTime.deltaTime; 468 if (level->waveEndTimer >= 2.0f) 469 { 470 level->nextState = LEVEL_STATE_LOST_WAVE; 471 } 472 } 473 else if (activeWaves == 0 && EnemyCount() == 0) 474 { 475 level->waveEndTimer += gameTime.deltaTime; 476 if (level->waveEndTimer >= 2.0f) 477 { 478 level->nextState = LEVEL_STATE_WON_WAVE; 479 }
480 }
481 } 482 483 PathFindingMapUpdate(0, 0); 484 EnemyUpdate(); 485 TowerUpdate(); 486 ProjectileUpdate(); 487 ParticleUpdate(); 488 489 if (level->nextState == LEVEL_STATE_RESET) 490 { 491 InitLevel(level); 492 } 493 494 if (level->nextState == LEVEL_STATE_BATTLE) 495 { 496 InitBattleStateConditions(level); 497 } 498 499 if (level->nextState == LEVEL_STATE_WON_WAVE) 500 { 501 level->currentWave++; 502 level->state = LEVEL_STATE_WON_WAVE; 503 } 504 505 if (level->nextState == LEVEL_STATE_LOST_WAVE) 506 { 507 level->state = LEVEL_STATE_LOST_WAVE; 508 } 509 510 if (level->nextState == LEVEL_STATE_BUILDING) 511 { 512 level->state = LEVEL_STATE_BUILDING; 513 } 514 515 if (level->nextState == LEVEL_STATE_WON_LEVEL) 516 { 517 // make something of this later 518 InitLevel(level); 519 } 520 521 level->nextState = LEVEL_STATE_NONE; 522 } 523 524 float nextSpawnTime = 0.0f; 525 526 void ResetGame() 527 { 528 InitLevel(currentLevel); 529 } 530 531 void InitGame() 532 { 533 TowerInit(); 534 EnemyInit(); 535 ProjectileInit(); 536 ParticleInit(); 537 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f); 538 539 currentLevel = levels; 540 InitLevel(currentLevel); 541 } 542 543 //# Immediate GUI functions 544 545 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth) 546 { 547 const float healthBarHeight = 6.0f; 548 const float healthBarOffset = 15.0f; 549 const float inset = 2.0f; 550 const float innerWidth = healthBarWidth - inset * 2; 551 const float innerHeight = healthBarHeight - inset * 2; 552 553 Vector2 screenPos = GetWorldToScreen(position, camera); 554 float centerX = screenPos.x - healthBarWidth * 0.5f; 555 float topY = screenPos.y - healthBarOffset; 556 DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK); 557 float healthWidth = innerWidth * healthRatio; 558 DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor); 559 } 560 561 int Button(const char *text, int x, int y, int width, int height, ButtonState *state) 562 { 563 Rectangle bounds = {x, y, width, height}; 564 int isPressed = 0; 565 int isSelected = state && state->isSelected; 566 int isDisabled = state && state->isDisabled; 567 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !isDisabled) 568 { 569 Color color = isSelected ? DARKGRAY : GRAY; 570 DrawRectangle(x, y, width, height, color); 571 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) 572 { 573 isPressed = 1; 574 } 575 guiState.isBlocked = 1; 576 } 577 else 578 { 579 Color color = isSelected ? WHITE : LIGHTGRAY; 580 DrawRectangle(x, y, width, height, color); 581 } 582 Font font = GetFontDefault(); 583 Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1); 584 Color textColor = isDisabled ? GRAY : BLACK; 585 DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, textColor); 586 return isPressed; 587 } 588 589 //# Main game loop 590 591 void GameUpdate() 592 { 593 float dt = GetFrameTime(); 594 // cap maximum delta time to 0.1 seconds to prevent large time steps 595 if (dt > 0.1f) dt = 0.1f; 596 gameTime.time += dt; 597 gameTime.deltaTime = dt; 598 599 UpdateLevel(currentLevel); 600 } 601 602 int main(void) 603 { 604 int screenWidth, screenHeight; 605 GetPreferredSize(&screenWidth, &screenHeight); 606 InitWindow(screenWidth, screenHeight, "Tower defense"); 607 SetTargetFPS(30); 608 609 LoadAssets(); 610 InitGame(); 611 612 while (!WindowShouldClose()) 613 { 614 if (IsPaused()) { 615 // canvas is not visible in browser - do nothing 616 continue; 617 } 618 619 BeginDrawing(); 620 ClearBackground((Color){0x4E, 0x63, 0x26, 0xFF}); 621 622 GameUpdate(); 623 DrawLevel(currentLevel); 624 625 EndDrawing(); 626 } 627 628 CloseWindow(); 629 630 return 0; 631 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 
  4 // The queue is a simple array of nodes, we add nodes to the end and remove
  5 // nodes from the front. We keep the array around to avoid unnecessary allocations
  6 static PathfindingNode *pathfindingNodeQueue = 0;
  7 static int pathfindingNodeQueueCount = 0;
  8 static int pathfindingNodeQueueCapacity = 0;
  9 
 10 // The pathfinding map stores the distances from the castle to each cell in the map.
 11 static PathfindingMap pathfindingMap = {0};
 12 
 13 void PathfindingMapInit(int width, int height, Vector3 translate, float scale)
 14 {
 15   // transforming between map space and world space allows us to adapt 
 16   // position and scale of the map without changing the pathfinding data
 17   pathfindingMap.toWorldSpace = MatrixTranslate(translate.x, translate.y, translate.z);
 18   pathfindingMap.toWorldSpace = MatrixMultiply(pathfindingMap.toWorldSpace, MatrixScale(scale, scale, scale));
 19   pathfindingMap.toMapSpace = MatrixInvert(pathfindingMap.toWorldSpace);
 20   pathfindingMap.width = width;
 21   pathfindingMap.height = height;
 22   pathfindingMap.scale = scale;
 23   pathfindingMap.distances = (float *)MemAlloc(width * height * sizeof(float));
 24   for (int i = 0; i < width * height; i++)
 25   {
 26     pathfindingMap.distances[i] = -1.0f;
 27   }
 28 
 29   pathfindingMap.towerIndex = (long *)MemAlloc(width * height * sizeof(long));
 30   pathfindingMap.deltaSrc = (DeltaSrc *)MemAlloc(width * height * sizeof(DeltaSrc));
 31 }
 32 
 33 static void PathFindingNodePush(int16_t x, int16_t y, int16_t fromX, int16_t fromY, float distance)
 34 {
 35   if (pathfindingNodeQueueCount >= pathfindingNodeQueueCapacity)
 36   {
 37     pathfindingNodeQueueCapacity = pathfindingNodeQueueCapacity == 0 ? 256 : pathfindingNodeQueueCapacity * 2;
 38     // we use MemAlloc/MemRealloc to allocate memory for the queue
 39     // I am not entirely sure if MemRealloc allows passing a null pointer
 40     // so we check if the pointer is null and use MemAlloc in that case
 41     if (pathfindingNodeQueue == 0)
 42     {
 43       pathfindingNodeQueue = (PathfindingNode *)MemAlloc(pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
 44     }
 45     else
 46     {
 47       pathfindingNodeQueue = (PathfindingNode *)MemRealloc(pathfindingNodeQueue, pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
 48     }
 49   }
 50 
 51   PathfindingNode *node = &pathfindingNodeQueue[pathfindingNodeQueueCount++];
 52   node->x = x;
 53   node->y = y;
 54   node->fromX = fromX;
 55   node->fromY = fromY;
 56   node->distance = distance;
 57 }
 58 
 59 static PathfindingNode *PathFindingNodePop()
 60 {
 61   if (pathfindingNodeQueueCount == 0)
 62   {
 63     return 0;
 64   }
 65   // we return the first node in the queue; we want to return a pointer to the node
 66   // so we can return 0 if the queue is empty. 
 67   // We should _not_ return a pointer to the element in the list, because the list
 68   // may be reallocated and the pointer would become invalid. Or the 
 69   // popped element is overwritten by the next push operation.
 70   // Using static here means that the variable is permanently allocated.
 71   static PathfindingNode node;
 72   node = pathfindingNodeQueue[0];
 73   // we shift all nodes one position to the front
 74   for (int i = 1; i < pathfindingNodeQueueCount; i++)
 75   {
 76     pathfindingNodeQueue[i - 1] = pathfindingNodeQueue[i];
 77   }
 78   --pathfindingNodeQueueCount;
 79   return &node;
 80 }
 81 
 82 float PathFindingGetDistance(int mapX, int mapY)
 83 {
 84   if (mapX < 0 || mapX >= pathfindingMap.width || mapY < 0 || mapY >= pathfindingMap.height)
 85   {
 86     // when outside the map, we return the manhattan distance to the castle (0,0)
 87     return fabsf((float)mapX) + fabsf((float)mapY);
 88   }
 89 
 90   return pathfindingMap.distances[mapY * pathfindingMap.width + mapX];
 91 }
 92 
 93 // transform a world position to a map position in the array; 
 94 // returns true if the position is inside the map
 95 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY)
 96 {
 97   Vector3 mapPosition = Vector3Transform(worldPosition, pathfindingMap.toMapSpace);
 98   *mapX = (int16_t)mapPosition.x;
 99   *mapY = (int16_t)mapPosition.z;
100   return *mapX >= 0 && *mapX < pathfindingMap.width && *mapY >= 0 && *mapY < pathfindingMap.height;
101 }
102 
103 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells)
104 { 105 const int castleX = 0, castleY = 0; 106 int16_t castleMapX, castleMapY; 107 if (!PathFindingFromWorldToMapPosition((Vector3){castleX, 0.0f, castleY}, &castleMapX, &castleMapY)) 108 { 109 return; 110 } 111 int width = pathfindingMap.width, height = pathfindingMap.height; 112 113 // reset the distances to -1 114 for (int i = 0; i < width * height; i++) 115 { 116 pathfindingMap.distances[i] = -1.0f; 117 } 118 // reset the tower indices 119 for (int i = 0; i < width * height; i++) 120 { 121 pathfindingMap.towerIndex[i] = -1; 122 } 123 // reset the delta src 124 for (int i = 0; i < width * height; i++) 125 { 126 pathfindingMap.deltaSrc[i].x = 0;
127 pathfindingMap.deltaSrc[i].y = 0; 128 } 129 130 for (int i = 0; i < blockedCellCount; i++) 131 { 132 int16_t mapX, mapY; 133 if (!PathFindingFromWorldToMapPosition((Vector3){blockedCells[i].x, 0.0f, blockedCells[i].y}, &mapX, &mapY)) 134 { 135 continue; 136 } 137 int index = mapY * width + mapX; 138 pathfindingMap.towerIndex[index] = -2;
139 } 140 141 for (int i = 0; i < towerCount; i++) 142 { 143 Tower *tower = &towers[i]; 144 if (tower->towerType == TOWER_TYPE_NONE || tower->towerType == TOWER_TYPE_BASE) 145 { 146 continue; 147 } 148 int16_t mapX, mapY; 149 // technically, if the tower cell scale is not in sync with the pathfinding map scale, 150 // this would not work correctly and needs to be refined to allow towers covering multiple cells 151 // or having multiple towers in one cell; for simplicity, we assume that the tower covers exactly 152 // one cell. For now. 153 if (!PathFindingFromWorldToMapPosition((Vector3){tower->x, 0.0f, tower->y}, &mapX, &mapY)) 154 { 155 continue; 156 } 157 int index = mapY * width + mapX; 158 pathfindingMap.towerIndex[index] = i; 159 } 160 161 // we start at the castle and add the castle to the queue 162 pathfindingMap.maxDistance = 0.0f; 163 pathfindingNodeQueueCount = 0; 164 PathFindingNodePush(castleMapX, castleMapY, castleMapX, castleMapY, 0.0f); 165 PathfindingNode *node = 0; 166 while ((node = PathFindingNodePop())) 167 { 168 if (node->x < 0 || node->x >= width || node->y < 0 || node->y >= height) 169 { 170 continue; 171 } 172 int index = node->y * width + node->x; 173 if (pathfindingMap.distances[index] >= 0 && pathfindingMap.distances[index] <= node->distance) 174 { 175 continue; 176 } 177 178 int deltaX = node->x - node->fromX; 179 int deltaY = node->y - node->fromY; 180 // even if the cell is blocked by a tower, we still may want to store the direction 181 // (though this might not be needed, IDK right now) 182 pathfindingMap.deltaSrc[index].x = (char) deltaX; 183 pathfindingMap.deltaSrc[index].y = (char) deltaY; 184
185 // we skip nodes that are blocked by towers or by the provided blocked cells 186 if (pathfindingMap.towerIndex[index] != -1)
187 { 188 node->distance += 8.0f; 189 } 190 pathfindingMap.distances[index] = node->distance; 191 pathfindingMap.maxDistance = fmaxf(pathfindingMap.maxDistance, node->distance); 192 PathFindingNodePush(node->x, node->y + 1, node->x, node->y, node->distance + 1.0f); 193 PathFindingNodePush(node->x, node->y - 1, node->x, node->y, node->distance + 1.0f); 194 PathFindingNodePush(node->x + 1, node->y, node->x, node->y, node->distance + 1.0f); 195 PathFindingNodePush(node->x - 1, node->y, node->x, node->y, node->distance + 1.0f); 196 } 197 } 198 199 void PathFindingMapDraw() 200 { 201 float cellSize = pathfindingMap.scale * 0.9f; 202 float highlightDistance = fmodf(GetTime() * 4.0f, pathfindingMap.maxDistance); 203 for (int x = 0; x < pathfindingMap.width; x++) 204 { 205 for (int y = 0; y < pathfindingMap.height; y++) 206 { 207 float distance = pathfindingMap.distances[y * pathfindingMap.width + x]; 208 float colorV = distance < 0 ? 0 : fminf(distance / pathfindingMap.maxDistance, 1.0f); 209 Color color = distance < 0 ? BLUE : (Color){fminf(colorV, 1.0f) * 255, 0, 0, 255}; 210 Vector3 position = Vector3Transform((Vector3){x, -0.25f, y}, pathfindingMap.toWorldSpace); 211 // animate the distance "wave" to show how the pathfinding algorithm expands 212 // from the castle 213 if (distance + 0.5f > highlightDistance && distance - 0.5f < highlightDistance) 214 { 215 color = BLACK; 216 } 217 DrawCube(position, cellSize, 0.1f, cellSize, color); 218 } 219 } 220 } 221 222 Vector2 PathFindingGetGradient(Vector3 world) 223 { 224 int16_t mapX, mapY; 225 if (PathFindingFromWorldToMapPosition(world, &mapX, &mapY)) 226 { 227 DeltaSrc delta = pathfindingMap.deltaSrc[mapY * pathfindingMap.width + mapX]; 228 return (Vector2){(float)-delta.x, (float)-delta.y}; 229 } 230 // fallback to a simple gradient calculation 231 float n = PathFindingGetDistance(mapX, mapY - 1); 232 float s = PathFindingGetDistance(mapX, mapY + 1); 233 float w = PathFindingGetDistance(mapX - 1, mapY); 234 float e = PathFindingGetDistance(mapX + 1, mapY); 235 return (Vector2){w - e + 0.25f, n - s + 0.125f}; 236 }
  1 #ifndef TD_TUT_2_MAIN_H
  2 #define TD_TUT_2_MAIN_H
  3 
  4 #include <inttypes.h>
  5 
  6 #include "raylib.h"
  7 #include "preferred_size.h"
  8 
  9 //# Declarations
 10 
 11 #define ENEMY_MAX_PATH_COUNT 8
 12 #define ENEMY_MAX_COUNT 400
 13 #define ENEMY_TYPE_NONE 0
 14 #define ENEMY_TYPE_MINION 1
 15 
 16 #define PARTICLE_MAX_COUNT 400
 17 #define PARTICLE_TYPE_NONE 0
 18 #define PARTICLE_TYPE_EXPLOSION 1
 19 
 20 typedef struct Particle
 21 {
 22   uint8_t particleType;
 23   float spawnTime;
 24   float lifetime;
 25   Vector3 position;
 26   Vector3 velocity;
 27   Vector3 scale;
 28 } Particle;
 29 
 30 #define TOWER_MAX_COUNT 400
 31 enum TowerType
 32 {
 33   TOWER_TYPE_NONE,
 34   TOWER_TYPE_BASE,
 35   TOWER_TYPE_ARCHER,
 36   TOWER_TYPE_BALLISTA,
 37   TOWER_TYPE_CATAPULT,
 38   TOWER_TYPE_WALL,
 39   TOWER_TYPE_COUNT
 40 };
 41 
 42 typedef struct HitEffectConfig
 43 {
 44   float damage;
 45   float areaDamageRadius;
 46   float pushbackPowerDistance;
 47 } HitEffectConfig;
 48 
 49 typedef struct TowerTypeConfig
 50 {
 51   float cooldown;
 52   float range;
 53   float projectileSpeed;
 54   
 55   uint8_t cost;
 56   uint8_t projectileType;
 57   uint16_t maxHealth;
 58 
 59   HitEffectConfig hitEffect;
 60 } TowerTypeConfig;
 61 
 62 typedef struct Tower
 63 {
 64   int16_t x, y;
 65   uint8_t towerType;
 66   Vector2 lastTargetPosition;
 67   float cooldown;
 68   float damage;
 69 } Tower;
 70 
 71 typedef struct GameTime
 72 {
 73   float time;
 74   float deltaTime;
 75 } GameTime;
 76 
 77 typedef struct ButtonState {
 78   char isSelected;
 79   char isDisabled;
 80 } ButtonState;
 81 
 82 typedef struct GUIState {
 83   int isBlocked;
 84 } GUIState;
 85 
 86 typedef enum LevelState
 87 {
 88   LEVEL_STATE_NONE,
 89   LEVEL_STATE_BUILDING,
 90   LEVEL_STATE_BATTLE,
 91   LEVEL_STATE_WON_WAVE,
 92   LEVEL_STATE_LOST_WAVE,
 93   LEVEL_STATE_WON_LEVEL,
 94   LEVEL_STATE_RESET,
 95 } LevelState;
 96 
 97 typedef struct EnemyWave {
 98   uint8_t enemyType;
 99   uint8_t wave;
100   uint16_t count;
101   float interval;
102   float delay;
103   Vector2 spawnPosition;
104 
105   uint16_t spawned;
106   float timeToSpawnNext;
107 } EnemyWave;
108 
109 #define ENEMY_MAX_WAVE_COUNT 10
110 
111 typedef struct Level
112 {
113   int seed;
114   LevelState state;
115   LevelState nextState;
116   Camera3D camera;
117   int placementMode;
118 
119   int initialGold;
120   int playerGold;
121 
122   EnemyWave waves[ENEMY_MAX_WAVE_COUNT];
123   int currentWave;
124   float waveEndTimer;
125 } Level;
126 
127 typedef struct DeltaSrc
128 {
129   char x, y;
130 } DeltaSrc;
131 
132 typedef struct PathfindingMap
133 {
134   int width, height;
135   float scale;
136   float *distances;
137   long *towerIndex; 
138   DeltaSrc *deltaSrc;
139   float maxDistance;
140   Matrix toMapSpace;
141   Matrix toWorldSpace;
142 } PathfindingMap;
143 
144 // when we execute the pathfinding algorithm, we need to store the active nodes
145 // in a queue. Each node has a position, a distance from the start, and the
146 // position of the node that we came from.
147 typedef struct PathfindingNode
148 {
149   int16_t x, y, fromX, fromY;
150   float distance;
151 } PathfindingNode;
152 
153 typedef struct EnemyId
154 {
155   uint16_t index;
156   uint16_t generation;
157 } EnemyId;
158 
159 typedef struct EnemyClassConfig
160 {
161   float speed;
162   float health;
163   float radius;
164   float maxAcceleration;
165   float requiredContactTime;
166   float explosionDamage;
167   float explosionRange;
168   float explosionPushbackPower;
169   int goldValue;
170 } EnemyClassConfig;
171 
172 typedef struct Enemy
173 {
174   int16_t currentX, currentY;
175   int16_t nextX, nextY;
176   Vector2 simPosition;
177   Vector2 simVelocity;
178   uint16_t generation;
179   float walkedDistance;
180   float startMovingTime;
181   float damage, futureDamage;
182   float contactTime;
183   uint8_t enemyType;
184   uint8_t movePathCount;
185   Vector2 movePath[ENEMY_MAX_PATH_COUNT];
186 } Enemy;
187 
188 // a unit that uses sprites to be drawn
189 #define SPRITE_UNIT_PHASE_WEAPON_IDLE 0
190 #define SPRITE_UNIT_PHASE_WEAPON_COOLDOWN 1
191 typedef struct SpriteUnit
192 {
193   Rectangle srcRect;
194   Vector2 offset;
195   int frameCount;
196   float frameDuration;
197   Rectangle srcWeaponIdleRect;
198   Vector2 srcWeaponIdleOffset;
199   Rectangle srcWeaponCooldownRect;
200   Vector2 srcWeaponCooldownOffset;
201 } SpriteUnit;
202 
203 #define PROJECTILE_MAX_COUNT 1200
204 #define PROJECTILE_TYPE_NONE 0
205 #define PROJECTILE_TYPE_ARROW 1
206 #define PROJECTILE_TYPE_CATAPULT 2
207 #define PROJECTILE_TYPE_BALLISTA 3
208 
209 typedef struct Projectile
210 {
211   uint8_t projectileType;
212   float shootTime;
213   float arrivalTime;
214   float distance;
215   Vector3 position;
216   Vector3 target;
217   Vector3 directionNormal;
218   EnemyId targetEnemy;
219   HitEffectConfig hitEffectConfig;
220 } Projectile;
221 
222 //# Function declarations
223 float TowerGetMaxHealth(Tower *tower);
224 int Button(const char *text, int x, int y, int width, int height, ButtonState *state);
225 int EnemyAddDamageRange(Vector2 position, float range, float damage);
226 int EnemyAddDamage(Enemy *enemy, float damage);
227 
228 //# Enemy functions
229 void EnemyInit();
230 void EnemyDraw();
231 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource);
232 void EnemyUpdate();
233 float EnemyGetCurrentMaxSpeed(Enemy *enemy);
234 float EnemyGetMaxHealth(Enemy *enemy);
235 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY);
236 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount);
237 EnemyId EnemyGetId(Enemy *enemy);
238 Enemy *EnemyTryResolve(EnemyId enemyId);
239 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY);
240 int EnemyAddDamage(Enemy *enemy, float damage);
241 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range);
242 int EnemyCount();
243 void EnemyDrawHealthbars(Camera3D camera);
244 
245 //# Tower functions
246 void TowerInit();
247 Tower *TowerGetAt(int16_t x, int16_t y);
248 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y);
249 Tower *GetTowerByType(uint8_t towerType);
250 int GetTowerCosts(uint8_t towerType);
251 float TowerGetMaxHealth(Tower *tower);
252 void TowerDraw();
253 void TowerUpdate();
254 void TowerDrawHealthBars(Camera3D camera);
255 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase);
256 
257 //# Particles
258 void ParticleInit();
259 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime);
260 void ParticleUpdate();
261 void ParticleDraw();
262 
263 //# Projectiles
264 void ProjectileInit();
265 void ProjectileDraw();
266 void ProjectileUpdate();
267 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig);
268 
269 //# Pathfinding map
270 void PathfindingMapInit(int width, int height, Vector3 translate, float scale);
271 float PathFindingGetDistance(int mapX, int mapY);
272 Vector2 PathFindingGetGradient(Vector3 world);
273 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY);
274 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells);
275 void PathFindingMapDraw(); 276 277 //# UI 278 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth); 279 280 //# Level 281 void DrawLevelGround(Level *level); 282 void DrawEnemyPath(Level *level); 283 284 //# variables 285 extern Level *currentLevel; 286 extern Enemy enemies[ENEMY_MAX_COUNT]; 287 extern int enemyCount; 288 extern EnemyClassConfig enemyClassConfigs[]; 289 290 extern GUIState guiState; 291 extern GameTime gameTime; 292 extern Tower towers[TOWER_MAX_COUNT]; 293 extern int towerCount; 294 295 extern Texture2D palette, spriteSheet; 296 297 #endif
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 
  4 static TowerTypeConfig towerTypeConfigs[TOWER_TYPE_COUNT] = {
  5     [TOWER_TYPE_BASE] = {
  6         .maxHealth = 10,
  7     },
  8     [TOWER_TYPE_ARCHER] = {
  9         .cooldown = 0.5f,
 10         .range = 3.0f,
 11         .cost = 6,
 12         .maxHealth = 10,
 13         .projectileSpeed = 4.0f,
 14         .projectileType = PROJECTILE_TYPE_ARROW,
 15         .hitEffect = {
 16           .damage = 3.0f,
 17         }
 18     },
 19     [TOWER_TYPE_BALLISTA] = {
 20         .cooldown = 1.5f,
 21         .range = 6.0f,
 22         .cost = 9,
 23         .maxHealth = 10,
 24         .projectileSpeed = 10.0f,
 25         .projectileType = PROJECTILE_TYPE_BALLISTA,
 26         .hitEffect = {
 27           .damage = 6.0f,
 28           .pushbackPowerDistance = 0.25f,
 29         }
 30     },
 31     [TOWER_TYPE_CATAPULT] = {
 32         .cooldown = 1.7f,
 33         .range = 5.0f,
 34         .cost = 10,
 35         .maxHealth = 10,
 36         .projectileSpeed = 3.0f,
 37         .projectileType = PROJECTILE_TYPE_CATAPULT,
 38         .hitEffect = {
 39           .damage = 2.0f,
 40           .areaDamageRadius = 1.75f,
 41         }
 42     },
 43     [TOWER_TYPE_WALL] = {
 44         .cost = 2,
 45         .maxHealth = 10,
 46     },
 47 };
 48 
 49 Tower towers[TOWER_MAX_COUNT];
 50 int towerCount = 0;
 51 
 52 Model towerModels[TOWER_TYPE_COUNT];
 53 
 54 // definition of our archer unit
 55 SpriteUnit archerUnit = {
 56     .srcRect = {0, 0, 16, 16},
 57     .offset = {7, 1},
 58     .frameCount = 1,
 59     .frameDuration = 0.0f,
 60     .srcWeaponIdleRect = {16, 0, 6, 16},
 61     .srcWeaponIdleOffset = {8, 0},
 62     .srcWeaponCooldownRect = {22, 0, 11, 16},
 63     .srcWeaponCooldownOffset = {10, 0},
 64 };
 65 
 66 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase)
 67 {
 68   float xScale = flip ? -1.0f : 1.0f;
 69   Camera3D camera = currentLevel->camera;
 70   float size = 0.5f;
 71   Vector2 offset = (Vector2){ unit.offset.x / 16.0f * size, unit.offset.y / 16.0f * size * xScale };
 72   Vector2 scale = (Vector2){ unit.srcRect.width / 16.0f * size, unit.srcRect.height / 16.0f * size };
 73   // we want the sprite to face the camera, so we need to calculate the up vector
 74   Vector3 forward = Vector3Subtract(camera.target, camera.position);
 75   Vector3 up = {0, 1, 0};
 76   Vector3 right = Vector3CrossProduct(forward, up);
 77   up = Vector3Normalize(Vector3CrossProduct(right, forward));
 78 
 79   Rectangle srcRect = unit.srcRect;
 80   if (unit.frameCount > 1)
 81   {
 82     srcRect.x += (int)(t / unit.frameDuration) % unit.frameCount * srcRect.width;
 83   }
 84   if (flip)
 85   {
 86     srcRect.x += srcRect.width;
 87     srcRect.width = -srcRect.width;
 88   }
 89   DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
 90 
 91   if (phase == SPRITE_UNIT_PHASE_WEAPON_COOLDOWN && unit.srcWeaponCooldownRect.width > 0)
 92   {
 93     offset = (Vector2){ unit.srcWeaponCooldownOffset.x / 16.0f * size, unit.srcWeaponCooldownOffset.y / 16.0f * size };
 94     scale = (Vector2){ unit.srcWeaponCooldownRect.width / 16.0f * size, unit.srcWeaponCooldownRect.height / 16.0f * size };
 95     srcRect = unit.srcWeaponCooldownRect;
 96     if (flip)
 97     {
 98       // position.x = flip * scale.x * 0.5f;
 99       srcRect.x += srcRect.width;
100       srcRect.width = -srcRect.width;
101       offset.x = scale.x - offset.x;
102     }
103     DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
104   }
105   else if (phase == SPRITE_UNIT_PHASE_WEAPON_IDLE && unit.srcWeaponIdleRect.width > 0)
106   {
107     offset = (Vector2){ unit.srcWeaponIdleOffset.x / 16.0f * size, unit.srcWeaponIdleOffset.y / 16.0f * size };
108     scale = (Vector2){ unit.srcWeaponIdleRect.width / 16.0f * size, unit.srcWeaponIdleRect.height / 16.0f * size };
109     srcRect = unit.srcWeaponIdleRect;
110     if (flip)
111     {
112       // position.x = flip * scale.x * 0.5f;
113       srcRect.x += srcRect.width;
114       srcRect.width = -srcRect.width;
115       offset.x = scale.x - offset.x;
116     }
117     DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
118   }
119 }
120 
121 void TowerInit()
122 {
123   for (int i = 0; i < TOWER_MAX_COUNT; i++)
124   {
125     towers[i] = (Tower){0};
126   }
127   towerCount = 0;
128 
129   towerModels[TOWER_TYPE_BASE] = LoadModel("data/keep.glb");
130   towerModels[TOWER_TYPE_WALL] = LoadModel("data/wall-0000.glb");
131 
132   for (int i = 0; i < TOWER_TYPE_COUNT; i++)
133   {
134     if (towerModels[i].materials)
135     {
136       // assign the palette texture to the material of the model (0 is not used afaik)
137       towerModels[i].materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
138     }
139   }
140 }
141 
142 static void TowerGunUpdate(Tower *tower)
143 {
144   TowerTypeConfig config = towerTypeConfigs[tower->towerType];
145   if (tower->cooldown <= 0.0f)
146   {
147     Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, config.range);
148     if (enemy)
149     {
150       tower->cooldown = config.cooldown;
151       // shoot the enemy; determine future position of the enemy
152       float bulletSpeed = config.projectileSpeed;
153       Vector2 velocity = enemy->simVelocity;
154       Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0);
155       Vector2 towerPosition = {tower->x, tower->y};
156       float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed;
157       for (int i = 0; i < 8; i++) {
158         velocity = enemy->simVelocity;
159         futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0);
160         float distance = Vector2Distance(towerPosition, futurePosition);
161         float eta2 = distance / bulletSpeed;
162         if (fabs(eta - eta2) < 0.01f) {
163           break;
164         }
165         eta = (eta2 + eta) * 0.5f;
166       }
167 
168       ProjectileTryAdd(config.projectileType, enemy, 
169         (Vector3){towerPosition.x, 1.33f, towerPosition.y}, 
170         (Vector3){futurePosition.x, 0.25f, futurePosition.y},
171         bulletSpeed, config.hitEffect);
172       enemy->futureDamage += config.hitEffect.damage;
173       tower->lastTargetPosition = futurePosition;
174     }
175   }
176   else
177   {
178     tower->cooldown -= gameTime.deltaTime;
179   }
180 }
181 
182 Tower *TowerGetAt(int16_t x, int16_t y)
183 {
184   for (int i = 0; i < towerCount; i++)
185   {
186     if (towers[i].x == x && towers[i].y == y && towers[i].towerType != TOWER_TYPE_NONE)
187     {
188       return &towers[i];
189     }
190   }
191   return 0;
192 }
193 
194 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y)
195 {
196   if (towerCount >= TOWER_MAX_COUNT)
197   {
198     return 0;
199   }
200 
201   Tower *tower = TowerGetAt(x, y);
202   if (tower)
203   {
204     return 0;
205   }
206 
207   tower = &towers[towerCount++];
208   tower->x = x;
209   tower->y = y;
210   tower->towerType = towerType;
211   tower->cooldown = 0.0f;
212   tower->damage = 0.0f;
213   return tower;
214 }
215 
216 Tower *GetTowerByType(uint8_t towerType)
217 {
218   for (int i = 0; i < towerCount; i++)
219   {
220     if (towers[i].towerType == towerType)
221     {
222       return &towers[i];
223     }
224   }
225   return 0;
226 }
227 
228 int GetTowerCosts(uint8_t towerType)
229 {
230   return towerTypeConfigs[towerType].cost;
231 }
232 
233 float TowerGetMaxHealth(Tower *tower)
234 {
235   return towerTypeConfigs[tower->towerType].maxHealth;
236 }
237 
238 void TowerDraw()
239 {
240   for (int i = 0; i < towerCount; i++)
241   {
242     Tower tower = towers[i];
243     if (tower.towerType == TOWER_TYPE_NONE)
244     {
245       continue;
246     }
247 
248     switch (tower.towerType)
249     {
250     case TOWER_TYPE_ARCHER:
251       {
252         Vector2 screenPosTower = GetWorldToScreen((Vector3){tower.x, 0.0f, tower.y}, currentLevel->camera);
253         Vector2 screenPosTarget = GetWorldToScreen((Vector3){tower.lastTargetPosition.x, 0.0f, tower.lastTargetPosition.y}, currentLevel->camera);
254         DrawModel(towerModels[TOWER_TYPE_WALL], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
255         DrawSpriteUnit(archerUnit, (Vector3){tower.x, 1.0f, tower.y}, 0, screenPosTarget.x > screenPosTower.x, 
256           tower.cooldown > 0.2f ? SPRITE_UNIT_PHASE_WEAPON_COOLDOWN : SPRITE_UNIT_PHASE_WEAPON_IDLE);
257       }
258       break;
259     case TOWER_TYPE_BALLISTA:
260       DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, BROWN);
261       break;
262     case TOWER_TYPE_CATAPULT:
263       DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, DARKGRAY);
264       break;
265     default:
266       if (towerModels[tower.towerType].materials)
267       {
268         DrawModel(towerModels[tower.towerType], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
269       } else {
270         DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY);
271       }
272       break;
273     }
274   }
275 }
276 
277 void TowerUpdate()
278 {
279   for (int i = 0; i < towerCount; i++)
280   {
281     Tower *tower = &towers[i];
282     switch (tower->towerType)
283     {
284     case TOWER_TYPE_CATAPULT:
285     case TOWER_TYPE_BALLISTA:
286     case TOWER_TYPE_ARCHER:
287       TowerGunUpdate(tower);
288       break;
289     }
290   }
291 }
292 
293 void TowerDrawHealthBars(Camera3D camera)
294 {
295   for (int i = 0; i < towerCount; i++)
296   {
297     Tower *tower = &towers[i];
298     if (tower->towerType == TOWER_TYPE_NONE || tower->damage <= 0.0f)
299     {
300       continue;
301     }
302     
303     Vector3 position = (Vector3){tower->x, 0.5f, tower->y};
304     float maxHealth = TowerGetMaxHealth(tower);
305     float health = maxHealth - tower->damage;
306     float healthRatio = health / maxHealth;
307     
308     DrawHealthBar(camera, position, healthRatio, GREEN, 35.0f);
309   }
310 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 #include <stdlib.h>
  4 #include <math.h>
  5 
  6 EnemyClassConfig enemyClassConfigs[] = {
  7     [ENEMY_TYPE_MINION] = {
  8       .health = 10.0f, 
  9       .speed = 0.6f, 
 10       .radius = 0.25f, 
 11       .maxAcceleration = 1.0f,
 12       .explosionDamage = 1.0f,
 13       .requiredContactTime = 0.5f,
 14       .explosionRange = 1.0f,
 15       .explosionPushbackPower = 0.25f,
 16       .goldValue = 1,
 17     },
 18 };
 19 
 20 Enemy enemies[ENEMY_MAX_COUNT];
 21 int enemyCount = 0;
 22 
 23 SpriteUnit enemySprites[] = {
 24     [ENEMY_TYPE_MINION] = {
 25       .srcRect = {0, 16, 16, 16},
 26       .offset = {8.0f, 0.0f},
 27       .frameCount = 6,
 28       .frameDuration = 0.1f,
 29     },
 30 };
 31 
 32 void EnemyInit()
 33 {
 34   for (int i = 0; i < ENEMY_MAX_COUNT; i++)
 35   {
 36     enemies[i] = (Enemy){0};
 37   }
 38   enemyCount = 0;
 39 }
 40 
 41 float EnemyGetCurrentMaxSpeed(Enemy *enemy)
 42 {
 43   return enemyClassConfigs[enemy->enemyType].speed;
 44 }
 45 
 46 float EnemyGetMaxHealth(Enemy *enemy)
 47 {
 48   return enemyClassConfigs[enemy->enemyType].health;
 49 }
 50 
 51 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY)
 52 {
 53   int16_t castleX = 0;
 54   int16_t castleY = 0;
 55   int16_t dx = castleX - currentX;
 56   int16_t dy = castleY - currentY;
 57   if (dx == 0 && dy == 0)
 58   {
 59     *nextX = currentX;
 60     *nextY = currentY;
 61     return 1;
 62   }
 63   Vector2 gradient = PathFindingGetGradient((Vector3){currentX, 0, currentY});
 64 
 65   if (gradient.x == 0 && gradient.y == 0)
 66   {
 67     *nextX = currentX;
 68     *nextY = currentY;
 69     return 1;
 70   }
 71 
 72   if (fabsf(gradient.x) > fabsf(gradient.y))
 73   {
 74     *nextX = currentX + (int16_t)(gradient.x > 0.0f ? 1 : -1);
 75     *nextY = currentY;
 76     return 0;
 77   }
 78   *nextX = currentX;
 79   *nextY = currentY + (int16_t)(gradient.y > 0.0f ? 1 : -1);
 80   return 0;
 81 }
 82 
 83 
 84 // this function predicts the movement of the unit for the next deltaT seconds
 85 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount)
 86 {
 87   const float pointReachedDistance = 0.25f;
 88   const float pointReachedDistance2 = pointReachedDistance * pointReachedDistance;
 89   const float maxSimStepTime = 0.015625f;
 90   
 91   float maxAcceleration = enemyClassConfigs[enemy->enemyType].maxAcceleration;
 92   float maxSpeed = EnemyGetCurrentMaxSpeed(enemy);
 93   int16_t nextX = enemy->nextX;
 94   int16_t nextY = enemy->nextY;
 95   Vector2 position = enemy->simPosition;
 96   int passedCount = 0;
 97   for (float t = 0.0f; t < deltaT; t += maxSimStepTime)
 98   {
 99     float stepTime = fminf(deltaT - t, maxSimStepTime);
100     Vector2 target = (Vector2){nextX, nextY};
101     float speed = Vector2Length(*velocity);
102     // draw the target position for debugging
103     DrawCubeWires((Vector3){target.x, 0.2f, target.y}, 0.1f, 0.4f, 0.1f, RED);
104     Vector2 lookForwardPos = Vector2Add(position, Vector2Scale(*velocity, speed));
105     if (Vector2DistanceSqr(target, lookForwardPos) <= pointReachedDistance2)
106     {
107       // we reached the target position, let's move to the next waypoint
108       EnemyGetNextPosition(nextX, nextY, &nextX, &nextY);
109       target = (Vector2){nextX, nextY};
110       // track how many waypoints we passed
111       passedCount++;
112     }
113     
114     // acceleration towards the target
115     Vector2 unitDirection = Vector2Normalize(Vector2Subtract(target, lookForwardPos));
116     Vector2 acceleration = Vector2Scale(unitDirection, maxAcceleration * stepTime);
117     *velocity = Vector2Add(*velocity, acceleration);
118 
119     // limit the speed to the maximum speed
120     if (speed > maxSpeed)
121     {
122       *velocity = Vector2Scale(*velocity, maxSpeed / speed);
123     }
124 
125     // move the enemy
126     position = Vector2Add(position, Vector2Scale(*velocity, stepTime));
127   }
128 
129   if (waypointPassedCount)
130   {
131     (*waypointPassedCount) = passedCount;
132   }
133 
134   return position;
135 }
136 
137 void EnemyDraw()
138 {
139   for (int i = 0; i < enemyCount; i++)
140   {
141     Enemy enemy = enemies[i];
142     if (enemy.enemyType == ENEMY_TYPE_NONE)
143     {
144       continue;
145     }
146 
147     Vector2 position = EnemyGetPosition(&enemy, gameTime.time - enemy.startMovingTime, &enemy.simVelocity, 0);
148     
149     // don't draw any trails for now; might replace this with footprints later
150     // if (enemy.movePathCount > 0)
151     // {
152     //   Vector3 p = {enemy.movePath[0].x, 0.2f, enemy.movePath[0].y};
153     //   DrawLine3D(p, (Vector3){position.x, 0.2f, position.y}, GREEN);
154     // }
155     // for (int j = 1; j < enemy.movePathCount; j++)
156     // {
157     //   Vector3 p = {enemy.movePath[j - 1].x, 0.2f, enemy.movePath[j - 1].y};
158     //   Vector3 q = {enemy.movePath[j].x, 0.2f, enemy.movePath[j].y};
159     //   DrawLine3D(p, q, GREEN);
160     // }
161 
162     switch (enemy.enemyType)
163     {
164     case ENEMY_TYPE_MINION:
165       DrawSpriteUnit(enemySprites[ENEMY_TYPE_MINION], (Vector3){position.x, 0.0f, position.y}, 
166         enemy.walkedDistance, 0, 0);
167       break;
168     }
169   }
170 }
171 
172 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource)
173 {
174   // damage the tower
175   float explosionDamge = enemyClassConfigs[enemy->enemyType].explosionDamage;
176   float explosionRange = enemyClassConfigs[enemy->enemyType].explosionRange;
177   float explosionPushbackPower = enemyClassConfigs[enemy->enemyType].explosionPushbackPower;
178   float explosionRange2 = explosionRange * explosionRange;
179   tower->damage += enemyClassConfigs[enemy->enemyType].explosionDamage;
180   // explode the enemy
181   if (tower->damage >= TowerGetMaxHealth(tower))
182   {
183     tower->towerType = TOWER_TYPE_NONE;
184   }
185 
186   ParticleAdd(PARTICLE_TYPE_EXPLOSION, 
187     explosionSource, 
188     (Vector3){0, 0.1f, 0}, (Vector3){1.0f, 1.0f, 1.0f}, 1.0f);
189 
190   enemy->enemyType = ENEMY_TYPE_NONE;
191 
192   // push back enemies & dealing damage
193   for (int i = 0; i < enemyCount; i++)
194   {
195     Enemy *other = &enemies[i];
196     if (other->enemyType == ENEMY_TYPE_NONE)
197     {
198       continue;
199     }
200     float distanceSqr = Vector2DistanceSqr(enemy->simPosition, other->simPosition);
201     if (distanceSqr > 0 && distanceSqr < explosionRange2)
202     {
203       Vector2 direction = Vector2Normalize(Vector2Subtract(other->simPosition, enemy->simPosition));
204       other->simPosition = Vector2Add(other->simPosition, Vector2Scale(direction, explosionPushbackPower));
205       EnemyAddDamage(other, explosionDamge);
206     }
207   }
208 }
209 
210 void EnemyUpdate()
211 {
212   const float castleX = 0;
213   const float castleY = 0;
214   const float maxPathDistance2 = 0.25f * 0.25f;
215   
216   for (int i = 0; i < enemyCount; i++)
217   {
218     Enemy *enemy = &enemies[i];
219     if (enemy->enemyType == ENEMY_TYPE_NONE)
220     {
221       continue;
222     }
223 
224     int waypointPassedCount = 0;
225     Vector2 prevPosition = enemy->simPosition;
226     enemy->simPosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &enemy->simVelocity, &waypointPassedCount);
227     enemy->startMovingTime = gameTime.time;
228     enemy->walkedDistance += Vector2Distance(prevPosition, enemy->simPosition);
229     // track path of unit
230     if (enemy->movePathCount == 0 || Vector2DistanceSqr(enemy->simPosition, enemy->movePath[0]) > maxPathDistance2)
231     {
232       for (int j = ENEMY_MAX_PATH_COUNT - 1; j > 0; j--)
233       {
234         enemy->movePath[j] = enemy->movePath[j - 1];
235       }
236       enemy->movePath[0] = enemy->simPosition;
237       if (++enemy->movePathCount > ENEMY_MAX_PATH_COUNT)
238       {
239         enemy->movePathCount = ENEMY_MAX_PATH_COUNT;
240       }
241     }
242 
243     if (waypointPassedCount > 0)
244     {
245       enemy->currentX = enemy->nextX;
246       enemy->currentY = enemy->nextY;
247       if (EnemyGetNextPosition(enemy->currentX, enemy->currentY, &enemy->nextX, &enemy->nextY) &&
248         Vector2DistanceSqr(enemy->simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
249       {
250         // enemy reached the castle; remove it
251         enemy->enemyType = ENEMY_TYPE_NONE;
252         continue;
253       }
254     }
255   }
256 
257   // handle collisions between enemies
258   for (int i = 0; i < enemyCount - 1; i++)
259   {
260     Enemy *enemyA = &enemies[i];
261     if (enemyA->enemyType == ENEMY_TYPE_NONE)
262     {
263       continue;
264     }
265     for (int j = i + 1; j < enemyCount; j++)
266     {
267       Enemy *enemyB = &enemies[j];
268       if (enemyB->enemyType == ENEMY_TYPE_NONE)
269       {
270         continue;
271       }
272       float distanceSqr = Vector2DistanceSqr(enemyA->simPosition, enemyB->simPosition);
273       float radiusA = enemyClassConfigs[enemyA->enemyType].radius;
274       float radiusB = enemyClassConfigs[enemyB->enemyType].radius;
275       float radiusSum = radiusA + radiusB;
276       if (distanceSqr < radiusSum * radiusSum && distanceSqr > 0.001f)
277       {
278         // collision
279         float distance = sqrtf(distanceSqr);
280         float overlap = radiusSum - distance;
281         // move the enemies apart, but softly; if we have a clog of enemies,
282         // moving them perfectly apart can cause them to jitter
283         float positionCorrection = overlap / 5.0f;
284         Vector2 direction = (Vector2){
285             (enemyB->simPosition.x - enemyA->simPosition.x) / distance * positionCorrection,
286             (enemyB->simPosition.y - enemyA->simPosition.y) / distance * positionCorrection};
287         enemyA->simPosition = Vector2Subtract(enemyA->simPosition, direction);
288         enemyB->simPosition = Vector2Add(enemyB->simPosition, direction);
289       }
290     }
291   }
292 
293   // handle collisions between enemies and towers
294   for (int i = 0; i < enemyCount; i++)
295   {
296     Enemy *enemy = &enemies[i];
297     if (enemy->enemyType == ENEMY_TYPE_NONE)
298     {
299       continue;
300     }
301     enemy->contactTime -= gameTime.deltaTime;
302     if (enemy->contactTime < 0.0f)
303     {
304       enemy->contactTime = 0.0f;
305     }
306 
307     float enemyRadius = enemyClassConfigs[enemy->enemyType].radius;
308     // linear search over towers; could be optimized by using path finding tower map,
309     // but for now, we keep it simple
310     for (int j = 0; j < towerCount; j++)
311     {
312       Tower *tower = &towers[j];
313       if (tower->towerType == TOWER_TYPE_NONE)
314       {
315         continue;
316       }
317       float distanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){tower->x, tower->y});
318       float combinedRadius = enemyRadius + 0.708; // sqrt(0.5^2 + 0.5^2), corner-center distance of square with side length 1
319       if (distanceSqr > combinedRadius * combinedRadius)
320       {
321         continue;
322       }
323       // potential collision; square / circle intersection
324       float dx = tower->x - enemy->simPosition.x;
325       float dy = tower->y - enemy->simPosition.y;
326       float absDx = fabsf(dx);
327       float absDy = fabsf(dy);
328       Vector3 contactPoint = {0};
329       if (absDx <= 0.5f && absDx <= absDy) {
330         // vertical collision; push the enemy out horizontally
331         float overlap = enemyRadius + 0.5f - absDy;
332         if (overlap < 0.0f)
333         {
334           continue;
335         }
336         float direction = dy > 0.0f ? -1.0f : 1.0f;
337         enemy->simPosition.y += direction * overlap;
338         contactPoint = (Vector3){enemy->simPosition.x, 0.2f, tower->y + direction * 0.5f};
339       }
340       else if (absDy <= 0.5f && absDy <= absDx)
341       {
342         // horizontal collision; push the enemy out vertically
343         float overlap = enemyRadius + 0.5f - absDx;
344         if (overlap < 0.0f)
345         {
346           continue;
347         }
348         float direction = dx > 0.0f ? -1.0f : 1.0f;
349         enemy->simPosition.x += direction * overlap;
350         contactPoint = (Vector3){tower->x + direction * 0.5f, 0.2f, enemy->simPosition.y};
351       }
352       else
353       {
354         // possible collision with a corner
355         float cornerDX = dx > 0.0f ? -0.5f : 0.5f;
356         float cornerDY = dy > 0.0f ? -0.5f : 0.5f;
357         float cornerX = tower->x + cornerDX;
358         float cornerY = tower->y + cornerDY;
359         float cornerDistanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){cornerX, cornerY});
360         if (cornerDistanceSqr > enemyRadius * enemyRadius)
361         {
362           continue;
363         }
364         // push the enemy out along the diagonal
365         float cornerDistance = sqrtf(cornerDistanceSqr);
366         float overlap = enemyRadius - cornerDistance;
367         float directionX = cornerDistance > 0.0f ? (cornerX - enemy->simPosition.x) / cornerDistance : -cornerDX;
368         float directionY = cornerDistance > 0.0f ? (cornerY - enemy->simPosition.y) / cornerDistance : -cornerDY;
369         enemy->simPosition.x -= directionX * overlap;
370         enemy->simPosition.y -= directionY * overlap;
371         contactPoint = (Vector3){cornerX, 0.2f, cornerY};
372       }
373 
374       if (enemyClassConfigs[enemy->enemyType].explosionDamage > 0.0f)
375       {
376         enemy->contactTime += gameTime.deltaTime * 2.0f; // * 2 to undo the subtraction above
377         if (enemy->contactTime >= enemyClassConfigs[enemy->enemyType].requiredContactTime)
378         {
379           EnemyTriggerExplode(enemy, tower, contactPoint);
380         }
381       }
382     }
383   }
384 }
385 
386 EnemyId EnemyGetId(Enemy *enemy)
387 {
388   return (EnemyId){enemy - enemies, enemy->generation};
389 }
390 
391 Enemy *EnemyTryResolve(EnemyId enemyId)
392 {
393   if (enemyId.index >= ENEMY_MAX_COUNT)
394   {
395     return 0;
396   }
397   Enemy *enemy = &enemies[enemyId.index];
398   if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE)
399   {
400     return 0;
401   }
402   return enemy;
403 }
404 
405 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY)
406 {
407   Enemy *spawn = 0;
408   for (int i = 0; i < enemyCount; i++)
409   {
410     Enemy *enemy = &enemies[i];
411     if (enemy->enemyType == ENEMY_TYPE_NONE)
412     {
413       spawn = enemy;
414       break;
415     }
416   }
417 
418   if (enemyCount < ENEMY_MAX_COUNT && !spawn)
419   {
420     spawn = &enemies[enemyCount++];
421   }
422 
423   if (spawn)
424   {
425     spawn->currentX = currentX;
426     spawn->currentY = currentY;
427     spawn->nextX = currentX;
428     spawn->nextY = currentY;
429     spawn->simPosition = (Vector2){currentX, currentY};
430     spawn->simVelocity = (Vector2){0, 0};
431     spawn->enemyType = enemyType;
432     spawn->startMovingTime = gameTime.time;
433     spawn->damage = 0.0f;
434     spawn->futureDamage = 0.0f;
435     spawn->generation++;
436     spawn->movePathCount = 0;
437     spawn->walkedDistance = 0.0f;
438   }
439 
440   return spawn;
441 }
442 
443 int EnemyAddDamageRange(Vector2 position, float range, float damage)
444 {
445   int count = 0;
446   float range2 = range * range;
447   for (int i = 0; i < enemyCount; i++)
448   {
449     Enemy *enemy = &enemies[i];
450     if (enemy->enemyType == ENEMY_TYPE_NONE)
451     {
452       continue;
453     }
454     float distance2 = Vector2DistanceSqr(position, enemy->simPosition);
455     if (distance2 <= range2)
456     {
457       EnemyAddDamage(enemy, damage);
458       count++;
459     }
460   }
461   return count;
462 }
463 
464 int EnemyAddDamage(Enemy *enemy, float damage)
465 {
466   enemy->damage += damage;
467   if (enemy->damage >= EnemyGetMaxHealth(enemy))
468   {
469     currentLevel->playerGold += enemyClassConfigs[enemy->enemyType].goldValue;
470     enemy->enemyType = ENEMY_TYPE_NONE;
471     return 1;
472   }
473 
474   return 0;
475 }
476 
477 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
478 {
479   int16_t castleX = 0;
480   int16_t castleY = 0;
481   Enemy* closest = 0;
482   int16_t closestDistance = 0;
483   float range2 = range * range;
484   for (int i = 0; i < enemyCount; i++)
485   {
486     Enemy* enemy = &enemies[i];
487     if (enemy->enemyType == ENEMY_TYPE_NONE)
488     {
489       continue;
490     }
491     float maxHealth = EnemyGetMaxHealth(enemy);
492     if (enemy->futureDamage >= maxHealth)
493     {
494       // ignore enemies that will die soon
495       continue;
496     }
497     int16_t dx = castleX - enemy->currentX;
498     int16_t dy = castleY - enemy->currentY;
499     int16_t distance = abs(dx) + abs(dy);
500     if (!closest || distance < closestDistance)
501     {
502       float tdx = towerX - enemy->currentX;
503       float tdy = towerY - enemy->currentY;
504       float tdistance2 = tdx * tdx + tdy * tdy;
505       if (tdistance2 <= range2)
506       {
507         closest = enemy;
508         closestDistance = distance;
509       }
510     }
511   }
512   return closest;
513 }
514 
515 int EnemyCount()
516 {
517   int count = 0;
518   for (int i = 0; i < enemyCount; i++)
519   {
520     if (enemies[i].enemyType != ENEMY_TYPE_NONE)
521     {
522       count++;
523     }
524   }
525   return count;
526 }
527 
528 void EnemyDrawHealthbars(Camera3D camera)
529 {
530   for (int i = 0; i < enemyCount; i++)
531   {
532     Enemy *enemy = &enemies[i];
533     if (enemy->enemyType == ENEMY_TYPE_NONE || enemy->damage == 0.0f)
534     {
535       continue;
536     }
537     Vector3 position = (Vector3){enemy->simPosition.x, 0.5f, enemy->simPosition.y};
538     float maxHealth = EnemyGetMaxHealth(enemy);
539     float health = maxHealth - enemy->damage;
540     float healthRatio = health / maxHealth;
541     
542     DrawHealthBar(camera, position, healthRatio, GREEN, 15.0f);
543   }
544 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 
  4 static Projectile projectiles[PROJECTILE_MAX_COUNT];
  5 static int projectileCount = 0;
  6 
  7 typedef struct ProjectileConfig
  8 {
  9   float arcFactor;
 10   Color color;
 11   Color trailColor;
 12 } ProjectileConfig;
 13 
 14 ProjectileConfig projectileConfigs[] = {
 15     [PROJECTILE_TYPE_ARROW] = {
 16         .arcFactor = 0.15f,
 17         .color = RED,
 18         .trailColor = BROWN,
 19     },
 20     [PROJECTILE_TYPE_CATAPULT] = {
 21         .arcFactor = 0.5f,
 22         .color = RED,
 23         .trailColor = GRAY,
 24     },
 25     [PROJECTILE_TYPE_BALLISTA] = {
 26         .arcFactor = 0.025f,
 27         .color = RED,
 28         .trailColor = BROWN,
 29     },
 30 };
 31 
 32 void ProjectileInit()
 33 {
 34   for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
 35   {
 36     projectiles[i] = (Projectile){0};
 37   }
 38 }
 39 
 40 void ProjectileDraw()
 41 {
 42   for (int i = 0; i < projectileCount; i++)
 43   {
 44     Projectile projectile = projectiles[i];
 45     if (projectile.projectileType == PROJECTILE_TYPE_NONE)
 46     {
 47       continue;
 48     }
 49     float transition = (gameTime.time - projectile.shootTime) / (projectile.arrivalTime - projectile.shootTime);
 50     if (transition >= 1.0f)
 51     {
 52       continue;
 53     }
 54 
 55     ProjectileConfig config = projectileConfigs[projectile.projectileType];
 56     for (float transitionOffset = 0.0f; transitionOffset < 1.0f; transitionOffset += 0.1f)
 57     {
 58       float t = transition + transitionOffset * 0.3f;
 59       if (t > 1.0f)
 60       {
 61         break;
 62       }
 63       Vector3 position = Vector3Lerp(projectile.position, projectile.target, t);
 64       Color color = config.color;
 65       color = ColorLerp(config.trailColor, config.color, transitionOffset * transitionOffset);
 66       // fake a ballista flight path using parabola equation
 67       float parabolaT = t - 0.5f;
 68       parabolaT = 1.0f - 4.0f * parabolaT * parabolaT;
 69       position.y += config.arcFactor * parabolaT * projectile.distance;
 70       
 71       float size = 0.06f * (transitionOffset + 0.25f);
 72       DrawCube(position, size, size, size, color);
 73     }
 74   }
 75 }
 76 
 77 void ProjectileUpdate()
 78 {
 79   for (int i = 0; i < projectileCount; i++)
 80   {
 81     Projectile *projectile = &projectiles[i];
 82     if (projectile->projectileType == PROJECTILE_TYPE_NONE)
 83     {
 84       continue;
 85     }
 86     float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime);
 87     if (transition >= 1.0f)
 88     {
 89       projectile->projectileType = PROJECTILE_TYPE_NONE;
 90       Enemy *enemy = EnemyTryResolve(projectile->targetEnemy);
 91       if (enemy && projectile->hitEffectConfig.pushbackPowerDistance > 0.0f)
 92       {
 93           Vector2 direction = Vector2Normalize(Vector2Subtract((Vector2){projectile->target.x, projectile->target.z}, enemy->simPosition));
 94           enemy->simPosition = Vector2Add(enemy->simPosition, Vector2Scale(direction, projectile->hitEffectConfig.pushbackPowerDistance));
 95       }
 96       
 97       if (projectile->hitEffectConfig.areaDamageRadius > 0.0f)
 98       {
 99         EnemyAddDamageRange((Vector2){projectile->target.x, projectile->target.z}, projectile->hitEffectConfig.areaDamageRadius, projectile->hitEffectConfig.damage);
100         // pancaked sphere explosion
101         float r = projectile->hitEffectConfig.areaDamageRadius;
102         ParticleAdd(PARTICLE_TYPE_EXPLOSION, projectile->target, (Vector3){0}, (Vector3){r, r * 0.2f, r}, 0.33f);
103       }
104       else if (projectile->hitEffectConfig.damage > 0.0f && enemy)
105       {
106         EnemyAddDamage(enemy, projectile->hitEffectConfig.damage);
107       }
108       continue;
109     }
110   }
111 }
112 
113 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig)
114 {
115   for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
116   {
117     Projectile *projectile = &projectiles[i];
118     if (projectile->projectileType == PROJECTILE_TYPE_NONE)
119     {
120       projectile->projectileType = projectileType;
121       projectile->shootTime = gameTime.time;
122       float distance = Vector3Distance(position, target);
123       projectile->arrivalTime = gameTime.time + distance / speed;
124       projectile->position = position;
125       projectile->target = target;
126       projectile->directionNormal = Vector3Scale(Vector3Subtract(target, position), 1.0f / distance);
127       projectile->distance = distance;
128       projectile->targetEnemy = EnemyGetId(enemy);
129       projectileCount = projectileCount <= i ? i + 1 : projectileCount;
130       projectile->hitEffectConfig = hitEffectConfig;
131       return projectile;
132     }
133   }
134   return 0;
135 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 #include <rlgl.h>
  4 
  5 static Particle particles[PARTICLE_MAX_COUNT];
  6 static int particleCount = 0;
  7 
  8 void ParticleInit()
  9 {
 10   for (int i = 0; i < PARTICLE_MAX_COUNT; i++)
 11   {
 12     particles[i] = (Particle){0};
 13   }
 14   particleCount = 0;
 15 }
 16 
 17 static void DrawExplosionParticle(Particle *particle, float transition)
 18 {
 19   Vector3 scale = particle->scale;
 20   float size = 1.0f * (1.0f - transition);
 21   Color startColor = WHITE;
 22   Color endColor = RED;
 23   Color color = ColorLerp(startColor, endColor, transition);
 24 
 25   rlPushMatrix();
 26   rlTranslatef(particle->position.x, particle->position.y, particle->position.z);
 27   rlScalef(scale.x, scale.y, scale.z);
 28   DrawSphere(Vector3Zero(), size, color);
 29   rlPopMatrix();
 30 }
 31 
 32 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime)
 33 {
 34   if (particleCount >= PARTICLE_MAX_COUNT)
 35   {
 36     return;
 37   }
 38 
 39   int index = -1;
 40   for (int i = 0; i < particleCount; i++)
 41   {
 42     if (particles[i].particleType == PARTICLE_TYPE_NONE)
 43     {
 44       index = i;
 45       break;
 46     }
 47   }
 48 
 49   if (index == -1)
 50   {
 51     index = particleCount++;
 52   }
 53 
 54   Particle *particle = &particles[index];
 55   particle->particleType = particleType;
 56   particle->spawnTime = gameTime.time;
 57   particle->lifetime = lifetime;
 58   particle->position = position;
 59   particle->velocity = velocity;
 60   particle->scale = scale;
 61 }
 62 
 63 void ParticleUpdate()
 64 {
 65   for (int i = 0; i < particleCount; i++)
 66   {
 67     Particle *particle = &particles[i];
 68     if (particle->particleType == PARTICLE_TYPE_NONE)
 69     {
 70       continue;
 71     }
 72 
 73     float age = gameTime.time - particle->spawnTime;
 74 
 75     if (particle->lifetime > age)
 76     {
 77       particle->position = Vector3Add(particle->position, Vector3Scale(particle->velocity, gameTime.deltaTime));
 78     }
 79     else {
 80       particle->particleType = PARTICLE_TYPE_NONE;
 81     }
 82   }
 83 }
 84 
 85 void ParticleDraw()
 86 {
 87   for (int i = 0; i < particleCount; i++)
 88   {
 89     Particle particle = particles[i];
 90     if (particle.particleType == PARTICLE_TYPE_NONE)
 91     {
 92       continue;
 93     }
 94 
 95     float age = gameTime.time - particle.spawnTime;
 96     float transition = age / particle.lifetime;
 97     switch (particle.particleType)
 98     {
 99     case PARTICLE_TYPE_EXPLOSION:
100       DrawExplosionParticle(&particle, transition);
101       break;
102     default:
103       DrawCube(particle.position, 0.3f, 0.5f, 0.3f, RED);
104       break;
105     }
106   }
107 }
  1 #include "raylib.h"
  2 #include "preferred_size.h"
  3 
  4 // Since the canvas size is not known at compile time, we need to query it at runtime;
  5 // the following platform specific code obtains the canvas size and we will use this
  6 // size as the preferred size for the window at init time. We're ignoring here the
  7 // possibility of the canvas size changing during runtime - this would require to
  8 // poll the canvas size in the game loop or establishing a callback to be notified
  9 
 10 #ifdef PLATFORM_WEB
 11 #include <emscripten.h>
 12 EMSCRIPTEN_RESULT emscripten_get_element_css_size(const char *target, double *width, double *height);
 13 
 14 void GetPreferredSize(int *screenWidth, int *screenHeight)
 15 {
 16   double canvasWidth, canvasHeight;
 17   emscripten_get_element_css_size("#" CANVAS_NAME, &canvasWidth, &canvasHeight);
 18   *screenWidth = (int)canvasWidth;
 19   *screenHeight = (int)canvasHeight;
 20   TraceLog(LOG_INFO, "preferred size for %s: %d %d", CANVAS_NAME, *screenWidth, *screenHeight);
 21 }
 22 
 23 int IsPaused()
 24 {
 25   const char *js = "(function(){\n"
 26   "  var canvas = document.getElementById(\"" CANVAS_NAME "\");\n"
 27   "  var rect = canvas.getBoundingClientRect();\n"
 28   "  var isVisible = (\n"
 29   "    rect.top >= 0 &&\n"
 30   "    rect.left >= 0 &&\n"
 31   "    rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&\n"
 32   "    rect.right <= (window.innerWidth || document.documentElement.clientWidth)\n"
 33   "  );\n"
 34   "  return isVisible ? 0 : 1;\n"
 35   "})()";
 36   return emscripten_run_script_int(js);
 37 }
 38 
 39 #else
 40 void GetPreferredSize(int *screenWidth, int *screenHeight)
 41 {
 42   *screenWidth = 600;
 43   *screenHeight = 240;
 44 }
 45 int IsPaused()
 46 {
 47   return 0;
 48 }
 49 #endif
  1 #ifndef PREFERRED_SIZE_H
  2 #define PREFERRED_SIZE_H
  3 
  4 void GetPreferredSize(int *screenWidth, int *screenHeight);
  5 int IsPaused();
  6 
  7 #endif
When previewing a new tower, the path is updated to show the new path the enemies will take.
Blocked cells when hovering over a tower
The cells are blocked when hovering over a tower. The path is updated to reflect the new path the enemies will take.

The path preview is now reacting to the position of the mouse cursor. The path preview is however only for debugging. Let's add some graphics for the path. There are again multiple ways to do this. I first considered creating 4 models of arrow lines and rendering them in each cell, according to the direction the enemy will move to. But one factor to consider is to communicate the direction of movement, which I found a bit difficult to achieve with this approach.

My second idea is to draw smaller arrows on the path the enemies will take, using the same function as enemies use to move. This way, we can reuse the code and there is only a single arrow model. Drawing them with small distances while animating them in a forward movement fashion should give a good impression of the path the enemies will take.

  • 💾
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 #include <stdlib.h>
  4 #include <math.h>
  5 
  6 //# Variables
  7 GUIState guiState = {0};
  8 GameTime gameTime = {0};
  9 
 10 Model floorTileAModel = {0};
 11 Model floorTileBModel = {0};
 12 Model treeModel[2] = {0};
 13 Model firTreeModel[2] = {0};
 14 Model rockModels[5] = {0};
 15 Model grassPatchModel[1] = {0};
 16 
17 Model pathArrowModel = {0}; 18
19 Texture2D palette, spriteSheet; 20 21 Level levels[] = { 22 [0] = { 23 .state = LEVEL_STATE_BUILDING, 24 .initialGold = 20, 25 .waves[0] = { 26 .enemyType = ENEMY_TYPE_MINION, 27 .wave = 0, 28 .count = 10, 29 .interval = 2.5f, 30 .delay = 1.0f, 31 .spawnPosition = {0, 6}, 32 }, 33 .waves[1] = { 34 .enemyType = ENEMY_TYPE_MINION, 35 .wave = 1, 36 .count = 20, 37 .interval = 1.5f, 38 .delay = 1.0f, 39 .spawnPosition = {0, 6}, 40 }, 41 .waves[2] = { 42 .enemyType = ENEMY_TYPE_MINION, 43 .wave = 2, 44 .count = 30, 45 .interval = 1.2f, 46 .delay = 1.0f, 47 .spawnPosition = {0, 6}, 48 } 49 }, 50 }; 51 52 Level *currentLevel = levels; 53 54 //# Game 55 56 static Model LoadGLBModel(char *filename) 57 { 58 Model model = LoadModel(TextFormat("data/%s.glb",filename));
59 for (int i = 0; i < model.materialCount; i++)
60 {
61 model.materials[i].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
62 } 63 return model; 64 } 65 66 void LoadAssets() 67 { 68 // load a sprite sheet that contains all units 69 spriteSheet = LoadTexture("data/spritesheet.png"); 70 SetTextureFilter(spriteSheet, TEXTURE_FILTER_BILINEAR); 71 72 // we'll use a palette texture to colorize the all buildings and environment art 73 palette = LoadTexture("data/palette.png"); 74 // The texture uses gradients on very small space, so we'll enable bilinear filtering 75 SetTextureFilter(palette, TEXTURE_FILTER_BILINEAR); 76 77 floorTileAModel = LoadGLBModel("floor-tile-a"); 78 floorTileBModel = LoadGLBModel("floor-tile-b"); 79 treeModel[0] = LoadGLBModel("leaftree-large-1-a"); 80 treeModel[1] = LoadGLBModel("leaftree-large-1-b"); 81 firTreeModel[0] = LoadGLBModel("firtree-1-a"); 82 firTreeModel[1] = LoadGLBModel("firtree-1-b"); 83 rockModels[0] = LoadGLBModel("rock-1"); 84 rockModels[1] = LoadGLBModel("rock-2"); 85 rockModels[2] = LoadGLBModel("rock-3"); 86 rockModels[3] = LoadGLBModel("rock-4"); 87 rockModels[4] = LoadGLBModel("rock-5");
88 grassPatchModel[0] = LoadGLBModel("grass-patch-1"); 89 90 pathArrowModel = LoadGLBModel("direction-arrow-x");
91 } 92 93 void InitLevel(Level *level) 94 { 95 level->seed = (int)(GetTime() * 100.0f); 96 97 TowerInit(); 98 EnemyInit(); 99 ProjectileInit(); 100 ParticleInit(); 101 TowerTryAdd(TOWER_TYPE_BASE, 0, 0); 102 103 level->placementMode = 0; 104 level->state = LEVEL_STATE_BUILDING; 105 level->nextState = LEVEL_STATE_NONE; 106 level->playerGold = level->initialGold; 107 level->currentWave = 0; 108 109 Camera *camera = &level->camera; 110 camera->position = (Vector3){4.0f, 8.0f, 8.0f}; 111 camera->target = (Vector3){0.0f, 0.0f, 0.0f}; 112 camera->up = (Vector3){0.0f, 1.0f, 0.0f}; 113 camera->fovy = 10.0f; 114 camera->projection = CAMERA_ORTHOGRAPHIC; 115 } 116 117 void DrawLevelHud(Level *level) 118 { 119 const char *text = TextFormat("Gold: %d", level->playerGold); 120 Font font = GetFontDefault(); 121 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 120, 10}, font.baseSize * 2.0f, 2.0f, BLACK); 122 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 122, 8}, font.baseSize * 2.0f, 2.0f, YELLOW); 123 } 124 125 void DrawLevelReportLostWave(Level *level) 126 { 127 BeginMode3D(level->camera); 128 DrawLevelGround(level); 129 TowerDraw(); 130 EnemyDraw(); 131 ProjectileDraw(); 132 ParticleDraw(); 133 guiState.isBlocked = 0; 134 EndMode3D(); 135 136 TowerDrawHealthBars(level->camera); 137 138 const char *text = "Wave lost"; 139 int textWidth = MeasureText(text, 20); 140 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 141 142 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 143 { 144 level->nextState = LEVEL_STATE_RESET; 145 } 146 } 147 148 int HasLevelNextWave(Level *level) 149 { 150 for (int i = 0; i < 10; i++) 151 { 152 EnemyWave *wave = &level->waves[i]; 153 if (wave->wave == level->currentWave) 154 { 155 return 1; 156 } 157 } 158 return 0; 159 } 160 161 void DrawLevelReportWonWave(Level *level) 162 { 163 BeginMode3D(level->camera); 164 DrawLevelGround(level); 165 TowerDraw(); 166 EnemyDraw(); 167 ProjectileDraw(); 168 ParticleDraw(); 169 guiState.isBlocked = 0; 170 EndMode3D(); 171 172 TowerDrawHealthBars(level->camera); 173 174 const char *text = "Wave won"; 175 int textWidth = MeasureText(text, 20); 176 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 177 178 179 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 180 { 181 level->nextState = LEVEL_STATE_RESET; 182 } 183 184 if (HasLevelNextWave(level)) 185 { 186 if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0)) 187 { 188 level->nextState = LEVEL_STATE_BUILDING; 189 } 190 } 191 else { 192 if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0)) 193 { 194 level->nextState = LEVEL_STATE_WON_LEVEL; 195 } 196 } 197 } 198 199 void DrawBuildingBuildButton(Level *level, int x, int y, int width, int height, uint8_t towerType, const char *name) 200 { 201 static ButtonState buttonStates[8] = {0}; 202 int cost = GetTowerCosts(towerType); 203 const char *text = TextFormat("%s: %d", name, cost); 204 buttonStates[towerType].isSelected = level->placementMode == towerType; 205 buttonStates[towerType].isDisabled = level->playerGold < cost; 206 if (Button(text, x, y, width, height, &buttonStates[towerType])) 207 { 208 level->placementMode = buttonStates[towerType].isSelected ? 0 : towerType; 209 } 210 } 211 212 float GetRandomFloat(float min, float max) 213 { 214 int random = GetRandomValue(0, 0xfffffff); 215 return ((float)random / (float)0xfffffff) * (max - min) + min; 216 } 217 218 void DrawLevelGround(Level *level) 219 { 220 // draw checkerboard ground pattern 221 for (int x = -5; x <= 5; x += 1) 222 { 223 for (int y = -5; y <= 5; y += 1) 224 { 225 Model *model = (x + y) % 2 == 0 ? &floorTileAModel : &floorTileBModel; 226 DrawModel(*model, (Vector3){x, 0.0f, y}, 1.0f, WHITE); 227 } 228 } 229 230 int oldSeed = GetRandomValue(0, 0xfffffff); 231 SetRandomSeed(level->seed); 232 // increase probability for trees via duplicated entries 233 Model borderModels[64]; 234 int maxRockCount = GetRandomValue(2, 6); 235 int maxTreeCount = GetRandomValue(10, 20); 236 int maxFirTreeCount = GetRandomValue(5, 10); 237 int maxLeafTreeCount = maxTreeCount - maxFirTreeCount; 238 int grassPatchCount = GetRandomValue(5, 30); 239 240 int modelCount = 0; 241 for (int i = 0; i < maxRockCount && modelCount < 63; i++) 242 { 243 borderModels[modelCount++] = rockModels[GetRandomValue(0, 5)]; 244 } 245 for (int i = 0; i < maxLeafTreeCount && modelCount < 63; i++) 246 { 247 borderModels[modelCount++] = treeModel[GetRandomValue(0, 1)]; 248 } 249 for (int i = 0; i < maxFirTreeCount && modelCount < 63; i++) 250 { 251 borderModels[modelCount++] = firTreeModel[GetRandomValue(0, 1)]; 252 } 253 for (int i = 0; i < grassPatchCount && modelCount < 63; i++) 254 { 255 borderModels[modelCount++] = grassPatchModel[0]; 256 } 257 258 // draw some objects around the border of the map 259 Vector3 up = {0, 1, 0}; 260 // a pseudo random number generator to get the same result every time 261 const float wiggle = 0.75f; 262 const int layerCount = 3; 263 for (int layer = 0; layer < layerCount; layer++) 264 { 265 int layerPos = 6 + layer; 266 for (int x = -6 + layer; x <= 6 + layer; x += 1) 267 { 268 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)], 269 (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, -layerPos + GetRandomFloat(0.0f, wiggle)}, 270 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE); 271 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)], 272 (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, layerPos + GetRandomFloat(0.0f, wiggle)}, 273 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE); 274 } 275 276 for (int z = -5 + layer; z <= 5 + layer; z += 1) 277 { 278 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)], 279 (Vector3){-layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)}, 280 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE); 281 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)], 282 (Vector3){layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)}, 283 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE); 284 } 285 } 286 287 SetRandomSeed(oldSeed); 288 } 289 290 void DrawEnemyPath(Level *level)
291 { 292 const int castleX = 0, castleY = 0; 293 const int maxWaypointCount = 200; 294 const float timeStep = 1.0f; 295 Vector3 arrowScale = {0.75f, 0.75f, 0.75f}; 296 297 // we start with a time offset to simulate the path, 298 // this way the arrows are animated in a forward moving direction 299 // The time is wrapped around the time step to get a smooth animation 300 float timeOffset = fmodf(GetTime(), timeStep); 301
302 for (int i = 0; i < ENEMY_MAX_WAVE_COUNT; i++) 303 { 304 EnemyWave *wave = &level->waves[i]; 305 if (wave->wave != level->currentWave) 306 { 307 continue; 308 }
309 310 // use this dummy enemy to simulate the path 311 Enemy dummy = { 312 .enemyType = ENEMY_TYPE_MINION, 313 .simPosition = (Vector2){wave->spawnPosition.x, wave->spawnPosition.y}, 314 .nextX = wave->spawnPosition.x, 315 .nextY = wave->spawnPosition.y, 316 .currentX = wave->spawnPosition.x, 317 .currentY = wave->spawnPosition.y, 318 }; 319 320 float deltaTime = timeOffset; 321 for (int j = 0; j < maxWaypointCount; j++)
322 {
323 int waypointPassedCount = 0; 324 Vector2 pos = EnemyGetPosition(&dummy, deltaTime, &dummy.simVelocity, &waypointPassedCount); 325 // after the initial variable starting offset, we use a fixed time step 326 deltaTime = timeStep; 327 dummy.simPosition = pos; 328 329 // Update the dummy's position just like we do in the regular enemy update loop 330 for (int k = 0; k < waypointPassedCount; k++) 331 { 332 dummy.currentX = dummy.nextX; 333 dummy.currentY = dummy.nextY; 334 if (EnemyGetNextPosition(dummy.currentX, dummy.currentY, &dummy.nextX, &dummy.nextY) && 335 Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
336 { 337 break; 338 }
339 } 340 if (Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f) 341 { 342 break; 343 } 344 345 // get the angle we need to rotate the arrow model. The velocity is just fine for this. 346 float angle = atan2f(dummy.simVelocity.x, dummy.simVelocity.y) * RAD2DEG - 90.0f; 347 DrawModelEx(pathArrowModel, (Vector3){pos.x, 0.15f, pos.y}, (Vector3){0, 1, 0}, angle, arrowScale, WHITE);
348 } 349 } 350 } 351 352 void DrawLevelBuildingState(Level *level) 353 { 354 BeginMode3D(level->camera); 355 DrawLevelGround(level); 356 357 int blockedCellCount = 0; 358 Vector2 blockedCells[1]; 359 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera); 360 float planeDistance = ray.position.y / -ray.direction.y; 361 float planeX = ray.direction.x * planeDistance + ray.position.x; 362 float planeY = ray.direction.z * planeDistance + ray.position.z; 363 int16_t mapX = (int16_t)floorf(planeX + 0.5f); 364 int16_t mapY = (int16_t)floorf(planeY + 0.5f); 365 if (level->placementMode && !guiState.isBlocked && mapX >= -5 && mapX <= 5 && mapY >= -5 && mapY <= 5) 366 { 367 DrawCubeWires((Vector3){mapX, 0.2f, mapY}, 1.0f, 0.4f, 1.0f, RED); 368 blockedCells[blockedCellCount++] = (Vector2){mapX, mapY}; 369 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) 370 { 371 if (TowerTryAdd(level->placementMode, mapX, mapY)) 372 { 373 level->playerGold -= GetTowerCosts(level->placementMode); 374 level->placementMode = TOWER_TYPE_NONE; 375 } 376 } 377 } 378 379 PathFindingMapUpdate(blockedCellCount, blockedCells); 380 DrawEnemyPath(level); 381 TowerDraw(); 382 EnemyDraw(); 383 ProjectileDraw(); 384 ParticleDraw(); 385 386 guiState.isBlocked = 0; 387 388 EndMode3D(); 389 390 TowerDrawHealthBars(level->camera); 391 392 static ButtonState buildWallButtonState = {0}; 393 static ButtonState buildGunButtonState = {0}; 394 buildWallButtonState.isSelected = level->placementMode == TOWER_TYPE_WALL; 395 buildGunButtonState.isSelected = level->placementMode == TOWER_TYPE_ARCHER; 396 397 DrawBuildingBuildButton(level, 10, 10, 110, 30, TOWER_TYPE_WALL, "Wall"); 398 DrawBuildingBuildButton(level, 10, 50, 110, 30, TOWER_TYPE_ARCHER, "Archer"); 399 DrawBuildingBuildButton(level, 10, 90, 110, 30, TOWER_TYPE_BALLISTA, "Ballista"); 400 DrawBuildingBuildButton(level, 10, 130, 110, 30, TOWER_TYPE_CATAPULT, "Catapult"); 401 402 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 403 { 404 level->nextState = LEVEL_STATE_RESET; 405 } 406 407 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0)) 408 { 409 level->nextState = LEVEL_STATE_BATTLE; 410 } 411 412 const char *text = "Building phase"; 413 int textWidth = MeasureText(text, 20); 414 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 415 } 416 417 void InitBattleStateConditions(Level *level) 418 { 419 level->state = LEVEL_STATE_BATTLE; 420 level->nextState = LEVEL_STATE_NONE; 421 level->waveEndTimer = 0.0f; 422 for (int i = 0; i < 10; i++) 423 { 424 EnemyWave *wave = &level->waves[i]; 425 wave->spawned = 0; 426 wave->timeToSpawnNext = wave->delay; 427 } 428 } 429 430 void DrawLevelBattleState(Level *level) 431 { 432 BeginMode3D(level->camera); 433 DrawLevelGround(level); 434 TowerDraw(); 435 EnemyDraw(); 436 ProjectileDraw(); 437 ParticleDraw(); 438 guiState.isBlocked = 0; 439 EndMode3D(); 440 441 EnemyDrawHealthbars(level->camera); 442 TowerDrawHealthBars(level->camera); 443 444 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 445 { 446 level->nextState = LEVEL_STATE_RESET; 447 } 448 449 int maxCount = 0; 450 int remainingCount = 0; 451 for (int i = 0; i < 10; i++) 452 { 453 EnemyWave *wave = &level->waves[i]; 454 if (wave->wave != level->currentWave) 455 { 456 continue; 457 } 458 maxCount += wave->count; 459 remainingCount += wave->count - wave->spawned; 460 } 461 int aliveCount = EnemyCount(); 462 remainingCount += aliveCount; 463 464 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount); 465 int textWidth = MeasureText(text, 20); 466 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 467 } 468 469 void DrawLevel(Level *level) 470 { 471 switch (level->state) 472 { 473 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break; 474 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break; 475 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break; 476 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break; 477 default: break; 478 } 479 480 DrawLevelHud(level); 481 } 482 483 void UpdateLevel(Level *level) 484 { 485 if (level->state == LEVEL_STATE_BATTLE) 486 { 487 int activeWaves = 0; 488 for (int i = 0; i < 10; i++) 489 { 490 EnemyWave *wave = &level->waves[i]; 491 if (wave->spawned >= wave->count || wave->wave != level->currentWave) 492 { 493 continue; 494 } 495 activeWaves++; 496 wave->timeToSpawnNext -= gameTime.deltaTime; 497 if (wave->timeToSpawnNext <= 0.0f) 498 { 499 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y); 500 if (enemy) 501 { 502 wave->timeToSpawnNext = wave->interval; 503 wave->spawned++; 504 } 505 } 506 } 507 if (GetTowerByType(TOWER_TYPE_BASE) == 0) { 508 level->waveEndTimer += gameTime.deltaTime; 509 if (level->waveEndTimer >= 2.0f) 510 { 511 level->nextState = LEVEL_STATE_LOST_WAVE; 512 } 513 } 514 else if (activeWaves == 0 && EnemyCount() == 0) 515 { 516 level->waveEndTimer += gameTime.deltaTime; 517 if (level->waveEndTimer >= 2.0f) 518 { 519 level->nextState = LEVEL_STATE_WON_WAVE; 520 } 521 } 522 } 523 524 PathFindingMapUpdate(0, 0); 525 EnemyUpdate(); 526 TowerUpdate(); 527 ProjectileUpdate(); 528 ParticleUpdate(); 529 530 if (level->nextState == LEVEL_STATE_RESET) 531 { 532 InitLevel(level); 533 } 534 535 if (level->nextState == LEVEL_STATE_BATTLE) 536 { 537 InitBattleStateConditions(level); 538 } 539 540 if (level->nextState == LEVEL_STATE_WON_WAVE) 541 { 542 level->currentWave++; 543 level->state = LEVEL_STATE_WON_WAVE; 544 } 545 546 if (level->nextState == LEVEL_STATE_LOST_WAVE) 547 { 548 level->state = LEVEL_STATE_LOST_WAVE; 549 } 550 551 if (level->nextState == LEVEL_STATE_BUILDING) 552 { 553 level->state = LEVEL_STATE_BUILDING; 554 } 555 556 if (level->nextState == LEVEL_STATE_WON_LEVEL) 557 { 558 // make something of this later 559 InitLevel(level); 560 } 561 562 level->nextState = LEVEL_STATE_NONE; 563 } 564 565 float nextSpawnTime = 0.0f; 566 567 void ResetGame() 568 { 569 InitLevel(currentLevel); 570 } 571 572 void InitGame() 573 { 574 TowerInit(); 575 EnemyInit(); 576 ProjectileInit(); 577 ParticleInit(); 578 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f); 579 580 currentLevel = levels; 581 InitLevel(currentLevel); 582 } 583 584 //# Immediate GUI functions 585 586 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth) 587 { 588 const float healthBarHeight = 6.0f; 589 const float healthBarOffset = 15.0f; 590 const float inset = 2.0f; 591 const float innerWidth = healthBarWidth - inset * 2; 592 const float innerHeight = healthBarHeight - inset * 2; 593 594 Vector2 screenPos = GetWorldToScreen(position, camera); 595 float centerX = screenPos.x - healthBarWidth * 0.5f; 596 float topY = screenPos.y - healthBarOffset; 597 DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK); 598 float healthWidth = innerWidth * healthRatio; 599 DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor); 600 } 601 602 int Button(const char *text, int x, int y, int width, int height, ButtonState *state) 603 { 604 Rectangle bounds = {x, y, width, height}; 605 int isPressed = 0; 606 int isSelected = state && state->isSelected; 607 int isDisabled = state && state->isDisabled; 608 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !isDisabled) 609 { 610 Color color = isSelected ? DARKGRAY : GRAY; 611 DrawRectangle(x, y, width, height, color); 612 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) 613 { 614 isPressed = 1; 615 } 616 guiState.isBlocked = 1; 617 } 618 else 619 { 620 Color color = isSelected ? WHITE : LIGHTGRAY; 621 DrawRectangle(x, y, width, height, color); 622 } 623 Font font = GetFontDefault(); 624 Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1); 625 Color textColor = isDisabled ? GRAY : BLACK; 626 DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, textColor); 627 return isPressed; 628 } 629 630 //# Main game loop 631 632 void GameUpdate() 633 { 634 float dt = GetFrameTime(); 635 // cap maximum delta time to 0.1 seconds to prevent large time steps 636 if (dt > 0.1f) dt = 0.1f; 637 gameTime.time += dt; 638 gameTime.deltaTime = dt; 639 640 UpdateLevel(currentLevel); 641 } 642 643 int main(void) 644 { 645 int screenWidth, screenHeight; 646 GetPreferredSize(&screenWidth, &screenHeight); 647 InitWindow(screenWidth, screenHeight, "Tower defense"); 648 SetTargetFPS(30); 649 650 LoadAssets(); 651 InitGame(); 652 653 while (!WindowShouldClose()) 654 { 655 if (IsPaused()) { 656 // canvas is not visible in browser - do nothing 657 continue; 658 } 659 660 BeginDrawing(); 661 ClearBackground((Color){0x4E, 0x63, 0x26, 0xFF}); 662 663 GameUpdate(); 664 DrawLevel(currentLevel); 665 666 EndDrawing(); 667 } 668 669 CloseWindow(); 670 671 return 0; 672 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 
  4 // The queue is a simple array of nodes, we add nodes to the end and remove
  5 // nodes from the front. We keep the array around to avoid unnecessary allocations
  6 static PathfindingNode *pathfindingNodeQueue = 0;
  7 static int pathfindingNodeQueueCount = 0;
  8 static int pathfindingNodeQueueCapacity = 0;
  9 
 10 // The pathfinding map stores the distances from the castle to each cell in the map.
 11 static PathfindingMap pathfindingMap = {0};
 12 
 13 void PathfindingMapInit(int width, int height, Vector3 translate, float scale)
 14 {
 15   // transforming between map space and world space allows us to adapt 
 16   // position and scale of the map without changing the pathfinding data
 17   pathfindingMap.toWorldSpace = MatrixTranslate(translate.x, translate.y, translate.z);
 18   pathfindingMap.toWorldSpace = MatrixMultiply(pathfindingMap.toWorldSpace, MatrixScale(scale, scale, scale));
 19   pathfindingMap.toMapSpace = MatrixInvert(pathfindingMap.toWorldSpace);
 20   pathfindingMap.width = width;
 21   pathfindingMap.height = height;
 22   pathfindingMap.scale = scale;
 23   pathfindingMap.distances = (float *)MemAlloc(width * height * sizeof(float));
 24   for (int i = 0; i < width * height; i++)
 25   {
 26     pathfindingMap.distances[i] = -1.0f;
 27   }
 28 
 29   pathfindingMap.towerIndex = (long *)MemAlloc(width * height * sizeof(long));
 30   pathfindingMap.deltaSrc = (DeltaSrc *)MemAlloc(width * height * sizeof(DeltaSrc));
 31 }
 32 
 33 static void PathFindingNodePush(int16_t x, int16_t y, int16_t fromX, int16_t fromY, float distance)
 34 {
 35   if (pathfindingNodeQueueCount >= pathfindingNodeQueueCapacity)
 36   {
 37     pathfindingNodeQueueCapacity = pathfindingNodeQueueCapacity == 0 ? 256 : pathfindingNodeQueueCapacity * 2;
 38     // we use MemAlloc/MemRealloc to allocate memory for the queue
 39     // I am not entirely sure if MemRealloc allows passing a null pointer
 40     // so we check if the pointer is null and use MemAlloc in that case
 41     if (pathfindingNodeQueue == 0)
 42     {
 43       pathfindingNodeQueue = (PathfindingNode *)MemAlloc(pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
 44     }
 45     else
 46     {
 47       pathfindingNodeQueue = (PathfindingNode *)MemRealloc(pathfindingNodeQueue, pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
 48     }
 49   }
 50 
 51   PathfindingNode *node = &pathfindingNodeQueue[pathfindingNodeQueueCount++];
 52   node->x = x;
 53   node->y = y;
 54   node->fromX = fromX;
 55   node->fromY = fromY;
 56   node->distance = distance;
 57 }
 58 
 59 static PathfindingNode *PathFindingNodePop()
 60 {
 61   if (pathfindingNodeQueueCount == 0)
 62   {
 63     return 0;
 64   }
 65   // we return the first node in the queue; we want to return a pointer to the node
 66   // so we can return 0 if the queue is empty. 
 67   // We should _not_ return a pointer to the element in the list, because the list
 68   // may be reallocated and the pointer would become invalid. Or the 
 69   // popped element is overwritten by the next push operation.
 70   // Using static here means that the variable is permanently allocated.
 71   static PathfindingNode node;
 72   node = pathfindingNodeQueue[0];
 73   // we shift all nodes one position to the front
 74   for (int i = 1; i < pathfindingNodeQueueCount; i++)
 75   {
 76     pathfindingNodeQueue[i - 1] = pathfindingNodeQueue[i];
 77   }
 78   --pathfindingNodeQueueCount;
 79   return &node;
 80 }
 81 
 82 float PathFindingGetDistance(int mapX, int mapY)
 83 {
 84   if (mapX < 0 || mapX >= pathfindingMap.width || mapY < 0 || mapY >= pathfindingMap.height)
 85   {
 86     // when outside the map, we return the manhattan distance to the castle (0,0)
 87     return fabsf((float)mapX) + fabsf((float)mapY);
 88   }
 89 
 90   return pathfindingMap.distances[mapY * pathfindingMap.width + mapX];
 91 }
 92 
 93 // transform a world position to a map position in the array; 
 94 // returns true if the position is inside the map
 95 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY)
 96 {
 97   Vector3 mapPosition = Vector3Transform(worldPosition, pathfindingMap.toMapSpace);
 98   *mapX = (int16_t)mapPosition.x;
 99   *mapY = (int16_t)mapPosition.z;
100   return *mapX >= 0 && *mapX < pathfindingMap.width && *mapY >= 0 && *mapY < pathfindingMap.height;
101 }
102 
103 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells)
104 {
105   const int castleX = 0, castleY = 0;
106   int16_t castleMapX, castleMapY;
107   if (!PathFindingFromWorldToMapPosition((Vector3){castleX, 0.0f, castleY}, &castleMapX, &castleMapY))
108   {
109     return;
110   }
111   int width = pathfindingMap.width, height = pathfindingMap.height;
112 
113   // reset the distances to -1
114   for (int i = 0; i < width * height; i++)
115   {
116     pathfindingMap.distances[i] = -1.0f;
117   }
118   // reset the tower indices
119   for (int i = 0; i < width * height; i++)
120   {
121     pathfindingMap.towerIndex[i] = -1;
122   }
123   // reset the delta src
124   for (int i = 0; i < width * height; i++)
125   {
126     pathfindingMap.deltaSrc[i].x = 0;
127     pathfindingMap.deltaSrc[i].y = 0;
128   }
129 
130   for (int i = 0; i < blockedCellCount; i++)
131   {
132     int16_t mapX, mapY;
133     if (!PathFindingFromWorldToMapPosition((Vector3){blockedCells[i].x, 0.0f, blockedCells[i].y}, &mapX, &mapY))
134     {
135       continue;
136     }
137     int index = mapY * width + mapX;
138     pathfindingMap.towerIndex[index] = -2;
139   }
140 
141   for (int i = 0; i < towerCount; i++)
142   {
143     Tower *tower = &towers[i];
144     if (tower->towerType == TOWER_TYPE_NONE || tower->towerType == TOWER_TYPE_BASE)
145     {
146       continue;
147     }
148     int16_t mapX, mapY;
149     // technically, if the tower cell scale is not in sync with the pathfinding map scale,
150     // this would not work correctly and needs to be refined to allow towers covering multiple cells
151     // or having multiple towers in one cell; for simplicity, we assume that the tower covers exactly
152     // one cell. For now.
153     if (!PathFindingFromWorldToMapPosition((Vector3){tower->x, 0.0f, tower->y}, &mapX, &mapY))
154     {
155       continue;
156     }
157     int index = mapY * width + mapX;
158     pathfindingMap.towerIndex[index] = i;
159   }
160 
161   // we start at the castle and add the castle to the queue
162   pathfindingMap.maxDistance = 0.0f;
163   pathfindingNodeQueueCount = 0;
164   PathFindingNodePush(castleMapX, castleMapY, castleMapX, castleMapY, 0.0f);
165   PathfindingNode *node = 0;
166   while ((node = PathFindingNodePop()))
167   {
168     if (node->x < 0 || node->x >= width || node->y < 0 || node->y >= height)
169     {
170       continue;
171     }
172     int index = node->y * width + node->x;
173     if (pathfindingMap.distances[index] >= 0 && pathfindingMap.distances[index] <= node->distance)
174     {
175       continue;
176     }
177 
178     int deltaX = node->x - node->fromX;
179     int deltaY = node->y - node->fromY;
180     // even if the cell is blocked by a tower, we still may want to store the direction
181     // (though this might not be needed, IDK right now)
182     pathfindingMap.deltaSrc[index].x = (char) deltaX;
183     pathfindingMap.deltaSrc[index].y = (char) deltaY;
184 
185     // we skip nodes that are blocked by towers or by the provided blocked cells
186     if (pathfindingMap.towerIndex[index] != -1)
187     {
188       node->distance += 8.0f;
189     }
190     pathfindingMap.distances[index] = node->distance;
191     pathfindingMap.maxDistance = fmaxf(pathfindingMap.maxDistance, node->distance);
192     PathFindingNodePush(node->x, node->y + 1, node->x, node->y, node->distance + 1.0f);
193     PathFindingNodePush(node->x, node->y - 1, node->x, node->y, node->distance + 1.0f);
194     PathFindingNodePush(node->x + 1, node->y, node->x, node->y, node->distance + 1.0f);
195     PathFindingNodePush(node->x - 1, node->y, node->x, node->y, node->distance + 1.0f);
196   }
197 }
198 
199 void PathFindingMapDraw()
200 {
201   float cellSize = pathfindingMap.scale * 0.9f;
202   float highlightDistance = fmodf(GetTime() * 4.0f, pathfindingMap.maxDistance);
203   for (int x = 0; x < pathfindingMap.width; x++)
204   {
205     for (int y = 0; y < pathfindingMap.height; y++)
206     {
207       float distance = pathfindingMap.distances[y * pathfindingMap.width + x];
208       float colorV = distance < 0 ? 0 : fminf(distance / pathfindingMap.maxDistance, 1.0f);
209       Color color = distance < 0 ? BLUE : (Color){fminf(colorV, 1.0f) * 255, 0, 0, 255};
210       Vector3 position = Vector3Transform((Vector3){x, -0.25f, y}, pathfindingMap.toWorldSpace);
211       // animate the distance "wave" to show how the pathfinding algorithm expands
212       // from the castle
213       if (distance + 0.5f > highlightDistance && distance - 0.5f < highlightDistance)
214       {
215         color = BLACK;
216       }
217       DrawCube(position, cellSize, 0.1f, cellSize, color);
218     }
219   }
220 }
221 
222 Vector2 PathFindingGetGradient(Vector3 world)
223 {
224   int16_t mapX, mapY;
225   if (PathFindingFromWorldToMapPosition(world, &mapX, &mapY))
226   {
227     DeltaSrc delta = pathfindingMap.deltaSrc[mapY * pathfindingMap.width + mapX];
228     return (Vector2){(float)-delta.x, (float)-delta.y};
229   }
230   // fallback to a simple gradient calculation
231   float n = PathFindingGetDistance(mapX, mapY - 1);
232   float s = PathFindingGetDistance(mapX, mapY + 1);
233   float w = PathFindingGetDistance(mapX - 1, mapY);
234   float e = PathFindingGetDistance(mapX + 1, mapY);
235   return (Vector2){w - e + 0.25f, n - s + 0.125f};
236 }
  1 #ifndef TD_TUT_2_MAIN_H
  2 #define TD_TUT_2_MAIN_H
  3 
  4 #include <inttypes.h>
  5 
  6 #include "raylib.h"
  7 #include "preferred_size.h"
  8 
  9 //# Declarations
 10 
 11 #define ENEMY_MAX_PATH_COUNT 8
 12 #define ENEMY_MAX_COUNT 400
 13 #define ENEMY_TYPE_NONE 0
 14 #define ENEMY_TYPE_MINION 1
 15 
 16 #define PARTICLE_MAX_COUNT 400
 17 #define PARTICLE_TYPE_NONE 0
 18 #define PARTICLE_TYPE_EXPLOSION 1
 19 
 20 typedef struct Particle
 21 {
 22   uint8_t particleType;
 23   float spawnTime;
 24   float lifetime;
 25   Vector3 position;
 26   Vector3 velocity;
 27   Vector3 scale;
 28 } Particle;
 29 
 30 #define TOWER_MAX_COUNT 400
 31 enum TowerType
 32 {
 33   TOWER_TYPE_NONE,
 34   TOWER_TYPE_BASE,
 35   TOWER_TYPE_ARCHER,
 36   TOWER_TYPE_BALLISTA,
 37   TOWER_TYPE_CATAPULT,
 38   TOWER_TYPE_WALL,
 39   TOWER_TYPE_COUNT
 40 };
 41 
 42 typedef struct HitEffectConfig
 43 {
 44   float damage;
 45   float areaDamageRadius;
 46   float pushbackPowerDistance;
 47 } HitEffectConfig;
 48 
 49 typedef struct TowerTypeConfig
 50 {
 51   float cooldown;
 52   float range;
 53   float projectileSpeed;
 54   
 55   uint8_t cost;
 56   uint8_t projectileType;
 57   uint16_t maxHealth;
 58 
 59   HitEffectConfig hitEffect;
 60 } TowerTypeConfig;
 61 
 62 typedef struct Tower
 63 {
 64   int16_t x, y;
 65   uint8_t towerType;
 66   Vector2 lastTargetPosition;
 67   float cooldown;
 68   float damage;
 69 } Tower;
 70 
 71 typedef struct GameTime
 72 {
 73   float time;
 74   float deltaTime;
 75 } GameTime;
 76 
 77 typedef struct ButtonState {
 78   char isSelected;
 79   char isDisabled;
 80 } ButtonState;
 81 
 82 typedef struct GUIState {
 83   int isBlocked;
 84 } GUIState;
 85 
 86 typedef enum LevelState
 87 {
 88   LEVEL_STATE_NONE,
 89   LEVEL_STATE_BUILDING,
 90   LEVEL_STATE_BATTLE,
 91   LEVEL_STATE_WON_WAVE,
 92   LEVEL_STATE_LOST_WAVE,
 93   LEVEL_STATE_WON_LEVEL,
 94   LEVEL_STATE_RESET,
 95 } LevelState;
 96 
 97 typedef struct EnemyWave {
 98   uint8_t enemyType;
 99   uint8_t wave;
100   uint16_t count;
101   float interval;
102   float delay;
103   Vector2 spawnPosition;
104 
105   uint16_t spawned;
106   float timeToSpawnNext;
107 } EnemyWave;
108 
109 #define ENEMY_MAX_WAVE_COUNT 10
110 
111 typedef struct Level
112 {
113   int seed;
114   LevelState state;
115   LevelState nextState;
116   Camera3D camera;
117   int placementMode;
118 
119   int initialGold;
120   int playerGold;
121 
122   EnemyWave waves[ENEMY_MAX_WAVE_COUNT];
123   int currentWave;
124   float waveEndTimer;
125 } Level;
126 
127 typedef struct DeltaSrc
128 {
129   char x, y;
130 } DeltaSrc;
131 
132 typedef struct PathfindingMap
133 {
134   int width, height;
135   float scale;
136   float *distances;
137   long *towerIndex; 
138   DeltaSrc *deltaSrc;
139   float maxDistance;
140   Matrix toMapSpace;
141   Matrix toWorldSpace;
142 } PathfindingMap;
143 
144 // when we execute the pathfinding algorithm, we need to store the active nodes
145 // in a queue. Each node has a position, a distance from the start, and the
146 // position of the node that we came from.
147 typedef struct PathfindingNode
148 {
149   int16_t x, y, fromX, fromY;
150   float distance;
151 } PathfindingNode;
152 
153 typedef struct EnemyId
154 {
155   uint16_t index;
156   uint16_t generation;
157 } EnemyId;
158 
159 typedef struct EnemyClassConfig
160 {
161   float speed;
162   float health;
163   float radius;
164   float maxAcceleration;
165   float requiredContactTime;
166   float explosionDamage;
167   float explosionRange;
168   float explosionPushbackPower;
169   int goldValue;
170 } EnemyClassConfig;
171 
172 typedef struct Enemy
173 {
174   int16_t currentX, currentY;
175   int16_t nextX, nextY;
176   Vector2 simPosition;
177   Vector2 simVelocity;
178   uint16_t generation;
179   float walkedDistance;
180   float startMovingTime;
181   float damage, futureDamage;
182   float contactTime;
183   uint8_t enemyType;
184   uint8_t movePathCount;
185   Vector2 movePath[ENEMY_MAX_PATH_COUNT];
186 } Enemy;
187 
188 // a unit that uses sprites to be drawn
189 #define SPRITE_UNIT_PHASE_WEAPON_IDLE 0
190 #define SPRITE_UNIT_PHASE_WEAPON_COOLDOWN 1
191 typedef struct SpriteUnit
192 {
193   Rectangle srcRect;
194   Vector2 offset;
195   int frameCount;
196   float frameDuration;
197   Rectangle srcWeaponIdleRect;
198   Vector2 srcWeaponIdleOffset;
199   Rectangle srcWeaponCooldownRect;
200   Vector2 srcWeaponCooldownOffset;
201 } SpriteUnit;
202 
203 #define PROJECTILE_MAX_COUNT 1200
204 #define PROJECTILE_TYPE_NONE 0
205 #define PROJECTILE_TYPE_ARROW 1
206 #define PROJECTILE_TYPE_CATAPULT 2
207 #define PROJECTILE_TYPE_BALLISTA 3
208 
209 typedef struct Projectile
210 {
211   uint8_t projectileType;
212   float shootTime;
213   float arrivalTime;
214   float distance;
215   Vector3 position;
216   Vector3 target;
217   Vector3 directionNormal;
218   EnemyId targetEnemy;
219   HitEffectConfig hitEffectConfig;
220 } Projectile;
221 
222 //# Function declarations
223 float TowerGetMaxHealth(Tower *tower);
224 int Button(const char *text, int x, int y, int width, int height, ButtonState *state);
225 int EnemyAddDamageRange(Vector2 position, float range, float damage);
226 int EnemyAddDamage(Enemy *enemy, float damage);
227 
228 //# Enemy functions
229 void EnemyInit();
230 void EnemyDraw();
231 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource);
232 void EnemyUpdate();
233 float EnemyGetCurrentMaxSpeed(Enemy *enemy);
234 float EnemyGetMaxHealth(Enemy *enemy);
235 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY);
236 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount);
237 EnemyId EnemyGetId(Enemy *enemy);
238 Enemy *EnemyTryResolve(EnemyId enemyId);
239 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY);
240 int EnemyAddDamage(Enemy *enemy, float damage);
241 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range);
242 int EnemyCount();
243 void EnemyDrawHealthbars(Camera3D camera);
244 
245 //# Tower functions
246 void TowerInit();
247 Tower *TowerGetAt(int16_t x, int16_t y);
248 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y);
249 Tower *GetTowerByType(uint8_t towerType);
250 int GetTowerCosts(uint8_t towerType);
251 float TowerGetMaxHealth(Tower *tower);
252 void TowerDraw();
253 void TowerUpdate();
254 void TowerDrawHealthBars(Camera3D camera);
255 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase);
256 
257 //# Particles
258 void ParticleInit();
259 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime);
260 void ParticleUpdate();
261 void ParticleDraw();
262 
263 //# Projectiles
264 void ProjectileInit();
265 void ProjectileDraw();
266 void ProjectileUpdate();
267 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig);
268 
269 //# Pathfinding map
270 void PathfindingMapInit(int width, int height, Vector3 translate, float scale);
271 float PathFindingGetDistance(int mapX, int mapY);
272 Vector2 PathFindingGetGradient(Vector3 world);
273 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY);
274 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells);
275 void PathFindingMapDraw();
276 
277 //# UI
278 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth);
279 
280 //# Level
281 void DrawLevelGround(Level *level);
282 void DrawEnemyPath(Level *level);
283 
284 //# variables
285 extern Level *currentLevel;
286 extern Enemy enemies[ENEMY_MAX_COUNT];
287 extern int enemyCount;
288 extern EnemyClassConfig enemyClassConfigs[];
289 
290 extern GUIState guiState;
291 extern GameTime gameTime;
292 extern Tower towers[TOWER_MAX_COUNT];
293 extern int towerCount;
294 
295 extern Texture2D palette, spriteSheet;
296 
297 #endif
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 
  4 static TowerTypeConfig towerTypeConfigs[TOWER_TYPE_COUNT] = {
  5     [TOWER_TYPE_BASE] = {
  6         .maxHealth = 10,
  7     },
  8     [TOWER_TYPE_ARCHER] = {
  9         .cooldown = 0.5f,
 10         .range = 3.0f,
 11         .cost = 6,
 12         .maxHealth = 10,
 13         .projectileSpeed = 4.0f,
 14         .projectileType = PROJECTILE_TYPE_ARROW,
 15         .hitEffect = {
 16           .damage = 3.0f,
 17         }
 18     },
 19     [TOWER_TYPE_BALLISTA] = {
 20         .cooldown = 1.5f,
 21         .range = 6.0f,
 22         .cost = 9,
 23         .maxHealth = 10,
 24         .projectileSpeed = 10.0f,
 25         .projectileType = PROJECTILE_TYPE_BALLISTA,
 26         .hitEffect = {
 27           .damage = 6.0f,
 28           .pushbackPowerDistance = 0.25f,
 29         }
 30     },
 31     [TOWER_TYPE_CATAPULT] = {
 32         .cooldown = 1.7f,
 33         .range = 5.0f,
 34         .cost = 10,
 35         .maxHealth = 10,
 36         .projectileSpeed = 3.0f,
 37         .projectileType = PROJECTILE_TYPE_CATAPULT,
 38         .hitEffect = {
 39           .damage = 2.0f,
 40           .areaDamageRadius = 1.75f,
 41         }
 42     },
 43     [TOWER_TYPE_WALL] = {
 44         .cost = 2,
 45         .maxHealth = 10,
 46     },
 47 };
 48 
 49 Tower towers[TOWER_MAX_COUNT];
 50 int towerCount = 0;
 51 
 52 Model towerModels[TOWER_TYPE_COUNT];
 53 
 54 // definition of our archer unit
 55 SpriteUnit archerUnit = {
 56     .srcRect = {0, 0, 16, 16},
 57     .offset = {7, 1},
 58     .frameCount = 1,
 59     .frameDuration = 0.0f,
 60     .srcWeaponIdleRect = {16, 0, 6, 16},
 61     .srcWeaponIdleOffset = {8, 0},
 62     .srcWeaponCooldownRect = {22, 0, 11, 16},
 63     .srcWeaponCooldownOffset = {10, 0},
 64 };
 65 
 66 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase)
 67 {
 68   float xScale = flip ? -1.0f : 1.0f;
 69   Camera3D camera = currentLevel->camera;
 70   float size = 0.5f;
 71   Vector2 offset = (Vector2){ unit.offset.x / 16.0f * size, unit.offset.y / 16.0f * size * xScale };
 72   Vector2 scale = (Vector2){ unit.srcRect.width / 16.0f * size, unit.srcRect.height / 16.0f * size };
 73   // we want the sprite to face the camera, so we need to calculate the up vector
 74   Vector3 forward = Vector3Subtract(camera.target, camera.position);
 75   Vector3 up = {0, 1, 0};
 76   Vector3 right = Vector3CrossProduct(forward, up);
 77   up = Vector3Normalize(Vector3CrossProduct(right, forward));
 78 
 79   Rectangle srcRect = unit.srcRect;
 80   if (unit.frameCount > 1)
 81   {
 82     srcRect.x += (int)(t / unit.frameDuration) % unit.frameCount * srcRect.width;
 83   }
 84   if (flip)
 85   {
 86     srcRect.x += srcRect.width;
 87     srcRect.width = -srcRect.width;
 88   }
 89   DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
 90 
 91   if (phase == SPRITE_UNIT_PHASE_WEAPON_COOLDOWN && unit.srcWeaponCooldownRect.width > 0)
 92   {
 93     offset = (Vector2){ unit.srcWeaponCooldownOffset.x / 16.0f * size, unit.srcWeaponCooldownOffset.y / 16.0f * size };
 94     scale = (Vector2){ unit.srcWeaponCooldownRect.width / 16.0f * size, unit.srcWeaponCooldownRect.height / 16.0f * size };
 95     srcRect = unit.srcWeaponCooldownRect;
 96     if (flip)
 97     {
 98       // position.x = flip * scale.x * 0.5f;
 99       srcRect.x += srcRect.width;
100       srcRect.width = -srcRect.width;
101       offset.x = scale.x - offset.x;
102     }
103     DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
104   }
105   else if (phase == SPRITE_UNIT_PHASE_WEAPON_IDLE && unit.srcWeaponIdleRect.width > 0)
106   {
107     offset = (Vector2){ unit.srcWeaponIdleOffset.x / 16.0f * size, unit.srcWeaponIdleOffset.y / 16.0f * size };
108     scale = (Vector2){ unit.srcWeaponIdleRect.width / 16.0f * size, unit.srcWeaponIdleRect.height / 16.0f * size };
109     srcRect = unit.srcWeaponIdleRect;
110     if (flip)
111     {
112       // position.x = flip * scale.x * 0.5f;
113       srcRect.x += srcRect.width;
114       srcRect.width = -srcRect.width;
115       offset.x = scale.x - offset.x;
116     }
117     DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
118   }
119 }
120 
121 void TowerInit()
122 {
123   for (int i = 0; i < TOWER_MAX_COUNT; i++)
124   {
125     towers[i] = (Tower){0};
126   }
127   towerCount = 0;
128 
129   towerModels[TOWER_TYPE_BASE] = LoadModel("data/keep.glb");
130   towerModels[TOWER_TYPE_WALL] = LoadModel("data/wall-0000.glb");
131 
132   for (int i = 0; i < TOWER_TYPE_COUNT; i++)
133   {
134     if (towerModels[i].materials)
135     {
136       // assign the palette texture to the material of the model (0 is not used afaik)
137       towerModels[i].materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
138     }
139   }
140 }
141 
142 static void TowerGunUpdate(Tower *tower)
143 {
144   TowerTypeConfig config = towerTypeConfigs[tower->towerType];
145   if (tower->cooldown <= 0.0f)
146   {
147     Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, config.range);
148     if (enemy)
149     {
150       tower->cooldown = config.cooldown;
151       // shoot the enemy; determine future position of the enemy
152       float bulletSpeed = config.projectileSpeed;
153       Vector2 velocity = enemy->simVelocity;
154       Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0);
155       Vector2 towerPosition = {tower->x, tower->y};
156       float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed;
157       for (int i = 0; i < 8; i++) {
158         velocity = enemy->simVelocity;
159         futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0);
160         float distance = Vector2Distance(towerPosition, futurePosition);
161         float eta2 = distance / bulletSpeed;
162         if (fabs(eta - eta2) < 0.01f) {
163           break;
164         }
165         eta = (eta2 + eta) * 0.5f;
166       }
167 
168       ProjectileTryAdd(config.projectileType, enemy, 
169         (Vector3){towerPosition.x, 1.33f, towerPosition.y}, 
170         (Vector3){futurePosition.x, 0.25f, futurePosition.y},
171         bulletSpeed, config.hitEffect);
172       enemy->futureDamage += config.hitEffect.damage;
173       tower->lastTargetPosition = futurePosition;
174     }
175   }
176   else
177   {
178     tower->cooldown -= gameTime.deltaTime;
179   }
180 }
181 
182 Tower *TowerGetAt(int16_t x, int16_t y)
183 {
184   for (int i = 0; i < towerCount; i++)
185   {
186     if (towers[i].x == x && towers[i].y == y && towers[i].towerType != TOWER_TYPE_NONE)
187     {
188       return &towers[i];
189     }
190   }
191   return 0;
192 }
193 
194 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y)
195 {
196   if (towerCount >= TOWER_MAX_COUNT)
197   {
198     return 0;
199   }
200 
201   Tower *tower = TowerGetAt(x, y);
202   if (tower)
203   {
204     return 0;
205   }
206 
207   tower = &towers[towerCount++];
208   tower->x = x;
209   tower->y = y;
210   tower->towerType = towerType;
211   tower->cooldown = 0.0f;
212   tower->damage = 0.0f;
213   return tower;
214 }
215 
216 Tower *GetTowerByType(uint8_t towerType)
217 {
218   for (int i = 0; i < towerCount; i++)
219   {
220     if (towers[i].towerType == towerType)
221     {
222       return &towers[i];
223     }
224   }
225   return 0;
226 }
227 
228 int GetTowerCosts(uint8_t towerType)
229 {
230   return towerTypeConfigs[towerType].cost;
231 }
232 
233 float TowerGetMaxHealth(Tower *tower)
234 {
235   return towerTypeConfigs[tower->towerType].maxHealth;
236 }
237 
238 void TowerDraw()
239 {
240   for (int i = 0; i < towerCount; i++)
241   {
242     Tower tower = towers[i];
243     if (tower.towerType == TOWER_TYPE_NONE)
244     {
245       continue;
246     }
247 
248     switch (tower.towerType)
249     {
250     case TOWER_TYPE_ARCHER:
251       {
252         Vector2 screenPosTower = GetWorldToScreen((Vector3){tower.x, 0.0f, tower.y}, currentLevel->camera);
253         Vector2 screenPosTarget = GetWorldToScreen((Vector3){tower.lastTargetPosition.x, 0.0f, tower.lastTargetPosition.y}, currentLevel->camera);
254         DrawModel(towerModels[TOWER_TYPE_WALL], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
255         DrawSpriteUnit(archerUnit, (Vector3){tower.x, 1.0f, tower.y}, 0, screenPosTarget.x > screenPosTower.x, 
256           tower.cooldown > 0.2f ? SPRITE_UNIT_PHASE_WEAPON_COOLDOWN : SPRITE_UNIT_PHASE_WEAPON_IDLE);
257       }
258       break;
259     case TOWER_TYPE_BALLISTA:
260       DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, BROWN);
261       break;
262     case TOWER_TYPE_CATAPULT:
263       DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, DARKGRAY);
264       break;
265     default:
266       if (towerModels[tower.towerType].materials)
267       {
268         DrawModel(towerModels[tower.towerType], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
269       } else {
270         DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY);
271       }
272       break;
273     }
274   }
275 }
276 
277 void TowerUpdate()
278 {
279   for (int i = 0; i < towerCount; i++)
280   {
281     Tower *tower = &towers[i];
282     switch (tower->towerType)
283     {
284     case TOWER_TYPE_CATAPULT:
285     case TOWER_TYPE_BALLISTA:
286     case TOWER_TYPE_ARCHER:
287       TowerGunUpdate(tower);
288       break;
289     }
290   }
291 }
292 
293 void TowerDrawHealthBars(Camera3D camera)
294 {
295   for (int i = 0; i < towerCount; i++)
296   {
297     Tower *tower = &towers[i];
298     if (tower->towerType == TOWER_TYPE_NONE || tower->damage <= 0.0f)
299     {
300       continue;
301     }
302     
303     Vector3 position = (Vector3){tower->x, 0.5f, tower->y};
304     float maxHealth = TowerGetMaxHealth(tower);
305     float health = maxHealth - tower->damage;
306     float healthRatio = health / maxHealth;
307     
308     DrawHealthBar(camera, position, healthRatio, GREEN, 35.0f);
309   }
310 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 #include <stdlib.h>
  4 #include <math.h>
  5 
  6 EnemyClassConfig enemyClassConfigs[] = {
  7     [ENEMY_TYPE_MINION] = {
  8       .health = 10.0f, 
  9       .speed = 0.6f, 
 10       .radius = 0.25f, 
 11       .maxAcceleration = 1.0f,
 12       .explosionDamage = 1.0f,
 13       .requiredContactTime = 0.5f,
 14       .explosionRange = 1.0f,
 15       .explosionPushbackPower = 0.25f,
 16       .goldValue = 1,
 17     },
 18 };
 19 
 20 Enemy enemies[ENEMY_MAX_COUNT];
 21 int enemyCount = 0;
 22 
 23 SpriteUnit enemySprites[] = {
 24     [ENEMY_TYPE_MINION] = {
 25       .srcRect = {0, 16, 16, 16},
 26       .offset = {8.0f, 0.0f},
 27       .frameCount = 6,
 28       .frameDuration = 0.1f,
 29     },
 30 };
 31 
 32 void EnemyInit()
 33 {
 34   for (int i = 0; i < ENEMY_MAX_COUNT; i++)
 35   {
 36     enemies[i] = (Enemy){0};
 37   }
 38   enemyCount = 0;
 39 }
 40 
 41 float EnemyGetCurrentMaxSpeed(Enemy *enemy)
 42 {
 43   return enemyClassConfigs[enemy->enemyType].speed;
 44 }
 45 
 46 float EnemyGetMaxHealth(Enemy *enemy)
 47 {
 48   return enemyClassConfigs[enemy->enemyType].health;
 49 }
 50 
 51 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY)
 52 {
 53   int16_t castleX = 0;
 54   int16_t castleY = 0;
 55   int16_t dx = castleX - currentX;
 56   int16_t dy = castleY - currentY;
 57   if (dx == 0 && dy == 0)
 58   {
 59     *nextX = currentX;
 60     *nextY = currentY;
 61     return 1;
 62   }
 63   Vector2 gradient = PathFindingGetGradient((Vector3){currentX, 0, currentY});
 64 
 65   if (gradient.x == 0 && gradient.y == 0)
 66   {
 67     *nextX = currentX;
 68     *nextY = currentY;
 69     return 1;
 70   }
 71 
 72   if (fabsf(gradient.x) > fabsf(gradient.y))
 73   {
 74     *nextX = currentX + (int16_t)(gradient.x > 0.0f ? 1 : -1);
 75     *nextY = currentY;
 76     return 0;
 77   }
 78   *nextX = currentX;
 79   *nextY = currentY + (int16_t)(gradient.y > 0.0f ? 1 : -1);
 80   return 0;
 81 }
 82 
 83 
 84 // this function predicts the movement of the unit for the next deltaT seconds
 85 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount)
 86 {
 87   const float pointReachedDistance = 0.25f;
 88   const float pointReachedDistance2 = pointReachedDistance * pointReachedDistance;
 89   const float maxSimStepTime = 0.015625f;
 90   
 91   float maxAcceleration = enemyClassConfigs[enemy->enemyType].maxAcceleration;
 92   float maxSpeed = EnemyGetCurrentMaxSpeed(enemy);
 93   int16_t nextX = enemy->nextX;
 94   int16_t nextY = enemy->nextY;
 95   Vector2 position = enemy->simPosition;
 96   int passedCount = 0;
 97   for (float t = 0.0f; t < deltaT; t += maxSimStepTime)
 98   {
 99     float stepTime = fminf(deltaT - t, maxSimStepTime);
100     Vector2 target = (Vector2){nextX, nextY};
101     float speed = Vector2Length(*velocity);
102     // draw the target position for debugging
103 //DrawCubeWires((Vector3){target.x, 0.2f, target.y}, 0.1f, 0.4f, 0.1f, RED);
104 Vector2 lookForwardPos = Vector2Add(position, Vector2Scale(*velocity, speed)); 105 if (Vector2DistanceSqr(target, lookForwardPos) <= pointReachedDistance2) 106 { 107 // we reached the target position, let's move to the next waypoint 108 EnemyGetNextPosition(nextX, nextY, &nextX, &nextY); 109 target = (Vector2){nextX, nextY}; 110 // track how many waypoints we passed 111 passedCount++; 112 } 113 114 // acceleration towards the target 115 Vector2 unitDirection = Vector2Normalize(Vector2Subtract(target, lookForwardPos)); 116 Vector2 acceleration = Vector2Scale(unitDirection, maxAcceleration * stepTime); 117 *velocity = Vector2Add(*velocity, acceleration); 118 119 // limit the speed to the maximum speed 120 if (speed > maxSpeed) 121 { 122 *velocity = Vector2Scale(*velocity, maxSpeed / speed); 123 } 124 125 // move the enemy 126 position = Vector2Add(position, Vector2Scale(*velocity, stepTime)); 127 } 128 129 if (waypointPassedCount) 130 { 131 (*waypointPassedCount) = passedCount; 132 } 133 134 return position; 135 } 136 137 void EnemyDraw() 138 { 139 for (int i = 0; i < enemyCount; i++) 140 { 141 Enemy enemy = enemies[i]; 142 if (enemy.enemyType == ENEMY_TYPE_NONE) 143 { 144 continue; 145 } 146 147 Vector2 position = EnemyGetPosition(&enemy, gameTime.time - enemy.startMovingTime, &enemy.simVelocity, 0); 148 149 // don't draw any trails for now; might replace this with footprints later 150 // if (enemy.movePathCount > 0) 151 // { 152 // Vector3 p = {enemy.movePath[0].x, 0.2f, enemy.movePath[0].y}; 153 // DrawLine3D(p, (Vector3){position.x, 0.2f, position.y}, GREEN); 154 // } 155 // for (int j = 1; j < enemy.movePathCount; j++) 156 // { 157 // Vector3 p = {enemy.movePath[j - 1].x, 0.2f, enemy.movePath[j - 1].y}; 158 // Vector3 q = {enemy.movePath[j].x, 0.2f, enemy.movePath[j].y}; 159 // DrawLine3D(p, q, GREEN); 160 // } 161 162 switch (enemy.enemyType) 163 { 164 case ENEMY_TYPE_MINION: 165 DrawSpriteUnit(enemySprites[ENEMY_TYPE_MINION], (Vector3){position.x, 0.0f, position.y}, 166 enemy.walkedDistance, 0, 0); 167 break; 168 } 169 } 170 } 171 172 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource) 173 { 174 // damage the tower 175 float explosionDamge = enemyClassConfigs[enemy->enemyType].explosionDamage; 176 float explosionRange = enemyClassConfigs[enemy->enemyType].explosionRange; 177 float explosionPushbackPower = enemyClassConfigs[enemy->enemyType].explosionPushbackPower; 178 float explosionRange2 = explosionRange * explosionRange; 179 tower->damage += enemyClassConfigs[enemy->enemyType].explosionDamage; 180 // explode the enemy 181 if (tower->damage >= TowerGetMaxHealth(tower)) 182 { 183 tower->towerType = TOWER_TYPE_NONE; 184 } 185 186 ParticleAdd(PARTICLE_TYPE_EXPLOSION, 187 explosionSource, 188 (Vector3){0, 0.1f, 0}, (Vector3){1.0f, 1.0f, 1.0f}, 1.0f); 189 190 enemy->enemyType = ENEMY_TYPE_NONE; 191 192 // push back enemies & dealing damage 193 for (int i = 0; i < enemyCount; i++) 194 { 195 Enemy *other = &enemies[i]; 196 if (other->enemyType == ENEMY_TYPE_NONE) 197 { 198 continue; 199 } 200 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, other->simPosition); 201 if (distanceSqr > 0 && distanceSqr < explosionRange2) 202 { 203 Vector2 direction = Vector2Normalize(Vector2Subtract(other->simPosition, enemy->simPosition)); 204 other->simPosition = Vector2Add(other->simPosition, Vector2Scale(direction, explosionPushbackPower)); 205 EnemyAddDamage(other, explosionDamge); 206 } 207 } 208 } 209 210 void EnemyUpdate() 211 { 212 const float castleX = 0; 213 const float castleY = 0; 214 const float maxPathDistance2 = 0.25f * 0.25f; 215 216 for (int i = 0; i < enemyCount; i++) 217 { 218 Enemy *enemy = &enemies[i]; 219 if (enemy->enemyType == ENEMY_TYPE_NONE) 220 { 221 continue; 222 } 223 224 int waypointPassedCount = 0; 225 Vector2 prevPosition = enemy->simPosition; 226 enemy->simPosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &enemy->simVelocity, &waypointPassedCount); 227 enemy->startMovingTime = gameTime.time; 228 enemy->walkedDistance += Vector2Distance(prevPosition, enemy->simPosition); 229 // track path of unit 230 if (enemy->movePathCount == 0 || Vector2DistanceSqr(enemy->simPosition, enemy->movePath[0]) > maxPathDistance2) 231 { 232 for (int j = ENEMY_MAX_PATH_COUNT - 1; j > 0; j--) 233 { 234 enemy->movePath[j] = enemy->movePath[j - 1]; 235 } 236 enemy->movePath[0] = enemy->simPosition; 237 if (++enemy->movePathCount > ENEMY_MAX_PATH_COUNT) 238 { 239 enemy->movePathCount = ENEMY_MAX_PATH_COUNT; 240 } 241 } 242 243 if (waypointPassedCount > 0) 244 { 245 enemy->currentX = enemy->nextX; 246 enemy->currentY = enemy->nextY; 247 if (EnemyGetNextPosition(enemy->currentX, enemy->currentY, &enemy->nextX, &enemy->nextY) && 248 Vector2DistanceSqr(enemy->simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f) 249 { 250 // enemy reached the castle; remove it 251 enemy->enemyType = ENEMY_TYPE_NONE; 252 continue; 253 } 254 } 255 } 256 257 // handle collisions between enemies 258 for (int i = 0; i < enemyCount - 1; i++) 259 { 260 Enemy *enemyA = &enemies[i]; 261 if (enemyA->enemyType == ENEMY_TYPE_NONE) 262 { 263 continue; 264 } 265 for (int j = i + 1; j < enemyCount; j++) 266 { 267 Enemy *enemyB = &enemies[j]; 268 if (enemyB->enemyType == ENEMY_TYPE_NONE) 269 { 270 continue; 271 } 272 float distanceSqr = Vector2DistanceSqr(enemyA->simPosition, enemyB->simPosition); 273 float radiusA = enemyClassConfigs[enemyA->enemyType].radius; 274 float radiusB = enemyClassConfigs[enemyB->enemyType].radius; 275 float radiusSum = radiusA + radiusB; 276 if (distanceSqr < radiusSum * radiusSum && distanceSqr > 0.001f) 277 { 278 // collision 279 float distance = sqrtf(distanceSqr); 280 float overlap = radiusSum - distance; 281 // move the enemies apart, but softly; if we have a clog of enemies, 282 // moving them perfectly apart can cause them to jitter 283 float positionCorrection = overlap / 5.0f; 284 Vector2 direction = (Vector2){ 285 (enemyB->simPosition.x - enemyA->simPosition.x) / distance * positionCorrection, 286 (enemyB->simPosition.y - enemyA->simPosition.y) / distance * positionCorrection}; 287 enemyA->simPosition = Vector2Subtract(enemyA->simPosition, direction); 288 enemyB->simPosition = Vector2Add(enemyB->simPosition, direction); 289 } 290 } 291 } 292 293 // handle collisions between enemies and towers 294 for (int i = 0; i < enemyCount; i++) 295 { 296 Enemy *enemy = &enemies[i]; 297 if (enemy->enemyType == ENEMY_TYPE_NONE) 298 { 299 continue; 300 } 301 enemy->contactTime -= gameTime.deltaTime; 302 if (enemy->contactTime < 0.0f) 303 { 304 enemy->contactTime = 0.0f; 305 } 306 307 float enemyRadius = enemyClassConfigs[enemy->enemyType].radius; 308 // linear search over towers; could be optimized by using path finding tower map, 309 // but for now, we keep it simple 310 for (int j = 0; j < towerCount; j++) 311 { 312 Tower *tower = &towers[j]; 313 if (tower->towerType == TOWER_TYPE_NONE) 314 { 315 continue; 316 } 317 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){tower->x, tower->y}); 318 float combinedRadius = enemyRadius + 0.708; // sqrt(0.5^2 + 0.5^2), corner-center distance of square with side length 1 319 if (distanceSqr > combinedRadius * combinedRadius) 320 { 321 continue; 322 } 323 // potential collision; square / circle intersection 324 float dx = tower->x - enemy->simPosition.x; 325 float dy = tower->y - enemy->simPosition.y; 326 float absDx = fabsf(dx); 327 float absDy = fabsf(dy); 328 Vector3 contactPoint = {0}; 329 if (absDx <= 0.5f && absDx <= absDy) { 330 // vertical collision; push the enemy out horizontally 331 float overlap = enemyRadius + 0.5f - absDy; 332 if (overlap < 0.0f) 333 { 334 continue; 335 } 336 float direction = dy > 0.0f ? -1.0f : 1.0f; 337 enemy->simPosition.y += direction * overlap; 338 contactPoint = (Vector3){enemy->simPosition.x, 0.2f, tower->y + direction * 0.5f}; 339 } 340 else if (absDy <= 0.5f && absDy <= absDx) 341 { 342 // horizontal collision; push the enemy out vertically 343 float overlap = enemyRadius + 0.5f - absDx; 344 if (overlap < 0.0f) 345 { 346 continue; 347 } 348 float direction = dx > 0.0f ? -1.0f : 1.0f; 349 enemy->simPosition.x += direction * overlap; 350 contactPoint = (Vector3){tower->x + direction * 0.5f, 0.2f, enemy->simPosition.y}; 351 } 352 else 353 { 354 // possible collision with a corner 355 float cornerDX = dx > 0.0f ? -0.5f : 0.5f; 356 float cornerDY = dy > 0.0f ? -0.5f : 0.5f; 357 float cornerX = tower->x + cornerDX; 358 float cornerY = tower->y + cornerDY; 359 float cornerDistanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){cornerX, cornerY}); 360 if (cornerDistanceSqr > enemyRadius * enemyRadius) 361 { 362 continue; 363 } 364 // push the enemy out along the diagonal 365 float cornerDistance = sqrtf(cornerDistanceSqr); 366 float overlap = enemyRadius - cornerDistance; 367 float directionX = cornerDistance > 0.0f ? (cornerX - enemy->simPosition.x) / cornerDistance : -cornerDX; 368 float directionY = cornerDistance > 0.0f ? (cornerY - enemy->simPosition.y) / cornerDistance : -cornerDY; 369 enemy->simPosition.x -= directionX * overlap; 370 enemy->simPosition.y -= directionY * overlap; 371 contactPoint = (Vector3){cornerX, 0.2f, cornerY}; 372 } 373 374 if (enemyClassConfigs[enemy->enemyType].explosionDamage > 0.0f) 375 { 376 enemy->contactTime += gameTime.deltaTime * 2.0f; // * 2 to undo the subtraction above 377 if (enemy->contactTime >= enemyClassConfigs[enemy->enemyType].requiredContactTime) 378 { 379 EnemyTriggerExplode(enemy, tower, contactPoint); 380 } 381 } 382 } 383 } 384 } 385 386 EnemyId EnemyGetId(Enemy *enemy) 387 { 388 return (EnemyId){enemy - enemies, enemy->generation}; 389 } 390 391 Enemy *EnemyTryResolve(EnemyId enemyId) 392 { 393 if (enemyId.index >= ENEMY_MAX_COUNT) 394 { 395 return 0; 396 } 397 Enemy *enemy = &enemies[enemyId.index]; 398 if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE) 399 { 400 return 0; 401 } 402 return enemy; 403 } 404 405 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY) 406 { 407 Enemy *spawn = 0; 408 for (int i = 0; i < enemyCount; i++) 409 { 410 Enemy *enemy = &enemies[i]; 411 if (enemy->enemyType == ENEMY_TYPE_NONE) 412 { 413 spawn = enemy; 414 break; 415 } 416 } 417 418 if (enemyCount < ENEMY_MAX_COUNT && !spawn) 419 { 420 spawn = &enemies[enemyCount++]; 421 } 422 423 if (spawn) 424 { 425 spawn->currentX = currentX; 426 spawn->currentY = currentY; 427 spawn->nextX = currentX; 428 spawn->nextY = currentY; 429 spawn->simPosition = (Vector2){currentX, currentY}; 430 spawn->simVelocity = (Vector2){0, 0}; 431 spawn->enemyType = enemyType; 432 spawn->startMovingTime = gameTime.time; 433 spawn->damage = 0.0f; 434 spawn->futureDamage = 0.0f; 435 spawn->generation++; 436 spawn->movePathCount = 0; 437 spawn->walkedDistance = 0.0f; 438 } 439 440 return spawn; 441 } 442 443 int EnemyAddDamageRange(Vector2 position, float range, float damage) 444 { 445 int count = 0; 446 float range2 = range * range; 447 for (int i = 0; i < enemyCount; i++) 448 { 449 Enemy *enemy = &enemies[i]; 450 if (enemy->enemyType == ENEMY_TYPE_NONE) 451 { 452 continue; 453 } 454 float distance2 = Vector2DistanceSqr(position, enemy->simPosition); 455 if (distance2 <= range2) 456 { 457 EnemyAddDamage(enemy, damage); 458 count++; 459 } 460 } 461 return count; 462 } 463 464 int EnemyAddDamage(Enemy *enemy, float damage) 465 { 466 enemy->damage += damage; 467 if (enemy->damage >= EnemyGetMaxHealth(enemy)) 468 { 469 currentLevel->playerGold += enemyClassConfigs[enemy->enemyType].goldValue; 470 enemy->enemyType = ENEMY_TYPE_NONE; 471 return 1; 472 } 473 474 return 0; 475 } 476 477 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range) 478 { 479 int16_t castleX = 0; 480 int16_t castleY = 0; 481 Enemy* closest = 0; 482 int16_t closestDistance = 0; 483 float range2 = range * range; 484 for (int i = 0; i < enemyCount; i++) 485 { 486 Enemy* enemy = &enemies[i]; 487 if (enemy->enemyType == ENEMY_TYPE_NONE) 488 { 489 continue; 490 } 491 float maxHealth = EnemyGetMaxHealth(enemy); 492 if (enemy->futureDamage >= maxHealth) 493 { 494 // ignore enemies that will die soon 495 continue; 496 } 497 int16_t dx = castleX - enemy->currentX; 498 int16_t dy = castleY - enemy->currentY; 499 int16_t distance = abs(dx) + abs(dy); 500 if (!closest || distance < closestDistance) 501 { 502 float tdx = towerX - enemy->currentX; 503 float tdy = towerY - enemy->currentY; 504 float tdistance2 = tdx * tdx + tdy * tdy; 505 if (tdistance2 <= range2) 506 { 507 closest = enemy; 508 closestDistance = distance; 509 } 510 } 511 } 512 return closest; 513 } 514 515 int EnemyCount() 516 { 517 int count = 0; 518 for (int i = 0; i < enemyCount; i++) 519 { 520 if (enemies[i].enemyType != ENEMY_TYPE_NONE) 521 { 522 count++; 523 } 524 } 525 return count; 526 } 527 528 void EnemyDrawHealthbars(Camera3D camera) 529 { 530 for (int i = 0; i < enemyCount; i++) 531 { 532 Enemy *enemy = &enemies[i]; 533 if (enemy->enemyType == ENEMY_TYPE_NONE || enemy->damage == 0.0f) 534 { 535 continue; 536 } 537 Vector3 position = (Vector3){enemy->simPosition.x, 0.5f, enemy->simPosition.y}; 538 float maxHealth = EnemyGetMaxHealth(enemy); 539 float health = maxHealth - enemy->damage; 540 float healthRatio = health / maxHealth; 541 542 DrawHealthBar(camera, position, healthRatio, GREEN, 15.0f); 543 } 544 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 
  4 static Projectile projectiles[PROJECTILE_MAX_COUNT];
  5 static int projectileCount = 0;
  6 
  7 typedef struct ProjectileConfig
  8 {
  9   float arcFactor;
 10   Color color;
 11   Color trailColor;
 12 } ProjectileConfig;
 13 
 14 ProjectileConfig projectileConfigs[] = {
 15     [PROJECTILE_TYPE_ARROW] = {
 16         .arcFactor = 0.15f,
 17         .color = RED,
 18         .trailColor = BROWN,
 19     },
 20     [PROJECTILE_TYPE_CATAPULT] = {
 21         .arcFactor = 0.5f,
 22         .color = RED,
 23         .trailColor = GRAY,
 24     },
 25     [PROJECTILE_TYPE_BALLISTA] = {
 26         .arcFactor = 0.025f,
 27         .color = RED,
 28         .trailColor = BROWN,
 29     },
 30 };
 31 
 32 void ProjectileInit()
 33 {
 34   for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
 35   {
 36     projectiles[i] = (Projectile){0};
 37   }
 38 }
 39 
 40 void ProjectileDraw()
 41 {
 42   for (int i = 0; i < projectileCount; i++)
 43   {
 44     Projectile projectile = projectiles[i];
 45     if (projectile.projectileType == PROJECTILE_TYPE_NONE)
 46     {
 47       continue;
 48     }
 49     float transition = (gameTime.time - projectile.shootTime) / (projectile.arrivalTime - projectile.shootTime);
 50     if (transition >= 1.0f)
 51     {
 52       continue;
 53     }
 54 
 55     ProjectileConfig config = projectileConfigs[projectile.projectileType];
 56     for (float transitionOffset = 0.0f; transitionOffset < 1.0f; transitionOffset += 0.1f)
 57     {
 58       float t = transition + transitionOffset * 0.3f;
 59       if (t > 1.0f)
 60       {
 61         break;
 62       }
 63       Vector3 position = Vector3Lerp(projectile.position, projectile.target, t);
 64       Color color = config.color;
 65       color = ColorLerp(config.trailColor, config.color, transitionOffset * transitionOffset);
 66       // fake a ballista flight path using parabola equation
 67       float parabolaT = t - 0.5f;
 68       parabolaT = 1.0f - 4.0f * parabolaT * parabolaT;
 69       position.y += config.arcFactor * parabolaT * projectile.distance;
 70       
 71       float size = 0.06f * (transitionOffset + 0.25f);
 72       DrawCube(position, size, size, size, color);
 73     }
 74   }
 75 }
 76 
 77 void ProjectileUpdate()
 78 {
 79   for (int i = 0; i < projectileCount; i++)
 80   {
 81     Projectile *projectile = &projectiles[i];
 82     if (projectile->projectileType == PROJECTILE_TYPE_NONE)
 83     {
 84       continue;
 85     }
 86     float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime);
 87     if (transition >= 1.0f)
 88     {
 89       projectile->projectileType = PROJECTILE_TYPE_NONE;
 90       Enemy *enemy = EnemyTryResolve(projectile->targetEnemy);
 91       if (enemy && projectile->hitEffectConfig.pushbackPowerDistance > 0.0f)
 92       {
 93           Vector2 direction = Vector2Normalize(Vector2Subtract((Vector2){projectile->target.x, projectile->target.z}, enemy->simPosition));
 94           enemy->simPosition = Vector2Add(enemy->simPosition, Vector2Scale(direction, projectile->hitEffectConfig.pushbackPowerDistance));
 95       }
 96       
 97       if (projectile->hitEffectConfig.areaDamageRadius > 0.0f)
 98       {
 99         EnemyAddDamageRange((Vector2){projectile->target.x, projectile->target.z}, projectile->hitEffectConfig.areaDamageRadius, projectile->hitEffectConfig.damage);
100         // pancaked sphere explosion
101         float r = projectile->hitEffectConfig.areaDamageRadius;
102         ParticleAdd(PARTICLE_TYPE_EXPLOSION, projectile->target, (Vector3){0}, (Vector3){r, r * 0.2f, r}, 0.33f);
103       }
104       else if (projectile->hitEffectConfig.damage > 0.0f && enemy)
105       {
106         EnemyAddDamage(enemy, projectile->hitEffectConfig.damage);
107       }
108       continue;
109     }
110   }
111 }
112 
113 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig)
114 {
115   for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
116   {
117     Projectile *projectile = &projectiles[i];
118     if (projectile->projectileType == PROJECTILE_TYPE_NONE)
119     {
120       projectile->projectileType = projectileType;
121       projectile->shootTime = gameTime.time;
122       float distance = Vector3Distance(position, target);
123       projectile->arrivalTime = gameTime.time + distance / speed;
124       projectile->position = position;
125       projectile->target = target;
126       projectile->directionNormal = Vector3Scale(Vector3Subtract(target, position), 1.0f / distance);
127       projectile->distance = distance;
128       projectile->targetEnemy = EnemyGetId(enemy);
129       projectileCount = projectileCount <= i ? i + 1 : projectileCount;
130       projectile->hitEffectConfig = hitEffectConfig;
131       return projectile;
132     }
133   }
134   return 0;
135 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 #include <rlgl.h>
  4 
  5 static Particle particles[PARTICLE_MAX_COUNT];
  6 static int particleCount = 0;
  7 
  8 void ParticleInit()
  9 {
 10   for (int i = 0; i < PARTICLE_MAX_COUNT; i++)
 11   {
 12     particles[i] = (Particle){0};
 13   }
 14   particleCount = 0;
 15 }
 16 
 17 static void DrawExplosionParticle(Particle *particle, float transition)
 18 {
 19   Vector3 scale = particle->scale;
 20   float size = 1.0f * (1.0f - transition);
 21   Color startColor = WHITE;
 22   Color endColor = RED;
 23   Color color = ColorLerp(startColor, endColor, transition);
 24 
 25   rlPushMatrix();
 26   rlTranslatef(particle->position.x, particle->position.y, particle->position.z);
 27   rlScalef(scale.x, scale.y, scale.z);
 28   DrawSphere(Vector3Zero(), size, color);
 29   rlPopMatrix();
 30 }
 31 
 32 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime)
 33 {
 34   if (particleCount >= PARTICLE_MAX_COUNT)
 35   {
 36     return;
 37   }
 38 
 39   int index = -1;
 40   for (int i = 0; i < particleCount; i++)
 41   {
 42     if (particles[i].particleType == PARTICLE_TYPE_NONE)
 43     {
 44       index = i;
 45       break;
 46     }
 47   }
 48 
 49   if (index == -1)
 50   {
 51     index = particleCount++;
 52   }
 53 
 54   Particle *particle = &particles[index];
 55   particle->particleType = particleType;
 56   particle->spawnTime = gameTime.time;
 57   particle->lifetime = lifetime;
 58   particle->position = position;
 59   particle->velocity = velocity;
 60   particle->scale = scale;
 61 }
 62 
 63 void ParticleUpdate()
 64 {
 65   for (int i = 0; i < particleCount; i++)
 66   {
 67     Particle *particle = &particles[i];
 68     if (particle->particleType == PARTICLE_TYPE_NONE)
 69     {
 70       continue;
 71     }
 72 
 73     float age = gameTime.time - particle->spawnTime;
 74 
 75     if (particle->lifetime > age)
 76     {
 77       particle->position = Vector3Add(particle->position, Vector3Scale(particle->velocity, gameTime.deltaTime));
 78     }
 79     else {
 80       particle->particleType = PARTICLE_TYPE_NONE;
 81     }
 82   }
 83 }
 84 
 85 void ParticleDraw()
 86 {
 87   for (int i = 0; i < particleCount; i++)
 88   {
 89     Particle particle = particles[i];
 90     if (particle.particleType == PARTICLE_TYPE_NONE)
 91     {
 92       continue;
 93     }
 94 
 95     float age = gameTime.time - particle.spawnTime;
 96     float transition = age / particle.lifetime;
 97     switch (particle.particleType)
 98     {
 99     case PARTICLE_TYPE_EXPLOSION:
100       DrawExplosionParticle(&particle, transition);
101       break;
102     default:
103       DrawCube(particle.position, 0.3f, 0.5f, 0.3f, RED);
104       break;
105     }
106   }
107 }
  1 #include "raylib.h"
  2 #include "preferred_size.h"
  3 
  4 // Since the canvas size is not known at compile time, we need to query it at runtime;
  5 // the following platform specific code obtains the canvas size and we will use this
  6 // size as the preferred size for the window at init time. We're ignoring here the
  7 // possibility of the canvas size changing during runtime - this would require to
  8 // poll the canvas size in the game loop or establishing a callback to be notified
  9 
 10 #ifdef PLATFORM_WEB
 11 #include <emscripten.h>
 12 EMSCRIPTEN_RESULT emscripten_get_element_css_size(const char *target, double *width, double *height);
 13 
 14 void GetPreferredSize(int *screenWidth, int *screenHeight)
 15 {
 16   double canvasWidth, canvasHeight;
 17   emscripten_get_element_css_size("#" CANVAS_NAME, &canvasWidth, &canvasHeight);
 18   *screenWidth = (int)canvasWidth;
 19   *screenHeight = (int)canvasHeight;
 20   TraceLog(LOG_INFO, "preferred size for %s: %d %d", CANVAS_NAME, *screenWidth, *screenHeight);
 21 }
 22 
 23 int IsPaused()
 24 {
 25   const char *js = "(function(){\n"
 26   "  var canvas = document.getElementById(\"" CANVAS_NAME "\");\n"
 27   "  var rect = canvas.getBoundingClientRect();\n"
 28   "  var isVisible = (\n"
 29   "    rect.top >= 0 &&\n"
 30   "    rect.left >= 0 &&\n"
 31   "    rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&\n"
 32   "    rect.right <= (window.innerWidth || document.documentElement.clientWidth)\n"
 33   "  );\n"
 34   "  return isVisible ? 0 : 1;\n"
 35   "})()";
 36   return emscripten_run_script_int(js);
 37 }
 38 
 39 #else
 40 void GetPreferredSize(int *screenWidth, int *screenHeight)
 41 {
 42   *screenWidth = 600;
 43   *screenHeight = 240;
 44 }
 45 int IsPaused()
 46 {
 47   return 0;
 48 }
 49 #endif
  1 #ifndef PREFERRED_SIZE_H
  2 #define PREFERRED_SIZE_H
  3 
  4 void GetPreferredSize(int *screenWidth, int *screenHeight);
  5 int IsPaused();
  6 
  7 #endif
Place some towers to see how the path changes.
Animated enemy path preview
Here's an example of how the path preview looks like after placing a number of towers.

I find this very satisfying to watch. It's quite simple, yet very effective. The animation is done by changing the start time offset for the dummy enemy that is used to preview the path.

One annoying thing now is, that the path preview can be blocked by the towers. A popular solution to this is to render the path preview after the towers are rendered and then drawing it twice: Once with depth testing enabled and once with depth testing disabled but where the coloring is a bit darker. Let's try this out:

  • 💾
  1 #include "td_main.h"
  2 #include <raymath.h>
3 #include <rlgl.h>
4 #include <stdlib.h> 5 #include <math.h> 6 7 //# Variables 8 GUIState guiState = {0}; 9 GameTime gameTime = {0}; 10 11 Model floorTileAModel = {0}; 12 Model floorTileBModel = {0}; 13 Model treeModel[2] = {0}; 14 Model firTreeModel[2] = {0}; 15 Model rockModels[5] = {0}; 16 Model grassPatchModel[1] = {0}; 17 18 Model pathArrowModel = {0}; 19 20 Texture2D palette, spriteSheet; 21 22 Level levels[] = { 23 [0] = { 24 .state = LEVEL_STATE_BUILDING, 25 .initialGold = 20, 26 .waves[0] = { 27 .enemyType = ENEMY_TYPE_MINION, 28 .wave = 0, 29 .count = 10, 30 .interval = 2.5f, 31 .delay = 1.0f, 32 .spawnPosition = {0, 6}, 33 }, 34 .waves[1] = { 35 .enemyType = ENEMY_TYPE_MINION, 36 .wave = 1, 37 .count = 20, 38 .interval = 1.5f, 39 .delay = 1.0f, 40 .spawnPosition = {0, 6}, 41 }, 42 .waves[2] = { 43 .enemyType = ENEMY_TYPE_MINION, 44 .wave = 2, 45 .count = 30, 46 .interval = 1.2f, 47 .delay = 1.0f, 48 .spawnPosition = {0, 6}, 49 } 50 }, 51 }; 52 53 Level *currentLevel = levels; 54 55 //# Game 56 57 static Model LoadGLBModel(char *filename) 58 { 59 Model model = LoadModel(TextFormat("data/%s.glb",filename)); 60 for (int i = 0; i < model.materialCount; i++) 61 { 62 model.materials[i].maps[MATERIAL_MAP_DIFFUSE].texture = palette; 63 } 64 return model; 65 } 66 67 void LoadAssets() 68 { 69 // load a sprite sheet that contains all units 70 spriteSheet = LoadTexture("data/spritesheet.png"); 71 SetTextureFilter(spriteSheet, TEXTURE_FILTER_BILINEAR); 72 73 // we'll use a palette texture to colorize the all buildings and environment art 74 palette = LoadTexture("data/palette.png"); 75 // The texture uses gradients on very small space, so we'll enable bilinear filtering 76 SetTextureFilter(palette, TEXTURE_FILTER_BILINEAR); 77 78 floorTileAModel = LoadGLBModel("floor-tile-a"); 79 floorTileBModel = LoadGLBModel("floor-tile-b"); 80 treeModel[0] = LoadGLBModel("leaftree-large-1-a"); 81 treeModel[1] = LoadGLBModel("leaftree-large-1-b"); 82 firTreeModel[0] = LoadGLBModel("firtree-1-a"); 83 firTreeModel[1] = LoadGLBModel("firtree-1-b"); 84 rockModels[0] = LoadGLBModel("rock-1"); 85 rockModels[1] = LoadGLBModel("rock-2"); 86 rockModels[2] = LoadGLBModel("rock-3"); 87 rockModels[3] = LoadGLBModel("rock-4"); 88 rockModels[4] = LoadGLBModel("rock-5"); 89 grassPatchModel[0] = LoadGLBModel("grass-patch-1"); 90 91 pathArrowModel = LoadGLBModel("direction-arrow-x"); 92 } 93 94 void InitLevel(Level *level) 95 { 96 level->seed = (int)(GetTime() * 100.0f); 97 98 TowerInit(); 99 EnemyInit(); 100 ProjectileInit(); 101 ParticleInit(); 102 TowerTryAdd(TOWER_TYPE_BASE, 0, 0); 103 104 level->placementMode = 0; 105 level->state = LEVEL_STATE_BUILDING; 106 level->nextState = LEVEL_STATE_NONE; 107 level->playerGold = level->initialGold; 108 level->currentWave = 0; 109 110 Camera *camera = &level->camera; 111 camera->position = (Vector3){4.0f, 8.0f, 8.0f}; 112 camera->target = (Vector3){0.0f, 0.0f, 0.0f}; 113 camera->up = (Vector3){0.0f, 1.0f, 0.0f}; 114 camera->fovy = 10.0f; 115 camera->projection = CAMERA_ORTHOGRAPHIC; 116 } 117 118 void DrawLevelHud(Level *level) 119 { 120 const char *text = TextFormat("Gold: %d", level->playerGold); 121 Font font = GetFontDefault(); 122 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 120, 10}, font.baseSize * 2.0f, 2.0f, BLACK); 123 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 122, 8}, font.baseSize * 2.0f, 2.0f, YELLOW); 124 } 125 126 void DrawLevelReportLostWave(Level *level) 127 { 128 BeginMode3D(level->camera); 129 DrawLevelGround(level); 130 TowerDraw(); 131 EnemyDraw(); 132 ProjectileDraw(); 133 ParticleDraw(); 134 guiState.isBlocked = 0; 135 EndMode3D(); 136 137 TowerDrawHealthBars(level->camera); 138 139 const char *text = "Wave lost"; 140 int textWidth = MeasureText(text, 20); 141 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 142 143 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 144 { 145 level->nextState = LEVEL_STATE_RESET; 146 } 147 } 148 149 int HasLevelNextWave(Level *level) 150 { 151 for (int i = 0; i < 10; i++) 152 { 153 EnemyWave *wave = &level->waves[i]; 154 if (wave->wave == level->currentWave) 155 { 156 return 1; 157 } 158 } 159 return 0; 160 } 161 162 void DrawLevelReportWonWave(Level *level) 163 { 164 BeginMode3D(level->camera); 165 DrawLevelGround(level); 166 TowerDraw(); 167 EnemyDraw(); 168 ProjectileDraw(); 169 ParticleDraw(); 170 guiState.isBlocked = 0; 171 EndMode3D(); 172 173 TowerDrawHealthBars(level->camera); 174 175 const char *text = "Wave won"; 176 int textWidth = MeasureText(text, 20); 177 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 178 179 180 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 181 { 182 level->nextState = LEVEL_STATE_RESET; 183 } 184 185 if (HasLevelNextWave(level)) 186 { 187 if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0)) 188 { 189 level->nextState = LEVEL_STATE_BUILDING; 190 } 191 } 192 else { 193 if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0)) 194 { 195 level->nextState = LEVEL_STATE_WON_LEVEL; 196 } 197 } 198 } 199 200 void DrawBuildingBuildButton(Level *level, int x, int y, int width, int height, uint8_t towerType, const char *name) 201 { 202 static ButtonState buttonStates[8] = {0}; 203 int cost = GetTowerCosts(towerType); 204 const char *text = TextFormat("%s: %d", name, cost); 205 buttonStates[towerType].isSelected = level->placementMode == towerType; 206 buttonStates[towerType].isDisabled = level->playerGold < cost; 207 if (Button(text, x, y, width, height, &buttonStates[towerType])) 208 { 209 level->placementMode = buttonStates[towerType].isSelected ? 0 : towerType; 210 } 211 } 212 213 float GetRandomFloat(float min, float max) 214 { 215 int random = GetRandomValue(0, 0xfffffff); 216 return ((float)random / (float)0xfffffff) * (max - min) + min; 217 } 218 219 void DrawLevelGround(Level *level) 220 { 221 // draw checkerboard ground pattern 222 for (int x = -5; x <= 5; x += 1) 223 { 224 for (int y = -5; y <= 5; y += 1) 225 { 226 Model *model = (x + y) % 2 == 0 ? &floorTileAModel : &floorTileBModel; 227 DrawModel(*model, (Vector3){x, 0.0f, y}, 1.0f, WHITE); 228 } 229 } 230 231 int oldSeed = GetRandomValue(0, 0xfffffff); 232 SetRandomSeed(level->seed); 233 // increase probability for trees via duplicated entries 234 Model borderModels[64]; 235 int maxRockCount = GetRandomValue(2, 6); 236 int maxTreeCount = GetRandomValue(10, 20); 237 int maxFirTreeCount = GetRandomValue(5, 10); 238 int maxLeafTreeCount = maxTreeCount - maxFirTreeCount; 239 int grassPatchCount = GetRandomValue(5, 30); 240 241 int modelCount = 0; 242 for (int i = 0; i < maxRockCount && modelCount < 63; i++) 243 { 244 borderModels[modelCount++] = rockModels[GetRandomValue(0, 5)]; 245 } 246 for (int i = 0; i < maxLeafTreeCount && modelCount < 63; i++) 247 { 248 borderModels[modelCount++] = treeModel[GetRandomValue(0, 1)]; 249 } 250 for (int i = 0; i < maxFirTreeCount && modelCount < 63; i++) 251 { 252 borderModels[modelCount++] = firTreeModel[GetRandomValue(0, 1)]; 253 } 254 for (int i = 0; i < grassPatchCount && modelCount < 63; i++) 255 { 256 borderModels[modelCount++] = grassPatchModel[0]; 257 } 258 259 // draw some objects around the border of the map 260 Vector3 up = {0, 1, 0}; 261 // a pseudo random number generator to get the same result every time 262 const float wiggle = 0.75f; 263 const int layerCount = 3; 264 for (int layer = 0; layer < layerCount; layer++) 265 { 266 int layerPos = 6 + layer; 267 for (int x = -6 + layer; x <= 6 + layer; x += 1) 268 { 269 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)], 270 (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, -layerPos + GetRandomFloat(0.0f, wiggle)}, 271 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE); 272 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)], 273 (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, layerPos + GetRandomFloat(0.0f, wiggle)}, 274 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE); 275 } 276 277 for (int z = -5 + layer; z <= 5 + layer; z += 1) 278 { 279 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)], 280 (Vector3){-layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)}, 281 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE); 282 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)], 283 (Vector3){layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)}, 284 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE); 285 } 286 } 287 288 SetRandomSeed(oldSeed); 289 } 290
291 void DrawEnemyPath(Level *level, Color arrowColor)
292 { 293 const int castleX = 0, castleY = 0; 294 const int maxWaypointCount = 200; 295 const float timeStep = 1.0f; 296 Vector3 arrowScale = {0.75f, 0.75f, 0.75f}; 297 298 // we start with a time offset to simulate the path, 299 // this way the arrows are animated in a forward moving direction 300 // The time is wrapped around the time step to get a smooth animation 301 float timeOffset = fmodf(GetTime(), timeStep); 302 303 for (int i = 0; i < ENEMY_MAX_WAVE_COUNT; i++) 304 { 305 EnemyWave *wave = &level->waves[i]; 306 if (wave->wave != level->currentWave) 307 { 308 continue; 309 } 310 311 // use this dummy enemy to simulate the path 312 Enemy dummy = { 313 .enemyType = ENEMY_TYPE_MINION, 314 .simPosition = (Vector2){wave->spawnPosition.x, wave->spawnPosition.y}, 315 .nextX = wave->spawnPosition.x, 316 .nextY = wave->spawnPosition.y, 317 .currentX = wave->spawnPosition.x, 318 .currentY = wave->spawnPosition.y, 319 }; 320 321 float deltaTime = timeOffset; 322 for (int j = 0; j < maxWaypointCount; j++) 323 { 324 int waypointPassedCount = 0; 325 Vector2 pos = EnemyGetPosition(&dummy, deltaTime, &dummy.simVelocity, &waypointPassedCount); 326 // after the initial variable starting offset, we use a fixed time step 327 deltaTime = timeStep; 328 dummy.simPosition = pos; 329 330 // Update the dummy's position just like we do in the regular enemy update loop 331 for (int k = 0; k < waypointPassedCount; k++) 332 { 333 dummy.currentX = dummy.nextX; 334 dummy.currentY = dummy.nextY; 335 if (EnemyGetNextPosition(dummy.currentX, dummy.currentY, &dummy.nextX, &dummy.nextY) && 336 Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f) 337 { 338 break; 339 } 340 } 341 if (Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f) 342 { 343 break; 344 } 345 346 // get the angle we need to rotate the arrow model. The velocity is just fine for this. 347 float angle = atan2f(dummy.simVelocity.x, dummy.simVelocity.y) * RAD2DEG - 90.0f;
348 DrawModelEx(pathArrowModel, (Vector3){pos.x, 0.15f, pos.y}, (Vector3){0, 1, 0}, angle, arrowScale, arrowColor);
349 } 350 } 351 } 352 353 void DrawLevelBuildingState(Level *level) 354 { 355 BeginMode3D(level->camera); 356 DrawLevelGround(level); 357 358 int blockedCellCount = 0; 359 Vector2 blockedCells[1]; 360 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera); 361 float planeDistance = ray.position.y / -ray.direction.y; 362 float planeX = ray.direction.x * planeDistance + ray.position.x; 363 float planeY = ray.direction.z * planeDistance + ray.position.z; 364 int16_t mapX = (int16_t)floorf(planeX + 0.5f); 365 int16_t mapY = (int16_t)floorf(planeY + 0.5f); 366 if (level->placementMode && !guiState.isBlocked && mapX >= -5 && mapX <= 5 && mapY >= -5 && mapY <= 5) 367 { 368 DrawCubeWires((Vector3){mapX, 0.2f, mapY}, 1.0f, 0.4f, 1.0f, RED); 369 blockedCells[blockedCellCount++] = (Vector2){mapX, mapY}; 370 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) 371 { 372 if (TowerTryAdd(level->placementMode, mapX, mapY)) 373 { 374 level->playerGold -= GetTowerCosts(level->placementMode); 375 level->placementMode = TOWER_TYPE_NONE; 376 } 377 } 378 }
379 380 PathFindingMapUpdate(blockedCellCount, blockedCells);
381 TowerDraw(); 382 EnemyDraw();
383 ProjectileDraw(); 384 ParticleDraw(); 385 386 // disable depth testing for the path arrows 387 // flush the 3D batch to draw the arrows on top of everything 388 rlDrawRenderBatchActive(); 389 rlDisableDepthTest(); 390 DrawEnemyPath(level, (Color){64, 64, 64, 160}); 391 392 rlDrawRenderBatchActive(); 393 rlEnableDepthTest();
394 DrawEnemyPath(level, WHITE); 395 396 guiState.isBlocked = 0; 397 398 EndMode3D(); 399 400 TowerDrawHealthBars(level->camera); 401 402 static ButtonState buildWallButtonState = {0}; 403 static ButtonState buildGunButtonState = {0}; 404 buildWallButtonState.isSelected = level->placementMode == TOWER_TYPE_WALL; 405 buildGunButtonState.isSelected = level->placementMode == TOWER_TYPE_ARCHER; 406 407 DrawBuildingBuildButton(level, 10, 10, 110, 30, TOWER_TYPE_WALL, "Wall"); 408 DrawBuildingBuildButton(level, 10, 50, 110, 30, TOWER_TYPE_ARCHER, "Archer"); 409 DrawBuildingBuildButton(level, 10, 90, 110, 30, TOWER_TYPE_BALLISTA, "Ballista"); 410 DrawBuildingBuildButton(level, 10, 130, 110, 30, TOWER_TYPE_CATAPULT, "Catapult"); 411 412 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 413 { 414 level->nextState = LEVEL_STATE_RESET; 415 } 416 417 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0)) 418 { 419 level->nextState = LEVEL_STATE_BATTLE; 420 } 421 422 const char *text = "Building phase"; 423 int textWidth = MeasureText(text, 20); 424 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 425 } 426 427 void InitBattleStateConditions(Level *level) 428 { 429 level->state = LEVEL_STATE_BATTLE; 430 level->nextState = LEVEL_STATE_NONE; 431 level->waveEndTimer = 0.0f; 432 for (int i = 0; i < 10; i++) 433 { 434 EnemyWave *wave = &level->waves[i]; 435 wave->spawned = 0; 436 wave->timeToSpawnNext = wave->delay; 437 } 438 } 439 440 void DrawLevelBattleState(Level *level) 441 { 442 BeginMode3D(level->camera); 443 DrawLevelGround(level); 444 TowerDraw(); 445 EnemyDraw(); 446 ProjectileDraw(); 447 ParticleDraw(); 448 guiState.isBlocked = 0; 449 EndMode3D(); 450 451 EnemyDrawHealthbars(level->camera); 452 TowerDrawHealthBars(level->camera); 453 454 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 455 { 456 level->nextState = LEVEL_STATE_RESET; 457 } 458 459 int maxCount = 0; 460 int remainingCount = 0; 461 for (int i = 0; i < 10; i++) 462 { 463 EnemyWave *wave = &level->waves[i]; 464 if (wave->wave != level->currentWave) 465 { 466 continue; 467 } 468 maxCount += wave->count; 469 remainingCount += wave->count - wave->spawned; 470 } 471 int aliveCount = EnemyCount(); 472 remainingCount += aliveCount; 473 474 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount); 475 int textWidth = MeasureText(text, 20); 476 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 477 } 478 479 void DrawLevel(Level *level) 480 { 481 switch (level->state) 482 { 483 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break; 484 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break; 485 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break; 486 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break; 487 default: break; 488 } 489 490 DrawLevelHud(level); 491 } 492 493 void UpdateLevel(Level *level) 494 { 495 if (level->state == LEVEL_STATE_BATTLE) 496 { 497 int activeWaves = 0; 498 for (int i = 0; i < 10; i++) 499 { 500 EnemyWave *wave = &level->waves[i]; 501 if (wave->spawned >= wave->count || wave->wave != level->currentWave) 502 { 503 continue; 504 } 505 activeWaves++; 506 wave->timeToSpawnNext -= gameTime.deltaTime; 507 if (wave->timeToSpawnNext <= 0.0f) 508 { 509 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y); 510 if (enemy) 511 { 512 wave->timeToSpawnNext = wave->interval; 513 wave->spawned++; 514 } 515 } 516 } 517 if (GetTowerByType(TOWER_TYPE_BASE) == 0) { 518 level->waveEndTimer += gameTime.deltaTime; 519 if (level->waveEndTimer >= 2.0f) 520 { 521 level->nextState = LEVEL_STATE_LOST_WAVE; 522 } 523 } 524 else if (activeWaves == 0 && EnemyCount() == 0) 525 { 526 level->waveEndTimer += gameTime.deltaTime; 527 if (level->waveEndTimer >= 2.0f) 528 { 529 level->nextState = LEVEL_STATE_WON_WAVE; 530 } 531 } 532 } 533 534 PathFindingMapUpdate(0, 0); 535 EnemyUpdate(); 536 TowerUpdate(); 537 ProjectileUpdate(); 538 ParticleUpdate(); 539 540 if (level->nextState == LEVEL_STATE_RESET) 541 { 542 InitLevel(level); 543 } 544 545 if (level->nextState == LEVEL_STATE_BATTLE) 546 { 547 InitBattleStateConditions(level); 548 } 549 550 if (level->nextState == LEVEL_STATE_WON_WAVE) 551 { 552 level->currentWave++; 553 level->state = LEVEL_STATE_WON_WAVE; 554 } 555 556 if (level->nextState == LEVEL_STATE_LOST_WAVE) 557 { 558 level->state = LEVEL_STATE_LOST_WAVE; 559 } 560 561 if (level->nextState == LEVEL_STATE_BUILDING) 562 { 563 level->state = LEVEL_STATE_BUILDING; 564 } 565 566 if (level->nextState == LEVEL_STATE_WON_LEVEL) 567 { 568 // make something of this later 569 InitLevel(level); 570 } 571 572 level->nextState = LEVEL_STATE_NONE; 573 } 574 575 float nextSpawnTime = 0.0f; 576 577 void ResetGame() 578 { 579 InitLevel(currentLevel); 580 } 581 582 void InitGame() 583 { 584 TowerInit(); 585 EnemyInit(); 586 ProjectileInit(); 587 ParticleInit(); 588 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f); 589 590 currentLevel = levels; 591 InitLevel(currentLevel); 592 } 593 594 //# Immediate GUI functions 595 596 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth) 597 { 598 const float healthBarHeight = 6.0f; 599 const float healthBarOffset = 15.0f; 600 const float inset = 2.0f; 601 const float innerWidth = healthBarWidth - inset * 2; 602 const float innerHeight = healthBarHeight - inset * 2; 603 604 Vector2 screenPos = GetWorldToScreen(position, camera); 605 float centerX = screenPos.x - healthBarWidth * 0.5f; 606 float topY = screenPos.y - healthBarOffset; 607 DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK); 608 float healthWidth = innerWidth * healthRatio; 609 DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor); 610 } 611 612 int Button(const char *text, int x, int y, int width, int height, ButtonState *state) 613 { 614 Rectangle bounds = {x, y, width, height}; 615 int isPressed = 0; 616 int isSelected = state && state->isSelected; 617 int isDisabled = state && state->isDisabled; 618 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !isDisabled) 619 { 620 Color color = isSelected ? DARKGRAY : GRAY; 621 DrawRectangle(x, y, width, height, color); 622 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) 623 { 624 isPressed = 1; 625 } 626 guiState.isBlocked = 1; 627 } 628 else 629 { 630 Color color = isSelected ? WHITE : LIGHTGRAY; 631 DrawRectangle(x, y, width, height, color); 632 } 633 Font font = GetFontDefault(); 634 Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1); 635 Color textColor = isDisabled ? GRAY : BLACK; 636 DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, textColor); 637 return isPressed; 638 } 639 640 //# Main game loop 641 642 void GameUpdate() 643 { 644 float dt = GetFrameTime(); 645 // cap maximum delta time to 0.1 seconds to prevent large time steps 646 if (dt > 0.1f) dt = 0.1f; 647 gameTime.time += dt; 648 gameTime.deltaTime = dt; 649 650 UpdateLevel(currentLevel); 651 } 652 653 int main(void) 654 { 655 int screenWidth, screenHeight; 656 GetPreferredSize(&screenWidth, &screenHeight); 657 InitWindow(screenWidth, screenHeight, "Tower defense"); 658 SetTargetFPS(30); 659 660 LoadAssets(); 661 InitGame(); 662 663 while (!WindowShouldClose()) 664 { 665 if (IsPaused()) { 666 // canvas is not visible in browser - do nothing 667 continue; 668 } 669 670 BeginDrawing(); 671 ClearBackground((Color){0x4E, 0x63, 0x26, 0xFF}); 672 673 GameUpdate(); 674 DrawLevel(currentLevel); 675 676 EndDrawing(); 677 } 678 679 CloseWindow(); 680 681 return 0; 682 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 
  4 // The queue is a simple array of nodes, we add nodes to the end and remove
  5 // nodes from the front. We keep the array around to avoid unnecessary allocations
  6 static PathfindingNode *pathfindingNodeQueue = 0;
  7 static int pathfindingNodeQueueCount = 0;
  8 static int pathfindingNodeQueueCapacity = 0;
  9 
 10 // The pathfinding map stores the distances from the castle to each cell in the map.
 11 static PathfindingMap pathfindingMap = {0};
 12 
 13 void PathfindingMapInit(int width, int height, Vector3 translate, float scale)
 14 {
 15   // transforming between map space and world space allows us to adapt 
 16   // position and scale of the map without changing the pathfinding data
 17   pathfindingMap.toWorldSpace = MatrixTranslate(translate.x, translate.y, translate.z);
 18   pathfindingMap.toWorldSpace = MatrixMultiply(pathfindingMap.toWorldSpace, MatrixScale(scale, scale, scale));
 19   pathfindingMap.toMapSpace = MatrixInvert(pathfindingMap.toWorldSpace);
 20   pathfindingMap.width = width;
 21   pathfindingMap.height = height;
 22   pathfindingMap.scale = scale;
 23   pathfindingMap.distances = (float *)MemAlloc(width * height * sizeof(float));
 24   for (int i = 0; i < width * height; i++)
 25   {
 26     pathfindingMap.distances[i] = -1.0f;
 27   }
 28 
 29   pathfindingMap.towerIndex = (long *)MemAlloc(width * height * sizeof(long));
 30   pathfindingMap.deltaSrc = (DeltaSrc *)MemAlloc(width * height * sizeof(DeltaSrc));
 31 }
 32 
 33 static void PathFindingNodePush(int16_t x, int16_t y, int16_t fromX, int16_t fromY, float distance)
 34 {
 35   if (pathfindingNodeQueueCount >= pathfindingNodeQueueCapacity)
 36   {
 37     pathfindingNodeQueueCapacity = pathfindingNodeQueueCapacity == 0 ? 256 : pathfindingNodeQueueCapacity * 2;
 38     // we use MemAlloc/MemRealloc to allocate memory for the queue
 39     // I am not entirely sure if MemRealloc allows passing a null pointer
 40     // so we check if the pointer is null and use MemAlloc in that case
 41     if (pathfindingNodeQueue == 0)
 42     {
 43       pathfindingNodeQueue = (PathfindingNode *)MemAlloc(pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
 44     }
 45     else
 46     {
 47       pathfindingNodeQueue = (PathfindingNode *)MemRealloc(pathfindingNodeQueue, pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
 48     }
 49   }
 50 
 51   PathfindingNode *node = &pathfindingNodeQueue[pathfindingNodeQueueCount++];
 52   node->x = x;
 53   node->y = y;
 54   node->fromX = fromX;
 55   node->fromY = fromY;
 56   node->distance = distance;
 57 }
 58 
 59 static PathfindingNode *PathFindingNodePop()
 60 {
 61   if (pathfindingNodeQueueCount == 0)
 62   {
 63     return 0;
 64   }
 65   // we return the first node in the queue; we want to return a pointer to the node
 66   // so we can return 0 if the queue is empty. 
 67   // We should _not_ return a pointer to the element in the list, because the list
 68   // may be reallocated and the pointer would become invalid. Or the 
 69   // popped element is overwritten by the next push operation.
 70   // Using static here means that the variable is permanently allocated.
 71   static PathfindingNode node;
 72   node = pathfindingNodeQueue[0];
 73   // we shift all nodes one position to the front
 74   for (int i = 1; i < pathfindingNodeQueueCount; i++)
 75   {
 76     pathfindingNodeQueue[i - 1] = pathfindingNodeQueue[i];
 77   }
 78   --pathfindingNodeQueueCount;
 79   return &node;
 80 }
 81 
 82 float PathFindingGetDistance(int mapX, int mapY)
 83 {
 84   if (mapX < 0 || mapX >= pathfindingMap.width || mapY < 0 || mapY >= pathfindingMap.height)
 85   {
 86     // when outside the map, we return the manhattan distance to the castle (0,0)
 87     return fabsf((float)mapX) + fabsf((float)mapY);
 88   }
 89 
 90   return pathfindingMap.distances[mapY * pathfindingMap.width + mapX];
 91 }
 92 
 93 // transform a world position to a map position in the array; 
 94 // returns true if the position is inside the map
 95 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY)
 96 {
 97   Vector3 mapPosition = Vector3Transform(worldPosition, pathfindingMap.toMapSpace);
 98   *mapX = (int16_t)mapPosition.x;
 99   *mapY = (int16_t)mapPosition.z;
100   return *mapX >= 0 && *mapX < pathfindingMap.width && *mapY >= 0 && *mapY < pathfindingMap.height;
101 }
102 
103 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells)
104 {
105   const int castleX = 0, castleY = 0;
106   int16_t castleMapX, castleMapY;
107   if (!PathFindingFromWorldToMapPosition((Vector3){castleX, 0.0f, castleY}, &castleMapX, &castleMapY))
108   {
109     return;
110   }
111   int width = pathfindingMap.width, height = pathfindingMap.height;
112 
113   // reset the distances to -1
114   for (int i = 0; i < width * height; i++)
115   {
116     pathfindingMap.distances[i] = -1.0f;
117   }
118   // reset the tower indices
119   for (int i = 0; i < width * height; i++)
120   {
121     pathfindingMap.towerIndex[i] = -1;
122   }
123   // reset the delta src
124   for (int i = 0; i < width * height; i++)
125   {
126     pathfindingMap.deltaSrc[i].x = 0;
127     pathfindingMap.deltaSrc[i].y = 0;
128   }
129 
130   for (int i = 0; i < blockedCellCount; i++)
131   {
132     int16_t mapX, mapY;
133     if (!PathFindingFromWorldToMapPosition((Vector3){blockedCells[i].x, 0.0f, blockedCells[i].y}, &mapX, &mapY))
134     {
135       continue;
136     }
137     int index = mapY * width + mapX;
138     pathfindingMap.towerIndex[index] = -2;
139   }
140 
141   for (int i = 0; i < towerCount; i++)
142   {
143     Tower *tower = &towers[i];
144     if (tower->towerType == TOWER_TYPE_NONE || tower->towerType == TOWER_TYPE_BASE)
145     {
146       continue;
147     }
148     int16_t mapX, mapY;
149     // technically, if the tower cell scale is not in sync with the pathfinding map scale,
150     // this would not work correctly and needs to be refined to allow towers covering multiple cells
151     // or having multiple towers in one cell; for simplicity, we assume that the tower covers exactly
152     // one cell. For now.
153     if (!PathFindingFromWorldToMapPosition((Vector3){tower->x, 0.0f, tower->y}, &mapX, &mapY))
154     {
155       continue;
156     }
157     int index = mapY * width + mapX;
158     pathfindingMap.towerIndex[index] = i;
159   }
160 
161   // we start at the castle and add the castle to the queue
162   pathfindingMap.maxDistance = 0.0f;
163   pathfindingNodeQueueCount = 0;
164   PathFindingNodePush(castleMapX, castleMapY, castleMapX, castleMapY, 0.0f);
165   PathfindingNode *node = 0;
166   while ((node = PathFindingNodePop()))
167   {
168     if (node->x < 0 || node->x >= width || node->y < 0 || node->y >= height)
169     {
170       continue;
171     }
172     int index = node->y * width + node->x;
173     if (pathfindingMap.distances[index] >= 0 && pathfindingMap.distances[index] <= node->distance)
174     {
175       continue;
176     }
177 
178     int deltaX = node->x - node->fromX;
179     int deltaY = node->y - node->fromY;
180     // even if the cell is blocked by a tower, we still may want to store the direction
181     // (though this might not be needed, IDK right now)
182     pathfindingMap.deltaSrc[index].x = (char) deltaX;
183     pathfindingMap.deltaSrc[index].y = (char) deltaY;
184 
185     // we skip nodes that are blocked by towers or by the provided blocked cells
186     if (pathfindingMap.towerIndex[index] != -1)
187     {
188       node->distance += 8.0f;
189     }
190     pathfindingMap.distances[index] = node->distance;
191     pathfindingMap.maxDistance = fmaxf(pathfindingMap.maxDistance, node->distance);
192     PathFindingNodePush(node->x, node->y + 1, node->x, node->y, node->distance + 1.0f);
193     PathFindingNodePush(node->x, node->y - 1, node->x, node->y, node->distance + 1.0f);
194     PathFindingNodePush(node->x + 1, node->y, node->x, node->y, node->distance + 1.0f);
195     PathFindingNodePush(node->x - 1, node->y, node->x, node->y, node->distance + 1.0f);
196   }
197 }
198 
199 void PathFindingMapDraw()
200 {
201   float cellSize = pathfindingMap.scale * 0.9f;
202   float highlightDistance = fmodf(GetTime() * 4.0f, pathfindingMap.maxDistance);
203   for (int x = 0; x < pathfindingMap.width; x++)
204   {
205     for (int y = 0; y < pathfindingMap.height; y++)
206     {
207       float distance = pathfindingMap.distances[y * pathfindingMap.width + x];
208       float colorV = distance < 0 ? 0 : fminf(distance / pathfindingMap.maxDistance, 1.0f);
209       Color color = distance < 0 ? BLUE : (Color){fminf(colorV, 1.0f) * 255, 0, 0, 255};
210       Vector3 position = Vector3Transform((Vector3){x, -0.25f, y}, pathfindingMap.toWorldSpace);
211       // animate the distance "wave" to show how the pathfinding algorithm expands
212       // from the castle
213       if (distance + 0.5f > highlightDistance && distance - 0.5f < highlightDistance)
214       {
215         color = BLACK;
216       }
217       DrawCube(position, cellSize, 0.1f, cellSize, color);
218     }
219   }
220 }
221 
222 Vector2 PathFindingGetGradient(Vector3 world)
223 {
224   int16_t mapX, mapY;
225   if (PathFindingFromWorldToMapPosition(world, &mapX, &mapY))
226   {
227     DeltaSrc delta = pathfindingMap.deltaSrc[mapY * pathfindingMap.width + mapX];
228     return (Vector2){(float)-delta.x, (float)-delta.y};
229   }
230   // fallback to a simple gradient calculation
231   float n = PathFindingGetDistance(mapX, mapY - 1);
232   float s = PathFindingGetDistance(mapX, mapY + 1);
233   float w = PathFindingGetDistance(mapX - 1, mapY);
234   float e = PathFindingGetDistance(mapX + 1, mapY);
235   return (Vector2){w - e + 0.25f, n - s + 0.125f};
236 }
  1 #ifndef TD_TUT_2_MAIN_H
  2 #define TD_TUT_2_MAIN_H
  3 
  4 #include <inttypes.h>
  5 
  6 #include "raylib.h"
  7 #include "preferred_size.h"
  8 
  9 //# Declarations
 10 
 11 #define ENEMY_MAX_PATH_COUNT 8
 12 #define ENEMY_MAX_COUNT 400
 13 #define ENEMY_TYPE_NONE 0
 14 #define ENEMY_TYPE_MINION 1
 15 
 16 #define PARTICLE_MAX_COUNT 400
 17 #define PARTICLE_TYPE_NONE 0
 18 #define PARTICLE_TYPE_EXPLOSION 1
 19 
 20 typedef struct Particle
 21 {
 22   uint8_t particleType;
 23   float spawnTime;
 24   float lifetime;
 25   Vector3 position;
 26   Vector3 velocity;
 27   Vector3 scale;
 28 } Particle;
 29 
 30 #define TOWER_MAX_COUNT 400
 31 enum TowerType
 32 {
 33   TOWER_TYPE_NONE,
 34   TOWER_TYPE_BASE,
 35   TOWER_TYPE_ARCHER,
 36   TOWER_TYPE_BALLISTA,
 37   TOWER_TYPE_CATAPULT,
 38   TOWER_TYPE_WALL,
 39   TOWER_TYPE_COUNT
 40 };
 41 
 42 typedef struct HitEffectConfig
 43 {
 44   float damage;
 45   float areaDamageRadius;
 46   float pushbackPowerDistance;
 47 } HitEffectConfig;
 48 
 49 typedef struct TowerTypeConfig
 50 {
 51   float cooldown;
 52   float range;
 53   float projectileSpeed;
 54   
 55   uint8_t cost;
 56   uint8_t projectileType;
 57   uint16_t maxHealth;
 58 
 59   HitEffectConfig hitEffect;
 60 } TowerTypeConfig;
 61 
 62 typedef struct Tower
 63 {
 64   int16_t x, y;
 65   uint8_t towerType;
 66   Vector2 lastTargetPosition;
 67   float cooldown;
 68   float damage;
 69 } Tower;
 70 
 71 typedef struct GameTime
 72 {
 73   float time;
 74   float deltaTime;
 75 } GameTime;
 76 
 77 typedef struct ButtonState {
 78   char isSelected;
 79   char isDisabled;
 80 } ButtonState;
 81 
 82 typedef struct GUIState {
 83   int isBlocked;
 84 } GUIState;
 85 
 86 typedef enum LevelState
 87 {
 88   LEVEL_STATE_NONE,
 89   LEVEL_STATE_BUILDING,
 90   LEVEL_STATE_BATTLE,
 91   LEVEL_STATE_WON_WAVE,
 92   LEVEL_STATE_LOST_WAVE,
 93   LEVEL_STATE_WON_LEVEL,
 94   LEVEL_STATE_RESET,
 95 } LevelState;
 96 
 97 typedef struct EnemyWave {
 98   uint8_t enemyType;
 99   uint8_t wave;
100   uint16_t count;
101   float interval;
102   float delay;
103   Vector2 spawnPosition;
104 
105   uint16_t spawned;
106   float timeToSpawnNext;
107 } EnemyWave;
108 
109 #define ENEMY_MAX_WAVE_COUNT 10
110 
111 typedef struct Level
112 {
113   int seed;
114   LevelState state;
115   LevelState nextState;
116   Camera3D camera;
117   int placementMode;
118 
119   int initialGold;
120   int playerGold;
121 
122   EnemyWave waves[ENEMY_MAX_WAVE_COUNT];
123   int currentWave;
124   float waveEndTimer;
125 } Level;
126 
127 typedef struct DeltaSrc
128 {
129   char x, y;
130 } DeltaSrc;
131 
132 typedef struct PathfindingMap
133 {
134   int width, height;
135   float scale;
136   float *distances;
137   long *towerIndex; 
138   DeltaSrc *deltaSrc;
139   float maxDistance;
140   Matrix toMapSpace;
141   Matrix toWorldSpace;
142 } PathfindingMap;
143 
144 // when we execute the pathfinding algorithm, we need to store the active nodes
145 // in a queue. Each node has a position, a distance from the start, and the
146 // position of the node that we came from.
147 typedef struct PathfindingNode
148 {
149   int16_t x, y, fromX, fromY;
150   float distance;
151 } PathfindingNode;
152 
153 typedef struct EnemyId
154 {
155   uint16_t index;
156   uint16_t generation;
157 } EnemyId;
158 
159 typedef struct EnemyClassConfig
160 {
161   float speed;
162   float health;
163   float radius;
164   float maxAcceleration;
165   float requiredContactTime;
166   float explosionDamage;
167   float explosionRange;
168   float explosionPushbackPower;
169   int goldValue;
170 } EnemyClassConfig;
171 
172 typedef struct Enemy
173 {
174   int16_t currentX, currentY;
175   int16_t nextX, nextY;
176   Vector2 simPosition;
177   Vector2 simVelocity;
178   uint16_t generation;
179   float walkedDistance;
180   float startMovingTime;
181   float damage, futureDamage;
182   float contactTime;
183   uint8_t enemyType;
184   uint8_t movePathCount;
185   Vector2 movePath[ENEMY_MAX_PATH_COUNT];
186 } Enemy;
187 
188 // a unit that uses sprites to be drawn
189 #define SPRITE_UNIT_PHASE_WEAPON_IDLE 0
190 #define SPRITE_UNIT_PHASE_WEAPON_COOLDOWN 1
191 typedef struct SpriteUnit
192 {
193   Rectangle srcRect;
194   Vector2 offset;
195   int frameCount;
196   float frameDuration;
197   Rectangle srcWeaponIdleRect;
198   Vector2 srcWeaponIdleOffset;
199   Rectangle srcWeaponCooldownRect;
200   Vector2 srcWeaponCooldownOffset;
201 } SpriteUnit;
202 
203 #define PROJECTILE_MAX_COUNT 1200
204 #define PROJECTILE_TYPE_NONE 0
205 #define PROJECTILE_TYPE_ARROW 1
206 #define PROJECTILE_TYPE_CATAPULT 2
207 #define PROJECTILE_TYPE_BALLISTA 3
208 
209 typedef struct Projectile
210 {
211   uint8_t projectileType;
212   float shootTime;
213   float arrivalTime;
214   float distance;
215   Vector3 position;
216   Vector3 target;
217   Vector3 directionNormal;
218   EnemyId targetEnemy;
219   HitEffectConfig hitEffectConfig;
220 } Projectile;
221 
222 //# Function declarations
223 float TowerGetMaxHealth(Tower *tower);
224 int Button(const char *text, int x, int y, int width, int height, ButtonState *state);
225 int EnemyAddDamageRange(Vector2 position, float range, float damage);
226 int EnemyAddDamage(Enemy *enemy, float damage);
227 
228 //# Enemy functions
229 void EnemyInit();
230 void EnemyDraw();
231 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource);
232 void EnemyUpdate();
233 float EnemyGetCurrentMaxSpeed(Enemy *enemy);
234 float EnemyGetMaxHealth(Enemy *enemy);
235 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY);
236 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount);
237 EnemyId EnemyGetId(Enemy *enemy);
238 Enemy *EnemyTryResolve(EnemyId enemyId);
239 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY);
240 int EnemyAddDamage(Enemy *enemy, float damage);
241 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range);
242 int EnemyCount();
243 void EnemyDrawHealthbars(Camera3D camera);
244 
245 //# Tower functions
246 void TowerInit();
247 Tower *TowerGetAt(int16_t x, int16_t y);
248 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y);
249 Tower *GetTowerByType(uint8_t towerType);
250 int GetTowerCosts(uint8_t towerType);
251 float TowerGetMaxHealth(Tower *tower);
252 void TowerDraw();
253 void TowerUpdate();
254 void TowerDrawHealthBars(Camera3D camera);
255 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase);
256 
257 //# Particles
258 void ParticleInit();
259 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime);
260 void ParticleUpdate();
261 void ParticleDraw();
262 
263 //# Projectiles
264 void ProjectileInit();
265 void ProjectileDraw();
266 void ProjectileUpdate();
267 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig);
268 
269 //# Pathfinding map
270 void PathfindingMapInit(int width, int height, Vector3 translate, float scale);
271 float PathFindingGetDistance(int mapX, int mapY);
272 Vector2 PathFindingGetGradient(Vector3 world);
273 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY);
274 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells);
275 void PathFindingMapDraw();
276 
277 //# UI
278 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth);
279 
280 //# Level
281 void DrawLevelGround(Level *level);
282 void DrawEnemyPath(Level *level, Color arrowColor);
283 284 //# variables 285 extern Level *currentLevel; 286 extern Enemy enemies[ENEMY_MAX_COUNT]; 287 extern int enemyCount; 288 extern EnemyClassConfig enemyClassConfigs[]; 289 290 extern GUIState guiState; 291 extern GameTime gameTime; 292 extern Tower towers[TOWER_MAX_COUNT]; 293 extern int towerCount; 294 295 extern Texture2D palette, spriteSheet; 296 297 #endif
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 
  4 static TowerTypeConfig towerTypeConfigs[TOWER_TYPE_COUNT] = {
  5     [TOWER_TYPE_BASE] = {
  6         .maxHealth = 10,
  7     },
  8     [TOWER_TYPE_ARCHER] = {
  9         .cooldown = 0.5f,
 10         .range = 3.0f,
 11         .cost = 6,
 12         .maxHealth = 10,
 13         .projectileSpeed = 4.0f,
 14         .projectileType = PROJECTILE_TYPE_ARROW,
 15         .hitEffect = {
 16           .damage = 3.0f,
 17         }
 18     },
 19     [TOWER_TYPE_BALLISTA] = {
 20         .cooldown = 1.5f,
 21         .range = 6.0f,
 22         .cost = 9,
 23         .maxHealth = 10,
 24         .projectileSpeed = 10.0f,
 25         .projectileType = PROJECTILE_TYPE_BALLISTA,
 26         .hitEffect = {
 27           .damage = 6.0f,
 28           .pushbackPowerDistance = 0.25f,
 29         }
 30     },
 31     [TOWER_TYPE_CATAPULT] = {
 32         .cooldown = 1.7f,
 33         .range = 5.0f,
 34         .cost = 10,
 35         .maxHealth = 10,
 36         .projectileSpeed = 3.0f,
 37         .projectileType = PROJECTILE_TYPE_CATAPULT,
 38         .hitEffect = {
 39           .damage = 2.0f,
 40           .areaDamageRadius = 1.75f,
 41         }
 42     },
 43     [TOWER_TYPE_WALL] = {
 44         .cost = 2,
 45         .maxHealth = 10,
 46     },
 47 };
 48 
 49 Tower towers[TOWER_MAX_COUNT];
 50 int towerCount = 0;
 51 
 52 Model towerModels[TOWER_TYPE_COUNT];
 53 
 54 // definition of our archer unit
 55 SpriteUnit archerUnit = {
 56     .srcRect = {0, 0, 16, 16},
 57     .offset = {7, 1},
 58     .frameCount = 1,
 59     .frameDuration = 0.0f,
 60     .srcWeaponIdleRect = {16, 0, 6, 16},
 61     .srcWeaponIdleOffset = {8, 0},
 62     .srcWeaponCooldownRect = {22, 0, 11, 16},
 63     .srcWeaponCooldownOffset = {10, 0},
 64 };
 65 
 66 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase)
 67 {
 68   float xScale = flip ? -1.0f : 1.0f;
 69   Camera3D camera = currentLevel->camera;
 70   float size = 0.5f;
 71   Vector2 offset = (Vector2){ unit.offset.x / 16.0f * size, unit.offset.y / 16.0f * size * xScale };
 72   Vector2 scale = (Vector2){ unit.srcRect.width / 16.0f * size, unit.srcRect.height / 16.0f * size };
 73   // we want the sprite to face the camera, so we need to calculate the up vector
 74   Vector3 forward = Vector3Subtract(camera.target, camera.position);
 75   Vector3 up = {0, 1, 0};
 76   Vector3 right = Vector3CrossProduct(forward, up);
 77   up = Vector3Normalize(Vector3CrossProduct(right, forward));
 78 
 79   Rectangle srcRect = unit.srcRect;
 80   if (unit.frameCount > 1)
 81   {
 82     srcRect.x += (int)(t / unit.frameDuration) % unit.frameCount * srcRect.width;
 83   }
 84   if (flip)
 85   {
 86     srcRect.x += srcRect.width;
 87     srcRect.width = -srcRect.width;
 88   }
 89   DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
 90 
 91   if (phase == SPRITE_UNIT_PHASE_WEAPON_COOLDOWN && unit.srcWeaponCooldownRect.width > 0)
 92   {
 93     offset = (Vector2){ unit.srcWeaponCooldownOffset.x / 16.0f * size, unit.srcWeaponCooldownOffset.y / 16.0f * size };
 94     scale = (Vector2){ unit.srcWeaponCooldownRect.width / 16.0f * size, unit.srcWeaponCooldownRect.height / 16.0f * size };
 95     srcRect = unit.srcWeaponCooldownRect;
 96     if (flip)
 97     {
 98       // position.x = flip * scale.x * 0.5f;
 99       srcRect.x += srcRect.width;
100       srcRect.width = -srcRect.width;
101       offset.x = scale.x - offset.x;
102     }
103     DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
104   }
105   else if (phase == SPRITE_UNIT_PHASE_WEAPON_IDLE && unit.srcWeaponIdleRect.width > 0)
106   {
107     offset = (Vector2){ unit.srcWeaponIdleOffset.x / 16.0f * size, unit.srcWeaponIdleOffset.y / 16.0f * size };
108     scale = (Vector2){ unit.srcWeaponIdleRect.width / 16.0f * size, unit.srcWeaponIdleRect.height / 16.0f * size };
109     srcRect = unit.srcWeaponIdleRect;
110     if (flip)
111     {
112       // position.x = flip * scale.x * 0.5f;
113       srcRect.x += srcRect.width;
114       srcRect.width = -srcRect.width;
115       offset.x = scale.x - offset.x;
116     }
117     DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
118   }
119 }
120 
121 void TowerInit()
122 {
123   for (int i = 0; i < TOWER_MAX_COUNT; i++)
124   {
125     towers[i] = (Tower){0};
126   }
127   towerCount = 0;
128 
129   towerModels[TOWER_TYPE_BASE] = LoadModel("data/keep.glb");
130   towerModels[TOWER_TYPE_WALL] = LoadModel("data/wall-0000.glb");
131 
132   for (int i = 0; i < TOWER_TYPE_COUNT; i++)
133   {
134     if (towerModels[i].materials)
135     {
136       // assign the palette texture to the material of the model (0 is not used afaik)
137       towerModels[i].materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
138     }
139   }
140 }
141 
142 static void TowerGunUpdate(Tower *tower)
143 {
144   TowerTypeConfig config = towerTypeConfigs[tower->towerType];
145   if (tower->cooldown <= 0.0f)
146   {
147     Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, config.range);
148     if (enemy)
149     {
150       tower->cooldown = config.cooldown;
151       // shoot the enemy; determine future position of the enemy
152       float bulletSpeed = config.projectileSpeed;
153       Vector2 velocity = enemy->simVelocity;
154       Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0);
155       Vector2 towerPosition = {tower->x, tower->y};
156       float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed;
157       for (int i = 0; i < 8; i++) {
158         velocity = enemy->simVelocity;
159         futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0);
160         float distance = Vector2Distance(towerPosition, futurePosition);
161         float eta2 = distance / bulletSpeed;
162         if (fabs(eta - eta2) < 0.01f) {
163           break;
164         }
165         eta = (eta2 + eta) * 0.5f;
166       }
167 
168       ProjectileTryAdd(config.projectileType, enemy, 
169         (Vector3){towerPosition.x, 1.33f, towerPosition.y}, 
170         (Vector3){futurePosition.x, 0.25f, futurePosition.y},
171         bulletSpeed, config.hitEffect);
172       enemy->futureDamage += config.hitEffect.damage;
173       tower->lastTargetPosition = futurePosition;
174     }
175   }
176   else
177   {
178     tower->cooldown -= gameTime.deltaTime;
179   }
180 }
181 
182 Tower *TowerGetAt(int16_t x, int16_t y)
183 {
184   for (int i = 0; i < towerCount; i++)
185   {
186     if (towers[i].x == x && towers[i].y == y && towers[i].towerType != TOWER_TYPE_NONE)
187     {
188       return &towers[i];
189     }
190   }
191   return 0;
192 }
193 
194 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y)
195 {
196   if (towerCount >= TOWER_MAX_COUNT)
197   {
198     return 0;
199   }
200 
201   Tower *tower = TowerGetAt(x, y);
202   if (tower)
203   {
204     return 0;
205   }
206 
207   tower = &towers[towerCount++];
208   tower->x = x;
209   tower->y = y;
210   tower->towerType = towerType;
211   tower->cooldown = 0.0f;
212   tower->damage = 0.0f;
213   return tower;
214 }
215 
216 Tower *GetTowerByType(uint8_t towerType)
217 {
218   for (int i = 0; i < towerCount; i++)
219   {
220     if (towers[i].towerType == towerType)
221     {
222       return &towers[i];
223     }
224   }
225   return 0;
226 }
227 
228 int GetTowerCosts(uint8_t towerType)
229 {
230   return towerTypeConfigs[towerType].cost;
231 }
232 
233 float TowerGetMaxHealth(Tower *tower)
234 {
235   return towerTypeConfigs[tower->towerType].maxHealth;
236 }
237 
238 void TowerDraw()
239 {
240   for (int i = 0; i < towerCount; i++)
241   {
242     Tower tower = towers[i];
243     if (tower.towerType == TOWER_TYPE_NONE)
244     {
245       continue;
246     }
247 
248     switch (tower.towerType)
249     {
250     case TOWER_TYPE_ARCHER:
251       {
252         Vector2 screenPosTower = GetWorldToScreen((Vector3){tower.x, 0.0f, tower.y}, currentLevel->camera);
253         Vector2 screenPosTarget = GetWorldToScreen((Vector3){tower.lastTargetPosition.x, 0.0f, tower.lastTargetPosition.y}, currentLevel->camera);
254         DrawModel(towerModels[TOWER_TYPE_WALL], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
255         DrawSpriteUnit(archerUnit, (Vector3){tower.x, 1.0f, tower.y}, 0, screenPosTarget.x > screenPosTower.x, 
256           tower.cooldown > 0.2f ? SPRITE_UNIT_PHASE_WEAPON_COOLDOWN : SPRITE_UNIT_PHASE_WEAPON_IDLE);
257       }
258       break;
259     case TOWER_TYPE_BALLISTA:
260       DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, BROWN);
261       break;
262     case TOWER_TYPE_CATAPULT:
263       DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, DARKGRAY);
264       break;
265     default:
266       if (towerModels[tower.towerType].materials)
267       {
268         DrawModel(towerModels[tower.towerType], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
269       } else {
270         DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY);
271       }
272       break;
273     }
274   }
275 }
276 
277 void TowerUpdate()
278 {
279   for (int i = 0; i < towerCount; i++)
280   {
281     Tower *tower = &towers[i];
282     switch (tower->towerType)
283     {
284     case TOWER_TYPE_CATAPULT:
285     case TOWER_TYPE_BALLISTA:
286     case TOWER_TYPE_ARCHER:
287       TowerGunUpdate(tower);
288       break;
289     }
290   }
291 }
292 
293 void TowerDrawHealthBars(Camera3D camera)
294 {
295   for (int i = 0; i < towerCount; i++)
296   {
297     Tower *tower = &towers[i];
298     if (tower->towerType == TOWER_TYPE_NONE || tower->damage <= 0.0f)
299     {
300       continue;
301     }
302     
303     Vector3 position = (Vector3){tower->x, 0.5f, tower->y};
304     float maxHealth = TowerGetMaxHealth(tower);
305     float health = maxHealth - tower->damage;
306     float healthRatio = health / maxHealth;
307     
308     DrawHealthBar(camera, position, healthRatio, GREEN, 35.0f);
309   }
310 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 #include <stdlib.h>
  4 #include <math.h>
  5 
  6 EnemyClassConfig enemyClassConfigs[] = {
  7     [ENEMY_TYPE_MINION] = {
  8       .health = 10.0f, 
  9       .speed = 0.6f, 
 10       .radius = 0.25f, 
 11       .maxAcceleration = 1.0f,
 12       .explosionDamage = 1.0f,
 13       .requiredContactTime = 0.5f,
 14       .explosionRange = 1.0f,
 15       .explosionPushbackPower = 0.25f,
 16       .goldValue = 1,
 17     },
 18 };
 19 
 20 Enemy enemies[ENEMY_MAX_COUNT];
 21 int enemyCount = 0;
 22 
 23 SpriteUnit enemySprites[] = {
 24     [ENEMY_TYPE_MINION] = {
 25       .srcRect = {0, 16, 16, 16},
 26       .offset = {8.0f, 0.0f},
 27       .frameCount = 6,
 28       .frameDuration = 0.1f,
 29     },
 30 };
 31 
 32 void EnemyInit()
 33 {
 34   for (int i = 0; i < ENEMY_MAX_COUNT; i++)
 35   {
 36     enemies[i] = (Enemy){0};
 37   }
 38   enemyCount = 0;
 39 }
 40 
 41 float EnemyGetCurrentMaxSpeed(Enemy *enemy)
 42 {
 43   return enemyClassConfigs[enemy->enemyType].speed;
 44 }
 45 
 46 float EnemyGetMaxHealth(Enemy *enemy)
 47 {
 48   return enemyClassConfigs[enemy->enemyType].health;
 49 }
 50 
 51 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY)
 52 {
 53   int16_t castleX = 0;
 54   int16_t castleY = 0;
 55   int16_t dx = castleX - currentX;
 56   int16_t dy = castleY - currentY;
 57   if (dx == 0 && dy == 0)
 58   {
 59     *nextX = currentX;
 60     *nextY = currentY;
 61     return 1;
 62   }
 63   Vector2 gradient = PathFindingGetGradient((Vector3){currentX, 0, currentY});
 64 
 65   if (gradient.x == 0 && gradient.y == 0)
 66   {
 67     *nextX = currentX;
 68     *nextY = currentY;
 69     return 1;
 70   }
 71 
 72   if (fabsf(gradient.x) > fabsf(gradient.y))
 73   {
 74     *nextX = currentX + (int16_t)(gradient.x > 0.0f ? 1 : -1);
 75     *nextY = currentY;
 76     return 0;
 77   }
 78   *nextX = currentX;
 79   *nextY = currentY + (int16_t)(gradient.y > 0.0f ? 1 : -1);
 80   return 0;
 81 }
 82 
 83 
 84 // this function predicts the movement of the unit for the next deltaT seconds
 85 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount)
 86 {
 87   const float pointReachedDistance = 0.25f;
 88   const float pointReachedDistance2 = pointReachedDistance * pointReachedDistance;
 89   const float maxSimStepTime = 0.015625f;
 90   
 91   float maxAcceleration = enemyClassConfigs[enemy->enemyType].maxAcceleration;
 92   float maxSpeed = EnemyGetCurrentMaxSpeed(enemy);
 93   int16_t nextX = enemy->nextX;
 94   int16_t nextY = enemy->nextY;
 95   Vector2 position = enemy->simPosition;
 96   int passedCount = 0;
 97   for (float t = 0.0f; t < deltaT; t += maxSimStepTime)
 98   {
 99     float stepTime = fminf(deltaT - t, maxSimStepTime);
100     Vector2 target = (Vector2){nextX, nextY};
101     float speed = Vector2Length(*velocity);
102     // draw the target position for debugging
103     //DrawCubeWires((Vector3){target.x, 0.2f, target.y}, 0.1f, 0.4f, 0.1f, RED);
104     Vector2 lookForwardPos = Vector2Add(position, Vector2Scale(*velocity, speed));
105     if (Vector2DistanceSqr(target, lookForwardPos) <= pointReachedDistance2)
106     {
107       // we reached the target position, let's move to the next waypoint
108       EnemyGetNextPosition(nextX, nextY, &nextX, &nextY);
109       target = (Vector2){nextX, nextY};
110       // track how many waypoints we passed
111       passedCount++;
112     }
113     
114     // acceleration towards the target
115     Vector2 unitDirection = Vector2Normalize(Vector2Subtract(target, lookForwardPos));
116     Vector2 acceleration = Vector2Scale(unitDirection, maxAcceleration * stepTime);
117     *velocity = Vector2Add(*velocity, acceleration);
118 
119     // limit the speed to the maximum speed
120     if (speed > maxSpeed)
121     {
122       *velocity = Vector2Scale(*velocity, maxSpeed / speed);
123     }
124 
125     // move the enemy
126     position = Vector2Add(position, Vector2Scale(*velocity, stepTime));
127   }
128 
129   if (waypointPassedCount)
130   {
131     (*waypointPassedCount) = passedCount;
132   }
133 
134   return position;
135 }
136 
137 void EnemyDraw()
138 {
139   for (int i = 0; i < enemyCount; i++)
140   {
141     Enemy enemy = enemies[i];
142     if (enemy.enemyType == ENEMY_TYPE_NONE)
143     {
144       continue;
145     }
146 
147     Vector2 position = EnemyGetPosition(&enemy, gameTime.time - enemy.startMovingTime, &enemy.simVelocity, 0);
148     
149     // don't draw any trails for now; might replace this with footprints later
150     // if (enemy.movePathCount > 0)
151     // {
152     //   Vector3 p = {enemy.movePath[0].x, 0.2f, enemy.movePath[0].y};
153     //   DrawLine3D(p, (Vector3){position.x, 0.2f, position.y}, GREEN);
154     // }
155     // for (int j = 1; j < enemy.movePathCount; j++)
156     // {
157     //   Vector3 p = {enemy.movePath[j - 1].x, 0.2f, enemy.movePath[j - 1].y};
158     //   Vector3 q = {enemy.movePath[j].x, 0.2f, enemy.movePath[j].y};
159     //   DrawLine3D(p, q, GREEN);
160     // }
161 
162     switch (enemy.enemyType)
163     {
164     case ENEMY_TYPE_MINION:
165       DrawSpriteUnit(enemySprites[ENEMY_TYPE_MINION], (Vector3){position.x, 0.0f, position.y}, 
166         enemy.walkedDistance, 0, 0);
167       break;
168     }
169   }
170 }
171 
172 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource)
173 {
174   // damage the tower
175   float explosionDamge = enemyClassConfigs[enemy->enemyType].explosionDamage;
176   float explosionRange = enemyClassConfigs[enemy->enemyType].explosionRange;
177   float explosionPushbackPower = enemyClassConfigs[enemy->enemyType].explosionPushbackPower;
178   float explosionRange2 = explosionRange * explosionRange;
179   tower->damage += enemyClassConfigs[enemy->enemyType].explosionDamage;
180   // explode the enemy
181   if (tower->damage >= TowerGetMaxHealth(tower))
182   {
183     tower->towerType = TOWER_TYPE_NONE;
184   }
185 
186   ParticleAdd(PARTICLE_TYPE_EXPLOSION, 
187     explosionSource, 
188     (Vector3){0, 0.1f, 0}, (Vector3){1.0f, 1.0f, 1.0f}, 1.0f);
189 
190   enemy->enemyType = ENEMY_TYPE_NONE;
191 
192   // push back enemies & dealing damage
193   for (int i = 0; i < enemyCount; i++)
194   {
195     Enemy *other = &enemies[i];
196     if (other->enemyType == ENEMY_TYPE_NONE)
197     {
198       continue;
199     }
200     float distanceSqr = Vector2DistanceSqr(enemy->simPosition, other->simPosition);
201     if (distanceSqr > 0 && distanceSqr < explosionRange2)
202     {
203       Vector2 direction = Vector2Normalize(Vector2Subtract(other->simPosition, enemy->simPosition));
204       other->simPosition = Vector2Add(other->simPosition, Vector2Scale(direction, explosionPushbackPower));
205       EnemyAddDamage(other, explosionDamge);
206     }
207   }
208 }
209 
210 void EnemyUpdate()
211 {
212   const float castleX = 0;
213   const float castleY = 0;
214   const float maxPathDistance2 = 0.25f * 0.25f;
215   
216   for (int i = 0; i < enemyCount; i++)
217   {
218     Enemy *enemy = &enemies[i];
219     if (enemy->enemyType == ENEMY_TYPE_NONE)
220     {
221       continue;
222     }
223 
224     int waypointPassedCount = 0;
225     Vector2 prevPosition = enemy->simPosition;
226     enemy->simPosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &enemy->simVelocity, &waypointPassedCount);
227     enemy->startMovingTime = gameTime.time;
228     enemy->walkedDistance += Vector2Distance(prevPosition, enemy->simPosition);
229     // track path of unit
230     if (enemy->movePathCount == 0 || Vector2DistanceSqr(enemy->simPosition, enemy->movePath[0]) > maxPathDistance2)
231     {
232       for (int j = ENEMY_MAX_PATH_COUNT - 1; j > 0; j--)
233       {
234         enemy->movePath[j] = enemy->movePath[j - 1];
235       }
236       enemy->movePath[0] = enemy->simPosition;
237       if (++enemy->movePathCount > ENEMY_MAX_PATH_COUNT)
238       {
239         enemy->movePathCount = ENEMY_MAX_PATH_COUNT;
240       }
241     }
242 
243     if (waypointPassedCount > 0)
244     {
245       enemy->currentX = enemy->nextX;
246       enemy->currentY = enemy->nextY;
247       if (EnemyGetNextPosition(enemy->currentX, enemy->currentY, &enemy->nextX, &enemy->nextY) &&
248         Vector2DistanceSqr(enemy->simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
249       {
250         // enemy reached the castle; remove it
251         enemy->enemyType = ENEMY_TYPE_NONE;
252         continue;
253       }
254     }
255   }
256 
257   // handle collisions between enemies
258   for (int i = 0; i < enemyCount - 1; i++)
259   {
260     Enemy *enemyA = &enemies[i];
261     if (enemyA->enemyType == ENEMY_TYPE_NONE)
262     {
263       continue;
264     }
265     for (int j = i + 1; j < enemyCount; j++)
266     {
267       Enemy *enemyB = &enemies[j];
268       if (enemyB->enemyType == ENEMY_TYPE_NONE)
269       {
270         continue;
271       }
272       float distanceSqr = Vector2DistanceSqr(enemyA->simPosition, enemyB->simPosition);
273       float radiusA = enemyClassConfigs[enemyA->enemyType].radius;
274       float radiusB = enemyClassConfigs[enemyB->enemyType].radius;
275       float radiusSum = radiusA + radiusB;
276       if (distanceSqr < radiusSum * radiusSum && distanceSqr > 0.001f)
277       {
278         // collision
279         float distance = sqrtf(distanceSqr);
280         float overlap = radiusSum - distance;
281         // move the enemies apart, but softly; if we have a clog of enemies,
282         // moving them perfectly apart can cause them to jitter
283         float positionCorrection = overlap / 5.0f;
284         Vector2 direction = (Vector2){
285             (enemyB->simPosition.x - enemyA->simPosition.x) / distance * positionCorrection,
286             (enemyB->simPosition.y - enemyA->simPosition.y) / distance * positionCorrection};
287         enemyA->simPosition = Vector2Subtract(enemyA->simPosition, direction);
288         enemyB->simPosition = Vector2Add(enemyB->simPosition, direction);
289       }
290     }
291   }
292 
293   // handle collisions between enemies and towers
294   for (int i = 0; i < enemyCount; i++)
295   {
296     Enemy *enemy = &enemies[i];
297     if (enemy->enemyType == ENEMY_TYPE_NONE)
298     {
299       continue;
300     }
301     enemy->contactTime -= gameTime.deltaTime;
302     if (enemy->contactTime < 0.0f)
303     {
304       enemy->contactTime = 0.0f;
305     }
306 
307     float enemyRadius = enemyClassConfigs[enemy->enemyType].radius;
308     // linear search over towers; could be optimized by using path finding tower map,
309     // but for now, we keep it simple
310     for (int j = 0; j < towerCount; j++)
311     {
312       Tower *tower = &towers[j];
313       if (tower->towerType == TOWER_TYPE_NONE)
314       {
315         continue;
316       }
317       float distanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){tower->x, tower->y});
318       float combinedRadius = enemyRadius + 0.708; // sqrt(0.5^2 + 0.5^2), corner-center distance of square with side length 1
319       if (distanceSqr > combinedRadius * combinedRadius)
320       {
321         continue;
322       }
323       // potential collision; square / circle intersection
324       float dx = tower->x - enemy->simPosition.x;
325       float dy = tower->y - enemy->simPosition.y;
326       float absDx = fabsf(dx);
327       float absDy = fabsf(dy);
328       Vector3 contactPoint = {0};
329       if (absDx <= 0.5f && absDx <= absDy) {
330         // vertical collision; push the enemy out horizontally
331         float overlap = enemyRadius + 0.5f - absDy;
332         if (overlap < 0.0f)
333         {
334           continue;
335         }
336         float direction = dy > 0.0f ? -1.0f : 1.0f;
337         enemy->simPosition.y += direction * overlap;
338         contactPoint = (Vector3){enemy->simPosition.x, 0.2f, tower->y + direction * 0.5f};
339       }
340       else if (absDy <= 0.5f && absDy <= absDx)
341       {
342         // horizontal collision; push the enemy out vertically
343         float overlap = enemyRadius + 0.5f - absDx;
344         if (overlap < 0.0f)
345         {
346           continue;
347         }
348         float direction = dx > 0.0f ? -1.0f : 1.0f;
349         enemy->simPosition.x += direction * overlap;
350         contactPoint = (Vector3){tower->x + direction * 0.5f, 0.2f, enemy->simPosition.y};
351       }
352       else
353       {
354         // possible collision with a corner
355         float cornerDX = dx > 0.0f ? -0.5f : 0.5f;
356         float cornerDY = dy > 0.0f ? -0.5f : 0.5f;
357         float cornerX = tower->x + cornerDX;
358         float cornerY = tower->y + cornerDY;
359         float cornerDistanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){cornerX, cornerY});
360         if (cornerDistanceSqr > enemyRadius * enemyRadius)
361         {
362           continue;
363         }
364         // push the enemy out along the diagonal
365         float cornerDistance = sqrtf(cornerDistanceSqr);
366         float overlap = enemyRadius - cornerDistance;
367         float directionX = cornerDistance > 0.0f ? (cornerX - enemy->simPosition.x) / cornerDistance : -cornerDX;
368         float directionY = cornerDistance > 0.0f ? (cornerY - enemy->simPosition.y) / cornerDistance : -cornerDY;
369         enemy->simPosition.x -= directionX * overlap;
370         enemy->simPosition.y -= directionY * overlap;
371         contactPoint = (Vector3){cornerX, 0.2f, cornerY};
372       }
373 
374       if (enemyClassConfigs[enemy->enemyType].explosionDamage > 0.0f)
375       {
376         enemy->contactTime += gameTime.deltaTime * 2.0f; // * 2 to undo the subtraction above
377         if (enemy->contactTime >= enemyClassConfigs[enemy->enemyType].requiredContactTime)
378         {
379           EnemyTriggerExplode(enemy, tower, contactPoint);
380         }
381       }
382     }
383   }
384 }
385 
386 EnemyId EnemyGetId(Enemy *enemy)
387 {
388   return (EnemyId){enemy - enemies, enemy->generation};
389 }
390 
391 Enemy *EnemyTryResolve(EnemyId enemyId)
392 {
393   if (enemyId.index >= ENEMY_MAX_COUNT)
394   {
395     return 0;
396   }
397   Enemy *enemy = &enemies[enemyId.index];
398   if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE)
399   {
400     return 0;
401   }
402   return enemy;
403 }
404 
405 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY)
406 {
407   Enemy *spawn = 0;
408   for (int i = 0; i < enemyCount; i++)
409   {
410     Enemy *enemy = &enemies[i];
411     if (enemy->enemyType == ENEMY_TYPE_NONE)
412     {
413       spawn = enemy;
414       break;
415     }
416   }
417 
418   if (enemyCount < ENEMY_MAX_COUNT && !spawn)
419   {
420     spawn = &enemies[enemyCount++];
421   }
422 
423   if (spawn)
424   {
425     spawn->currentX = currentX;
426     spawn->currentY = currentY;
427     spawn->nextX = currentX;
428     spawn->nextY = currentY;
429     spawn->simPosition = (Vector2){currentX, currentY};
430     spawn->simVelocity = (Vector2){0, 0};
431     spawn->enemyType = enemyType;
432     spawn->startMovingTime = gameTime.time;
433     spawn->damage = 0.0f;
434     spawn->futureDamage = 0.0f;
435     spawn->generation++;
436     spawn->movePathCount = 0;
437     spawn->walkedDistance = 0.0f;
438   }
439 
440   return spawn;
441 }
442 
443 int EnemyAddDamageRange(Vector2 position, float range, float damage)
444 {
445   int count = 0;
446   float range2 = range * range;
447   for (int i = 0; i < enemyCount; i++)
448   {
449     Enemy *enemy = &enemies[i];
450     if (enemy->enemyType == ENEMY_TYPE_NONE)
451     {
452       continue;
453     }
454     float distance2 = Vector2DistanceSqr(position, enemy->simPosition);
455     if (distance2 <= range2)
456     {
457       EnemyAddDamage(enemy, damage);
458       count++;
459     }
460   }
461   return count;
462 }
463 
464 int EnemyAddDamage(Enemy *enemy, float damage)
465 {
466   enemy->damage += damage;
467   if (enemy->damage >= EnemyGetMaxHealth(enemy))
468   {
469     currentLevel->playerGold += enemyClassConfigs[enemy->enemyType].goldValue;
470     enemy->enemyType = ENEMY_TYPE_NONE;
471     return 1;
472   }
473 
474   return 0;
475 }
476 
477 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
478 {
479   int16_t castleX = 0;
480   int16_t castleY = 0;
481   Enemy* closest = 0;
482   int16_t closestDistance = 0;
483   float range2 = range * range;
484   for (int i = 0; i < enemyCount; i++)
485   {
486     Enemy* enemy = &enemies[i];
487     if (enemy->enemyType == ENEMY_TYPE_NONE)
488     {
489       continue;
490     }
491     float maxHealth = EnemyGetMaxHealth(enemy);
492     if (enemy->futureDamage >= maxHealth)
493     {
494       // ignore enemies that will die soon
495       continue;
496     }
497     int16_t dx = castleX - enemy->currentX;
498     int16_t dy = castleY - enemy->currentY;
499     int16_t distance = abs(dx) + abs(dy);
500     if (!closest || distance < closestDistance)
501     {
502       float tdx = towerX - enemy->currentX;
503       float tdy = towerY - enemy->currentY;
504       float tdistance2 = tdx * tdx + tdy * tdy;
505       if (tdistance2 <= range2)
506       {
507         closest = enemy;
508         closestDistance = distance;
509       }
510     }
511   }
512   return closest;
513 }
514 
515 int EnemyCount()
516 {
517   int count = 0;
518   for (int i = 0; i < enemyCount; i++)
519   {
520     if (enemies[i].enemyType != ENEMY_TYPE_NONE)
521     {
522       count++;
523     }
524   }
525   return count;
526 }
527 
528 void EnemyDrawHealthbars(Camera3D camera)
529 {
530   for (int i = 0; i < enemyCount; i++)
531   {
532     Enemy *enemy = &enemies[i];
533     if (enemy->enemyType == ENEMY_TYPE_NONE || enemy->damage == 0.0f)
534     {
535       continue;
536     }
537     Vector3 position = (Vector3){enemy->simPosition.x, 0.5f, enemy->simPosition.y};
538     float maxHealth = EnemyGetMaxHealth(enemy);
539     float health = maxHealth - enemy->damage;
540     float healthRatio = health / maxHealth;
541     
542     DrawHealthBar(camera, position, healthRatio, GREEN, 15.0f);
543   }
544 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 
  4 static Projectile projectiles[PROJECTILE_MAX_COUNT];
  5 static int projectileCount = 0;
  6 
  7 typedef struct ProjectileConfig
  8 {
  9   float arcFactor;
 10   Color color;
 11   Color trailColor;
 12 } ProjectileConfig;
 13 
 14 ProjectileConfig projectileConfigs[] = {
 15     [PROJECTILE_TYPE_ARROW] = {
 16         .arcFactor = 0.15f,
 17         .color = RED,
 18         .trailColor = BROWN,
 19     },
 20     [PROJECTILE_TYPE_CATAPULT] = {
 21         .arcFactor = 0.5f,
 22         .color = RED,
 23         .trailColor = GRAY,
 24     },
 25     [PROJECTILE_TYPE_BALLISTA] = {
 26         .arcFactor = 0.025f,
 27         .color = RED,
 28         .trailColor = BROWN,
 29     },
 30 };
 31 
 32 void ProjectileInit()
 33 {
 34   for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
 35   {
 36     projectiles[i] = (Projectile){0};
 37   }
 38 }
 39 
 40 void ProjectileDraw()
 41 {
 42   for (int i = 0; i < projectileCount; i++)
 43   {
 44     Projectile projectile = projectiles[i];
 45     if (projectile.projectileType == PROJECTILE_TYPE_NONE)
 46     {
 47       continue;
 48     }
 49     float transition = (gameTime.time - projectile.shootTime) / (projectile.arrivalTime - projectile.shootTime);
 50     if (transition >= 1.0f)
 51     {
 52       continue;
 53     }
 54 
 55     ProjectileConfig config = projectileConfigs[projectile.projectileType];
 56     for (float transitionOffset = 0.0f; transitionOffset < 1.0f; transitionOffset += 0.1f)
 57     {
 58       float t = transition + transitionOffset * 0.3f;
 59       if (t > 1.0f)
 60       {
 61         break;
 62       }
 63       Vector3 position = Vector3Lerp(projectile.position, projectile.target, t);
 64       Color color = config.color;
 65       color = ColorLerp(config.trailColor, config.color, transitionOffset * transitionOffset);
 66       // fake a ballista flight path using parabola equation
 67       float parabolaT = t - 0.5f;
 68       parabolaT = 1.0f - 4.0f * parabolaT * parabolaT;
 69       position.y += config.arcFactor * parabolaT * projectile.distance;
 70       
 71       float size = 0.06f * (transitionOffset + 0.25f);
 72       DrawCube(position, size, size, size, color);
 73     }
 74   }
 75 }
 76 
 77 void ProjectileUpdate()
 78 {
 79   for (int i = 0; i < projectileCount; i++)
 80   {
 81     Projectile *projectile = &projectiles[i];
 82     if (projectile->projectileType == PROJECTILE_TYPE_NONE)
 83     {
 84       continue;
 85     }
 86     float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime);
 87     if (transition >= 1.0f)
 88     {
 89       projectile->projectileType = PROJECTILE_TYPE_NONE;
 90       Enemy *enemy = EnemyTryResolve(projectile->targetEnemy);
 91       if (enemy && projectile->hitEffectConfig.pushbackPowerDistance > 0.0f)
 92       {
 93           Vector2 direction = Vector2Normalize(Vector2Subtract((Vector2){projectile->target.x, projectile->target.z}, enemy->simPosition));
 94           enemy->simPosition = Vector2Add(enemy->simPosition, Vector2Scale(direction, projectile->hitEffectConfig.pushbackPowerDistance));
 95       }
 96       
 97       if (projectile->hitEffectConfig.areaDamageRadius > 0.0f)
 98       {
 99         EnemyAddDamageRange((Vector2){projectile->target.x, projectile->target.z}, projectile->hitEffectConfig.areaDamageRadius, projectile->hitEffectConfig.damage);
100         // pancaked sphere explosion
101         float r = projectile->hitEffectConfig.areaDamageRadius;
102         ParticleAdd(PARTICLE_TYPE_EXPLOSION, projectile->target, (Vector3){0}, (Vector3){r, r * 0.2f, r}, 0.33f);
103       }
104       else if (projectile->hitEffectConfig.damage > 0.0f && enemy)
105       {
106         EnemyAddDamage(enemy, projectile->hitEffectConfig.damage);
107       }
108       continue;
109     }
110   }
111 }
112 
113 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig)
114 {
115   for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
116   {
117     Projectile *projectile = &projectiles[i];
118     if (projectile->projectileType == PROJECTILE_TYPE_NONE)
119     {
120       projectile->projectileType = projectileType;
121       projectile->shootTime = gameTime.time;
122       float distance = Vector3Distance(position, target);
123       projectile->arrivalTime = gameTime.time + distance / speed;
124       projectile->position = position;
125       projectile->target = target;
126       projectile->directionNormal = Vector3Scale(Vector3Subtract(target, position), 1.0f / distance);
127       projectile->distance = distance;
128       projectile->targetEnemy = EnemyGetId(enemy);
129       projectileCount = projectileCount <= i ? i + 1 : projectileCount;
130       projectile->hitEffectConfig = hitEffectConfig;
131       return projectile;
132     }
133   }
134   return 0;
135 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 #include <rlgl.h>
  4 
  5 static Particle particles[PARTICLE_MAX_COUNT];
  6 static int particleCount = 0;
  7 
  8 void ParticleInit()
  9 {
 10   for (int i = 0; i < PARTICLE_MAX_COUNT; i++)
 11   {
 12     particles[i] = (Particle){0};
 13   }
 14   particleCount = 0;
 15 }
 16 
 17 static void DrawExplosionParticle(Particle *particle, float transition)
 18 {
 19   Vector3 scale = particle->scale;
 20   float size = 1.0f * (1.0f - transition);
 21   Color startColor = WHITE;
 22   Color endColor = RED;
 23   Color color = ColorLerp(startColor, endColor, transition);
 24 
 25   rlPushMatrix();
 26   rlTranslatef(particle->position.x, particle->position.y, particle->position.z);
 27   rlScalef(scale.x, scale.y, scale.z);
 28   DrawSphere(Vector3Zero(), size, color);
 29   rlPopMatrix();
 30 }
 31 
 32 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime)
 33 {
 34   if (particleCount >= PARTICLE_MAX_COUNT)
 35   {
 36     return;
 37   }
 38 
 39   int index = -1;
 40   for (int i = 0; i < particleCount; i++)
 41   {
 42     if (particles[i].particleType == PARTICLE_TYPE_NONE)
 43     {
 44       index = i;
 45       break;
 46     }
 47   }
 48 
 49   if (index == -1)
 50   {
 51     index = particleCount++;
 52   }
 53 
 54   Particle *particle = &particles[index];
 55   particle->particleType = particleType;
 56   particle->spawnTime = gameTime.time;
 57   particle->lifetime = lifetime;
 58   particle->position = position;
 59   particle->velocity = velocity;
 60   particle->scale = scale;
 61 }
 62 
 63 void ParticleUpdate()
 64 {
 65   for (int i = 0; i < particleCount; i++)
 66   {
 67     Particle *particle = &particles[i];
 68     if (particle->particleType == PARTICLE_TYPE_NONE)
 69     {
 70       continue;
 71     }
 72 
 73     float age = gameTime.time - particle->spawnTime;
 74 
 75     if (particle->lifetime > age)
 76     {
 77       particle->position = Vector3Add(particle->position, Vector3Scale(particle->velocity, gameTime.deltaTime));
 78     }
 79     else {
 80       particle->particleType = PARTICLE_TYPE_NONE;
 81     }
 82   }
 83 }
 84 
 85 void ParticleDraw()
 86 {
 87   for (int i = 0; i < particleCount; i++)
 88   {
 89     Particle particle = particles[i];
 90     if (particle.particleType == PARTICLE_TYPE_NONE)
 91     {
 92       continue;
 93     }
 94 
 95     float age = gameTime.time - particle.spawnTime;
 96     float transition = age / particle.lifetime;
 97     switch (particle.particleType)
 98     {
 99     case PARTICLE_TYPE_EXPLOSION:
100       DrawExplosionParticle(&particle, transition);
101       break;
102     default:
103       DrawCube(particle.position, 0.3f, 0.5f, 0.3f, RED);
104       break;
105     }
106   }
107 }
  1 #include "raylib.h"
  2 #include "preferred_size.h"
  3 
  4 // Since the canvas size is not known at compile time, we need to query it at runtime;
  5 // the following platform specific code obtains the canvas size and we will use this
  6 // size as the preferred size for the window at init time. We're ignoring here the
  7 // possibility of the canvas size changing during runtime - this would require to
  8 // poll the canvas size in the game loop or establishing a callback to be notified
  9 
 10 #ifdef PLATFORM_WEB
 11 #include <emscripten.h>
 12 EMSCRIPTEN_RESULT emscripten_get_element_css_size(const char *target, double *width, double *height);
 13 
 14 void GetPreferredSize(int *screenWidth, int *screenHeight)
 15 {
 16   double canvasWidth, canvasHeight;
 17   emscripten_get_element_css_size("#" CANVAS_NAME, &canvasWidth, &canvasHeight);
 18   *screenWidth = (int)canvasWidth;
 19   *screenHeight = (int)canvasHeight;
 20   TraceLog(LOG_INFO, "preferred size for %s: %d %d", CANVAS_NAME, *screenWidth, *screenHeight);
 21 }
 22 
 23 int IsPaused()
 24 {
 25   const char *js = "(function(){\n"
 26   "  var canvas = document.getElementById(\"" CANVAS_NAME "\");\n"
 27   "  var rect = canvas.getBoundingClientRect();\n"
 28   "  var isVisible = (\n"
 29   "    rect.top >= 0 &&\n"
 30   "    rect.left >= 0 &&\n"
 31   "    rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&\n"
 32   "    rect.right <= (window.innerWidth || document.documentElement.clientWidth)\n"
 33   "  );\n"
 34   "  return isVisible ? 0 : 1;\n"
 35   "})()";
 36   return emscripten_run_script_int(js);
 37 }
 38 
 39 #else
 40 void GetPreferredSize(int *screenWidth, int *screenHeight)
 41 {
 42   *screenWidth = 600;
 43   *screenHeight = 240;
 44 }
 45 int IsPaused()
 46 {
 47   return 0;
 48 }
 49 #endif
  1 #ifndef PREFERRED_SIZE_H
  2 #define PREFERRED_SIZE_H
  3 
  4 void GetPreferredSize(int *screenWidth, int *screenHeight);
  5 int IsPaused();
  6 
  7 #endif
The path preview is now rendered twice: Once with depth testing enabled and once with depth testing disabled.
Path preview shadows through the towers

This improves the visualization when the path preview is blocked by the towers quite a bit.

One thing on my mind is now, that there could be multiple enemy entry points. Let's test how it will look like when the enemy path previews merge along the way:

  • 💾
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 #include <rlgl.h>
  4 #include <stdlib.h>
  5 #include <math.h>
  6 
  7 //# Variables
  8 GUIState guiState = {0};
  9 GameTime gameTime = {0};
 10 
 11 Model floorTileAModel = {0};
 12 Model floorTileBModel = {0};
 13 Model treeModel[2] = {0};
 14 Model firTreeModel[2] = {0};
 15 Model rockModels[5] = {0};
 16 Model grassPatchModel[1] = {0};
 17 
 18 Model pathArrowModel = {0};
 19 
 20 Texture2D palette, spriteSheet;
 21 
 22 Level levels[] = {
 23   [0] = {
 24     .state = LEVEL_STATE_BUILDING,
 25     .initialGold = 20,
 26     .waves[0] = {
 27       .enemyType = ENEMY_TYPE_MINION,
 28       .wave = 0,
29 .count = 5,
30 .interval = 2.5f, 31 .delay = 1.0f,
32 .spawnPosition = {2, 6},
33 },
34 .waves[1] = { 35 .enemyType = ENEMY_TYPE_MINION, 36 .wave = 0, 37 .count = 5, 38 .interval = 2.5f, 39 .delay = 1.0f, 40 .spawnPosition = {-2, 6}, 41 }, 42 .waves[2] = {
43 .enemyType = ENEMY_TYPE_MINION, 44 .wave = 1, 45 .count = 20, 46 .interval = 1.5f, 47 .delay = 1.0f, 48 .spawnPosition = {0, 6}, 49 },
50 .waves[3] = {
51 .enemyType = ENEMY_TYPE_MINION, 52 .wave = 2, 53 .count = 30, 54 .interval = 1.2f, 55 .delay = 1.0f, 56 .spawnPosition = {0, 6}, 57 } 58 }, 59 }; 60 61 Level *currentLevel = levels; 62 63 //# Game 64 65 static Model LoadGLBModel(char *filename) 66 { 67 Model model = LoadModel(TextFormat("data/%s.glb",filename)); 68 for (int i = 0; i < model.materialCount; i++) 69 { 70 model.materials[i].maps[MATERIAL_MAP_DIFFUSE].texture = palette; 71 } 72 return model; 73 } 74 75 void LoadAssets() 76 { 77 // load a sprite sheet that contains all units 78 spriteSheet = LoadTexture("data/spritesheet.png"); 79 SetTextureFilter(spriteSheet, TEXTURE_FILTER_BILINEAR); 80 81 // we'll use a palette texture to colorize the all buildings and environment art 82 palette = LoadTexture("data/palette.png"); 83 // The texture uses gradients on very small space, so we'll enable bilinear filtering 84 SetTextureFilter(palette, TEXTURE_FILTER_BILINEAR); 85 86 floorTileAModel = LoadGLBModel("floor-tile-a"); 87 floorTileBModel = LoadGLBModel("floor-tile-b"); 88 treeModel[0] = LoadGLBModel("leaftree-large-1-a"); 89 treeModel[1] = LoadGLBModel("leaftree-large-1-b"); 90 firTreeModel[0] = LoadGLBModel("firtree-1-a"); 91 firTreeModel[1] = LoadGLBModel("firtree-1-b"); 92 rockModels[0] = LoadGLBModel("rock-1"); 93 rockModels[1] = LoadGLBModel("rock-2"); 94 rockModels[2] = LoadGLBModel("rock-3"); 95 rockModels[3] = LoadGLBModel("rock-4"); 96 rockModels[4] = LoadGLBModel("rock-5"); 97 grassPatchModel[0] = LoadGLBModel("grass-patch-1"); 98 99 pathArrowModel = LoadGLBModel("direction-arrow-x"); 100 } 101 102 void InitLevel(Level *level) 103 { 104 level->seed = (int)(GetTime() * 100.0f); 105 106 TowerInit(); 107 EnemyInit(); 108 ProjectileInit(); 109 ParticleInit(); 110 TowerTryAdd(TOWER_TYPE_BASE, 0, 0); 111 112 level->placementMode = 0; 113 level->state = LEVEL_STATE_BUILDING; 114 level->nextState = LEVEL_STATE_NONE; 115 level->playerGold = level->initialGold; 116 level->currentWave = 0; 117 118 Camera *camera = &level->camera; 119 camera->position = (Vector3){4.0f, 8.0f, 8.0f}; 120 camera->target = (Vector3){0.0f, 0.0f, 0.0f}; 121 camera->up = (Vector3){0.0f, 1.0f, 0.0f}; 122 camera->fovy = 10.0f; 123 camera->projection = CAMERA_ORTHOGRAPHIC; 124 } 125 126 void DrawLevelHud(Level *level) 127 { 128 const char *text = TextFormat("Gold: %d", level->playerGold); 129 Font font = GetFontDefault(); 130 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 120, 10}, font.baseSize * 2.0f, 2.0f, BLACK); 131 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 122, 8}, font.baseSize * 2.0f, 2.0f, YELLOW); 132 } 133 134 void DrawLevelReportLostWave(Level *level) 135 { 136 BeginMode3D(level->camera); 137 DrawLevelGround(level); 138 TowerDraw(); 139 EnemyDraw(); 140 ProjectileDraw(); 141 ParticleDraw(); 142 guiState.isBlocked = 0; 143 EndMode3D(); 144 145 TowerDrawHealthBars(level->camera); 146 147 const char *text = "Wave lost"; 148 int textWidth = MeasureText(text, 20); 149 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 150 151 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 152 { 153 level->nextState = LEVEL_STATE_RESET; 154 } 155 } 156 157 int HasLevelNextWave(Level *level) 158 { 159 for (int i = 0; i < 10; i++) 160 { 161 EnemyWave *wave = &level->waves[i]; 162 if (wave->wave == level->currentWave) 163 { 164 return 1; 165 } 166 } 167 return 0; 168 } 169 170 void DrawLevelReportWonWave(Level *level) 171 { 172 BeginMode3D(level->camera); 173 DrawLevelGround(level); 174 TowerDraw(); 175 EnemyDraw(); 176 ProjectileDraw(); 177 ParticleDraw(); 178 guiState.isBlocked = 0; 179 EndMode3D(); 180 181 TowerDrawHealthBars(level->camera); 182 183 const char *text = "Wave won"; 184 int textWidth = MeasureText(text, 20); 185 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 186 187 188 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 189 { 190 level->nextState = LEVEL_STATE_RESET; 191 } 192 193 if (HasLevelNextWave(level)) 194 { 195 if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0)) 196 { 197 level->nextState = LEVEL_STATE_BUILDING; 198 } 199 } 200 else { 201 if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0)) 202 { 203 level->nextState = LEVEL_STATE_WON_LEVEL; 204 } 205 } 206 } 207 208 void DrawBuildingBuildButton(Level *level, int x, int y, int width, int height, uint8_t towerType, const char *name) 209 { 210 static ButtonState buttonStates[8] = {0}; 211 int cost = GetTowerCosts(towerType); 212 const char *text = TextFormat("%s: %d", name, cost); 213 buttonStates[towerType].isSelected = level->placementMode == towerType; 214 buttonStates[towerType].isDisabled = level->playerGold < cost; 215 if (Button(text, x, y, width, height, &buttonStates[towerType])) 216 { 217 level->placementMode = buttonStates[towerType].isSelected ? 0 : towerType; 218 } 219 } 220 221 float GetRandomFloat(float min, float max) 222 { 223 int random = GetRandomValue(0, 0xfffffff); 224 return ((float)random / (float)0xfffffff) * (max - min) + min; 225 } 226 227 void DrawLevelGround(Level *level) 228 { 229 // draw checkerboard ground pattern 230 for (int x = -5; x <= 5; x += 1) 231 { 232 for (int y = -5; y <= 5; y += 1) 233 { 234 Model *model = (x + y) % 2 == 0 ? &floorTileAModel : &floorTileBModel; 235 DrawModel(*model, (Vector3){x, 0.0f, y}, 1.0f, WHITE); 236 } 237 } 238 239 int oldSeed = GetRandomValue(0, 0xfffffff); 240 SetRandomSeed(level->seed); 241 // increase probability for trees via duplicated entries 242 Model borderModels[64]; 243 int maxRockCount = GetRandomValue(2, 6); 244 int maxTreeCount = GetRandomValue(10, 20); 245 int maxFirTreeCount = GetRandomValue(5, 10); 246 int maxLeafTreeCount = maxTreeCount - maxFirTreeCount; 247 int grassPatchCount = GetRandomValue(5, 30); 248 249 int modelCount = 0; 250 for (int i = 0; i < maxRockCount && modelCount < 63; i++) 251 { 252 borderModels[modelCount++] = rockModels[GetRandomValue(0, 5)]; 253 } 254 for (int i = 0; i < maxLeafTreeCount && modelCount < 63; i++) 255 { 256 borderModels[modelCount++] = treeModel[GetRandomValue(0, 1)]; 257 } 258 for (int i = 0; i < maxFirTreeCount && modelCount < 63; i++) 259 { 260 borderModels[modelCount++] = firTreeModel[GetRandomValue(0, 1)]; 261 } 262 for (int i = 0; i < grassPatchCount && modelCount < 63; i++) 263 { 264 borderModels[modelCount++] = grassPatchModel[0]; 265 } 266 267 // draw some objects around the border of the map 268 Vector3 up = {0, 1, 0}; 269 // a pseudo random number generator to get the same result every time 270 const float wiggle = 0.75f; 271 const int layerCount = 3; 272 for (int layer = 0; layer < layerCount; layer++) 273 { 274 int layerPos = 6 + layer; 275 for (int x = -6 + layer; x <= 6 + layer; x += 1) 276 { 277 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)], 278 (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, -layerPos + GetRandomFloat(0.0f, wiggle)}, 279 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE); 280 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)], 281 (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, layerPos + GetRandomFloat(0.0f, wiggle)}, 282 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE); 283 } 284 285 for (int z = -5 + layer; z <= 5 + layer; z += 1) 286 { 287 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)], 288 (Vector3){-layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)}, 289 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE); 290 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)], 291 (Vector3){layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)}, 292 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE); 293 } 294 } 295 296 SetRandomSeed(oldSeed); 297 } 298 299 void DrawEnemyPath(Level *level, Color arrowColor) 300 { 301 const int castleX = 0, castleY = 0; 302 const int maxWaypointCount = 200; 303 const float timeStep = 1.0f; 304 Vector3 arrowScale = {0.75f, 0.75f, 0.75f}; 305 306 // we start with a time offset to simulate the path, 307 // this way the arrows are animated in a forward moving direction 308 // The time is wrapped around the time step to get a smooth animation 309 float timeOffset = fmodf(GetTime(), timeStep); 310 311 for (int i = 0; i < ENEMY_MAX_WAVE_COUNT; i++) 312 { 313 EnemyWave *wave = &level->waves[i]; 314 if (wave->wave != level->currentWave) 315 { 316 continue; 317 } 318 319 // use this dummy enemy to simulate the path 320 Enemy dummy = { 321 .enemyType = ENEMY_TYPE_MINION, 322 .simPosition = (Vector2){wave->spawnPosition.x, wave->spawnPosition.y}, 323 .nextX = wave->spawnPosition.x, 324 .nextY = wave->spawnPosition.y, 325 .currentX = wave->spawnPosition.x, 326 .currentY = wave->spawnPosition.y, 327 }; 328 329 float deltaTime = timeOffset; 330 for (int j = 0; j < maxWaypointCount; j++) 331 { 332 int waypointPassedCount = 0; 333 Vector2 pos = EnemyGetPosition(&dummy, deltaTime, &dummy.simVelocity, &waypointPassedCount); 334 // after the initial variable starting offset, we use a fixed time step 335 deltaTime = timeStep; 336 dummy.simPosition = pos; 337 338 // Update the dummy's position just like we do in the regular enemy update loop 339 for (int k = 0; k < waypointPassedCount; k++) 340 { 341 dummy.currentX = dummy.nextX; 342 dummy.currentY = dummy.nextY; 343 if (EnemyGetNextPosition(dummy.currentX, dummy.currentY, &dummy.nextX, &dummy.nextY) && 344 Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f) 345 { 346 break; 347 } 348 } 349 if (Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f) 350 { 351 break; 352 } 353 354 // get the angle we need to rotate the arrow model. The velocity is just fine for this. 355 float angle = atan2f(dummy.simVelocity.x, dummy.simVelocity.y) * RAD2DEG - 90.0f; 356 DrawModelEx(pathArrowModel, (Vector3){pos.x, 0.15f, pos.y}, (Vector3){0, 1, 0}, angle, arrowScale, arrowColor); 357 } 358 } 359 } 360 361 void DrawLevelBuildingState(Level *level) 362 { 363 BeginMode3D(level->camera); 364 DrawLevelGround(level); 365 366 int blockedCellCount = 0; 367 Vector2 blockedCells[1]; 368 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera); 369 float planeDistance = ray.position.y / -ray.direction.y; 370 float planeX = ray.direction.x * planeDistance + ray.position.x; 371 float planeY = ray.direction.z * planeDistance + ray.position.z; 372 int16_t mapX = (int16_t)floorf(planeX + 0.5f); 373 int16_t mapY = (int16_t)floorf(planeY + 0.5f); 374 if (level->placementMode && !guiState.isBlocked && mapX >= -5 && mapX <= 5 && mapY >= -5 && mapY <= 5) 375 { 376 DrawCubeWires((Vector3){mapX, 0.2f, mapY}, 1.0f, 0.4f, 1.0f, RED); 377 blockedCells[blockedCellCount++] = (Vector2){mapX, mapY}; 378 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) 379 { 380 if (TowerTryAdd(level->placementMode, mapX, mapY)) 381 { 382 level->playerGold -= GetTowerCosts(level->placementMode); 383 level->placementMode = TOWER_TYPE_NONE; 384 } 385 } 386 } 387 388 PathFindingMapUpdate(blockedCellCount, blockedCells); 389 TowerDraw(); 390 EnemyDraw(); 391 ProjectileDraw(); 392 ParticleDraw(); 393 394 // disable depth testing for the path arrows 395 // flush the 3D batch to draw the arrows on top of everything 396 rlDrawRenderBatchActive(); 397 rlDisableDepthTest(); 398 DrawEnemyPath(level, (Color){64, 64, 64, 160}); 399 400 rlDrawRenderBatchActive(); 401 rlEnableDepthTest(); 402 DrawEnemyPath(level, WHITE); 403 404 guiState.isBlocked = 0; 405 406 EndMode3D(); 407 408 TowerDrawHealthBars(level->camera); 409 410 static ButtonState buildWallButtonState = {0}; 411 static ButtonState buildGunButtonState = {0}; 412 buildWallButtonState.isSelected = level->placementMode == TOWER_TYPE_WALL; 413 buildGunButtonState.isSelected = level->placementMode == TOWER_TYPE_ARCHER; 414 415 DrawBuildingBuildButton(level, 10, 10, 110, 30, TOWER_TYPE_WALL, "Wall"); 416 DrawBuildingBuildButton(level, 10, 50, 110, 30, TOWER_TYPE_ARCHER, "Archer"); 417 DrawBuildingBuildButton(level, 10, 90, 110, 30, TOWER_TYPE_BALLISTA, "Ballista"); 418 DrawBuildingBuildButton(level, 10, 130, 110, 30, TOWER_TYPE_CATAPULT, "Catapult"); 419 420 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 421 { 422 level->nextState = LEVEL_STATE_RESET; 423 } 424 425 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0)) 426 { 427 level->nextState = LEVEL_STATE_BATTLE; 428 } 429 430 const char *text = "Building phase"; 431 int textWidth = MeasureText(text, 20); 432 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 433 } 434 435 void InitBattleStateConditions(Level *level) 436 { 437 level->state = LEVEL_STATE_BATTLE; 438 level->nextState = LEVEL_STATE_NONE; 439 level->waveEndTimer = 0.0f; 440 for (int i = 0; i < 10; i++) 441 { 442 EnemyWave *wave = &level->waves[i]; 443 wave->spawned = 0; 444 wave->timeToSpawnNext = wave->delay; 445 } 446 } 447 448 void DrawLevelBattleState(Level *level) 449 { 450 BeginMode3D(level->camera); 451 DrawLevelGround(level); 452 TowerDraw(); 453 EnemyDraw(); 454 ProjectileDraw(); 455 ParticleDraw(); 456 guiState.isBlocked = 0; 457 EndMode3D(); 458 459 EnemyDrawHealthbars(level->camera); 460 TowerDrawHealthBars(level->camera); 461 462 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 463 { 464 level->nextState = LEVEL_STATE_RESET; 465 } 466 467 int maxCount = 0; 468 int remainingCount = 0; 469 for (int i = 0; i < 10; i++) 470 { 471 EnemyWave *wave = &level->waves[i]; 472 if (wave->wave != level->currentWave) 473 { 474 continue; 475 } 476 maxCount += wave->count; 477 remainingCount += wave->count - wave->spawned; 478 } 479 int aliveCount = EnemyCount(); 480 remainingCount += aliveCount; 481 482 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount); 483 int textWidth = MeasureText(text, 20); 484 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 485 } 486 487 void DrawLevel(Level *level) 488 { 489 switch (level->state) 490 { 491 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break; 492 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break; 493 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break; 494 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break; 495 default: break; 496 } 497 498 DrawLevelHud(level); 499 } 500 501 void UpdateLevel(Level *level) 502 { 503 if (level->state == LEVEL_STATE_BATTLE) 504 { 505 int activeWaves = 0; 506 for (int i = 0; i < 10; i++) 507 { 508 EnemyWave *wave = &level->waves[i]; 509 if (wave->spawned >= wave->count || wave->wave != level->currentWave) 510 { 511 continue; 512 } 513 activeWaves++; 514 wave->timeToSpawnNext -= gameTime.deltaTime; 515 if (wave->timeToSpawnNext <= 0.0f) 516 { 517 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y); 518 if (enemy) 519 { 520 wave->timeToSpawnNext = wave->interval; 521 wave->spawned++; 522 } 523 } 524 } 525 if (GetTowerByType(TOWER_TYPE_BASE) == 0) { 526 level->waveEndTimer += gameTime.deltaTime; 527 if (level->waveEndTimer >= 2.0f) 528 { 529 level->nextState = LEVEL_STATE_LOST_WAVE; 530 } 531 } 532 else if (activeWaves == 0 && EnemyCount() == 0) 533 { 534 level->waveEndTimer += gameTime.deltaTime; 535 if (level->waveEndTimer >= 2.0f) 536 { 537 level->nextState = LEVEL_STATE_WON_WAVE; 538 } 539 } 540 } 541 542 PathFindingMapUpdate(0, 0); 543 EnemyUpdate(); 544 TowerUpdate(); 545 ProjectileUpdate(); 546 ParticleUpdate(); 547 548 if (level->nextState == LEVEL_STATE_RESET) 549 { 550 InitLevel(level); 551 } 552 553 if (level->nextState == LEVEL_STATE_BATTLE) 554 { 555 InitBattleStateConditions(level); 556 } 557 558 if (level->nextState == LEVEL_STATE_WON_WAVE) 559 { 560 level->currentWave++; 561 level->state = LEVEL_STATE_WON_WAVE; 562 } 563 564 if (level->nextState == LEVEL_STATE_LOST_WAVE) 565 { 566 level->state = LEVEL_STATE_LOST_WAVE; 567 } 568 569 if (level->nextState == LEVEL_STATE_BUILDING) 570 { 571 level->state = LEVEL_STATE_BUILDING; 572 } 573 574 if (level->nextState == LEVEL_STATE_WON_LEVEL) 575 { 576 // make something of this later 577 InitLevel(level); 578 } 579 580 level->nextState = LEVEL_STATE_NONE; 581 } 582 583 float nextSpawnTime = 0.0f; 584 585 void ResetGame() 586 { 587 InitLevel(currentLevel); 588 } 589 590 void InitGame() 591 { 592 TowerInit(); 593 EnemyInit(); 594 ProjectileInit(); 595 ParticleInit(); 596 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f); 597 598 currentLevel = levels; 599 InitLevel(currentLevel); 600 } 601 602 //# Immediate GUI functions 603 604 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth) 605 { 606 const float healthBarHeight = 6.0f; 607 const float healthBarOffset = 15.0f; 608 const float inset = 2.0f; 609 const float innerWidth = healthBarWidth - inset * 2; 610 const float innerHeight = healthBarHeight - inset * 2; 611 612 Vector2 screenPos = GetWorldToScreen(position, camera); 613 float centerX = screenPos.x - healthBarWidth * 0.5f; 614 float topY = screenPos.y - healthBarOffset; 615 DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK); 616 float healthWidth = innerWidth * healthRatio; 617 DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor); 618 } 619 620 int Button(const char *text, int x, int y, int width, int height, ButtonState *state) 621 { 622 Rectangle bounds = {x, y, width, height}; 623 int isPressed = 0; 624 int isSelected = state && state->isSelected; 625 int isDisabled = state && state->isDisabled; 626 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !isDisabled) 627 { 628 Color color = isSelected ? DARKGRAY : GRAY; 629 DrawRectangle(x, y, width, height, color); 630 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) 631 { 632 isPressed = 1; 633 } 634 guiState.isBlocked = 1; 635 } 636 else 637 { 638 Color color = isSelected ? WHITE : LIGHTGRAY; 639 DrawRectangle(x, y, width, height, color); 640 } 641 Font font = GetFontDefault(); 642 Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1); 643 Color textColor = isDisabled ? GRAY : BLACK; 644 DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, textColor); 645 return isPressed; 646 } 647 648 //# Main game loop 649 650 void GameUpdate() 651 { 652 float dt = GetFrameTime(); 653 // cap maximum delta time to 0.1 seconds to prevent large time steps 654 if (dt > 0.1f) dt = 0.1f; 655 gameTime.time += dt; 656 gameTime.deltaTime = dt; 657 658 UpdateLevel(currentLevel); 659 } 660 661 int main(void) 662 { 663 int screenWidth, screenHeight; 664 GetPreferredSize(&screenWidth, &screenHeight); 665 InitWindow(screenWidth, screenHeight, "Tower defense"); 666 SetTargetFPS(30); 667 668 LoadAssets(); 669 InitGame(); 670 671 while (!WindowShouldClose()) 672 { 673 if (IsPaused()) { 674 // canvas is not visible in browser - do nothing 675 continue; 676 } 677 678 BeginDrawing(); 679 ClearBackground((Color){0x4E, 0x63, 0x26, 0xFF}); 680 681 GameUpdate(); 682 DrawLevel(currentLevel); 683 684 EndDrawing(); 685 } 686 687 CloseWindow(); 688 689 return 0; 690 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 
  4 // The queue is a simple array of nodes, we add nodes to the end and remove
  5 // nodes from the front. We keep the array around to avoid unnecessary allocations
  6 static PathfindingNode *pathfindingNodeQueue = 0;
  7 static int pathfindingNodeQueueCount = 0;
  8 static int pathfindingNodeQueueCapacity = 0;
  9 
 10 // The pathfinding map stores the distances from the castle to each cell in the map.
 11 static PathfindingMap pathfindingMap = {0};
 12 
 13 void PathfindingMapInit(int width, int height, Vector3 translate, float scale)
 14 {
 15   // transforming between map space and world space allows us to adapt 
 16   // position and scale of the map without changing the pathfinding data
 17   pathfindingMap.toWorldSpace = MatrixTranslate(translate.x, translate.y, translate.z);
 18   pathfindingMap.toWorldSpace = MatrixMultiply(pathfindingMap.toWorldSpace, MatrixScale(scale, scale, scale));
 19   pathfindingMap.toMapSpace = MatrixInvert(pathfindingMap.toWorldSpace);
 20   pathfindingMap.width = width;
 21   pathfindingMap.height = height;
 22   pathfindingMap.scale = scale;
 23   pathfindingMap.distances = (float *)MemAlloc(width * height * sizeof(float));
 24   for (int i = 0; i < width * height; i++)
 25   {
 26     pathfindingMap.distances[i] = -1.0f;
 27   }
 28 
 29   pathfindingMap.towerIndex = (long *)MemAlloc(width * height * sizeof(long));
 30   pathfindingMap.deltaSrc = (DeltaSrc *)MemAlloc(width * height * sizeof(DeltaSrc));
 31 }
 32 
 33 static void PathFindingNodePush(int16_t x, int16_t y, int16_t fromX, int16_t fromY, float distance)
 34 {
 35   if (pathfindingNodeQueueCount >= pathfindingNodeQueueCapacity)
 36   {
 37     pathfindingNodeQueueCapacity = pathfindingNodeQueueCapacity == 0 ? 256 : pathfindingNodeQueueCapacity * 2;
 38     // we use MemAlloc/MemRealloc to allocate memory for the queue
 39     // I am not entirely sure if MemRealloc allows passing a null pointer
 40     // so we check if the pointer is null and use MemAlloc in that case
 41     if (pathfindingNodeQueue == 0)
 42     {
 43       pathfindingNodeQueue = (PathfindingNode *)MemAlloc(pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
 44     }
 45     else
 46     {
 47       pathfindingNodeQueue = (PathfindingNode *)MemRealloc(pathfindingNodeQueue, pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
 48     }
 49   }
 50 
 51   PathfindingNode *node = &pathfindingNodeQueue[pathfindingNodeQueueCount++];
 52   node->x = x;
 53   node->y = y;
 54   node->fromX = fromX;
 55   node->fromY = fromY;
 56   node->distance = distance;
 57 }
 58 
 59 static PathfindingNode *PathFindingNodePop()
 60 {
 61   if (pathfindingNodeQueueCount == 0)
 62   {
 63     return 0;
 64   }
 65   // we return the first node in the queue; we want to return a pointer to the node
 66   // so we can return 0 if the queue is empty. 
 67   // We should _not_ return a pointer to the element in the list, because the list
 68   // may be reallocated and the pointer would become invalid. Or the 
 69   // popped element is overwritten by the next push operation.
 70   // Using static here means that the variable is permanently allocated.
 71   static PathfindingNode node;
 72   node = pathfindingNodeQueue[0];
 73   // we shift all nodes one position to the front
 74   for (int i = 1; i < pathfindingNodeQueueCount; i++)
 75   {
 76     pathfindingNodeQueue[i - 1] = pathfindingNodeQueue[i];
 77   }
 78   --pathfindingNodeQueueCount;
 79   return &node;
 80 }
 81 
 82 float PathFindingGetDistance(int mapX, int mapY)
 83 {
 84   if (mapX < 0 || mapX >= pathfindingMap.width || mapY < 0 || mapY >= pathfindingMap.height)
 85   {
 86     // when outside the map, we return the manhattan distance to the castle (0,0)
 87     return fabsf((float)mapX) + fabsf((float)mapY);
 88   }
 89 
 90   return pathfindingMap.distances[mapY * pathfindingMap.width + mapX];
 91 }
 92 
 93 // transform a world position to a map position in the array; 
 94 // returns true if the position is inside the map
 95 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY)
 96 {
 97   Vector3 mapPosition = Vector3Transform(worldPosition, pathfindingMap.toMapSpace);
 98   *mapX = (int16_t)mapPosition.x;
 99   *mapY = (int16_t)mapPosition.z;
100   return *mapX >= 0 && *mapX < pathfindingMap.width && *mapY >= 0 && *mapY < pathfindingMap.height;
101 }
102 
103 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells)
104 {
105   const int castleX = 0, castleY = 0;
106   int16_t castleMapX, castleMapY;
107   if (!PathFindingFromWorldToMapPosition((Vector3){castleX, 0.0f, castleY}, &castleMapX, &castleMapY))
108   {
109     return;
110   }
111   int width = pathfindingMap.width, height = pathfindingMap.height;
112 
113   // reset the distances to -1
114   for (int i = 0; i < width * height; i++)
115   {
116     pathfindingMap.distances[i] = -1.0f;
117   }
118   // reset the tower indices
119   for (int i = 0; i < width * height; i++)
120   {
121     pathfindingMap.towerIndex[i] = -1;
122   }
123   // reset the delta src
124   for (int i = 0; i < width * height; i++)
125   {
126     pathfindingMap.deltaSrc[i].x = 0;
127     pathfindingMap.deltaSrc[i].y = 0;
128   }
129 
130   for (int i = 0; i < blockedCellCount; i++)
131   {
132     int16_t mapX, mapY;
133     if (!PathFindingFromWorldToMapPosition((Vector3){blockedCells[i].x, 0.0f, blockedCells[i].y}, &mapX, &mapY))
134     {
135       continue;
136     }
137     int index = mapY * width + mapX;
138     pathfindingMap.towerIndex[index] = -2;
139   }
140 
141   for (int i = 0; i < towerCount; i++)
142   {
143     Tower *tower = &towers[i];
144     if (tower->towerType == TOWER_TYPE_NONE || tower->towerType == TOWER_TYPE_BASE)
145     {
146       continue;
147     }
148     int16_t mapX, mapY;
149     // technically, if the tower cell scale is not in sync with the pathfinding map scale,
150     // this would not work correctly and needs to be refined to allow towers covering multiple cells
151     // or having multiple towers in one cell; for simplicity, we assume that the tower covers exactly
152     // one cell. For now.
153     if (!PathFindingFromWorldToMapPosition((Vector3){tower->x, 0.0f, tower->y}, &mapX, &mapY))
154     {
155       continue;
156     }
157     int index = mapY * width + mapX;
158     pathfindingMap.towerIndex[index] = i;
159   }
160 
161   // we start at the castle and add the castle to the queue
162   pathfindingMap.maxDistance = 0.0f;
163   pathfindingNodeQueueCount = 0;
164   PathFindingNodePush(castleMapX, castleMapY, castleMapX, castleMapY, 0.0f);
165   PathfindingNode *node = 0;
166   while ((node = PathFindingNodePop()))
167   {
168     if (node->x < 0 || node->x >= width || node->y < 0 || node->y >= height)
169     {
170       continue;
171     }
172     int index = node->y * width + node->x;
173     if (pathfindingMap.distances[index] >= 0 && pathfindingMap.distances[index] <= node->distance)
174     {
175       continue;
176     }
177 
178     int deltaX = node->x - node->fromX;
179     int deltaY = node->y - node->fromY;
180     // even if the cell is blocked by a tower, we still may want to store the direction
181     // (though this might not be needed, IDK right now)
182     pathfindingMap.deltaSrc[index].x = (char) deltaX;
183     pathfindingMap.deltaSrc[index].y = (char) deltaY;
184 
185     // we skip nodes that are blocked by towers or by the provided blocked cells
186     if (pathfindingMap.towerIndex[index] != -1)
187     {
188       node->distance += 8.0f;
189     }
190     pathfindingMap.distances[index] = node->distance;
191     pathfindingMap.maxDistance = fmaxf(pathfindingMap.maxDistance, node->distance);
192     PathFindingNodePush(node->x, node->y + 1, node->x, node->y, node->distance + 1.0f);
193     PathFindingNodePush(node->x, node->y - 1, node->x, node->y, node->distance + 1.0f);
194     PathFindingNodePush(node->x + 1, node->y, node->x, node->y, node->distance + 1.0f);
195     PathFindingNodePush(node->x - 1, node->y, node->x, node->y, node->distance + 1.0f);
196   }
197 }
198 
199 void PathFindingMapDraw()
200 {
201   float cellSize = pathfindingMap.scale * 0.9f;
202   float highlightDistance = fmodf(GetTime() * 4.0f, pathfindingMap.maxDistance);
203   for (int x = 0; x < pathfindingMap.width; x++)
204   {
205     for (int y = 0; y < pathfindingMap.height; y++)
206     {
207       float distance = pathfindingMap.distances[y * pathfindingMap.width + x];
208       float colorV = distance < 0 ? 0 : fminf(distance / pathfindingMap.maxDistance, 1.0f);
209       Color color = distance < 0 ? BLUE : (Color){fminf(colorV, 1.0f) * 255, 0, 0, 255};
210       Vector3 position = Vector3Transform((Vector3){x, -0.25f, y}, pathfindingMap.toWorldSpace);
211       // animate the distance "wave" to show how the pathfinding algorithm expands
212       // from the castle
213       if (distance + 0.5f > highlightDistance && distance - 0.5f < highlightDistance)
214       {
215         color = BLACK;
216       }
217       DrawCube(position, cellSize, 0.1f, cellSize, color);
218     }
219   }
220 }
221 
222 Vector2 PathFindingGetGradient(Vector3 world)
223 {
224   int16_t mapX, mapY;
225   if (PathFindingFromWorldToMapPosition(world, &mapX, &mapY))
226   {
227     DeltaSrc delta = pathfindingMap.deltaSrc[mapY * pathfindingMap.width + mapX];
228     return (Vector2){(float)-delta.x, (float)-delta.y};
229   }
230   // fallback to a simple gradient calculation
231   float n = PathFindingGetDistance(mapX, mapY - 1);
232   float s = PathFindingGetDistance(mapX, mapY + 1);
233   float w = PathFindingGetDistance(mapX - 1, mapY);
234   float e = PathFindingGetDistance(mapX + 1, mapY);
235   return (Vector2){w - e + 0.25f, n - s + 0.125f};
236 }
  1 #ifndef TD_TUT_2_MAIN_H
  2 #define TD_TUT_2_MAIN_H
  3 
  4 #include <inttypes.h>
  5 
  6 #include "raylib.h"
  7 #include "preferred_size.h"
  8 
  9 //# Declarations
 10 
 11 #define ENEMY_MAX_PATH_COUNT 8
 12 #define ENEMY_MAX_COUNT 400
 13 #define ENEMY_TYPE_NONE 0
 14 #define ENEMY_TYPE_MINION 1
 15 
 16 #define PARTICLE_MAX_COUNT 400
 17 #define PARTICLE_TYPE_NONE 0
 18 #define PARTICLE_TYPE_EXPLOSION 1
 19 
 20 typedef struct Particle
 21 {
 22   uint8_t particleType;
 23   float spawnTime;
 24   float lifetime;
 25   Vector3 position;
 26   Vector3 velocity;
 27   Vector3 scale;
 28 } Particle;
 29 
 30 #define TOWER_MAX_COUNT 400
 31 enum TowerType
 32 {
 33   TOWER_TYPE_NONE,
 34   TOWER_TYPE_BASE,
 35   TOWER_TYPE_ARCHER,
 36   TOWER_TYPE_BALLISTA,
 37   TOWER_TYPE_CATAPULT,
 38   TOWER_TYPE_WALL,
 39   TOWER_TYPE_COUNT
 40 };
 41 
 42 typedef struct HitEffectConfig
 43 {
 44   float damage;
 45   float areaDamageRadius;
 46   float pushbackPowerDistance;
 47 } HitEffectConfig;
 48 
 49 typedef struct TowerTypeConfig
 50 {
 51   float cooldown;
 52   float range;
 53   float projectileSpeed;
 54   
 55   uint8_t cost;
 56   uint8_t projectileType;
 57   uint16_t maxHealth;
 58 
 59   HitEffectConfig hitEffect;
 60 } TowerTypeConfig;
 61 
 62 typedef struct Tower
 63 {
 64   int16_t x, y;
 65   uint8_t towerType;
 66   Vector2 lastTargetPosition;
 67   float cooldown;
 68   float damage;
 69 } Tower;
 70 
 71 typedef struct GameTime
 72 {
 73   float time;
 74   float deltaTime;
 75 } GameTime;
 76 
 77 typedef struct ButtonState {
 78   char isSelected;
 79   char isDisabled;
 80 } ButtonState;
 81 
 82 typedef struct GUIState {
 83   int isBlocked;
 84 } GUIState;
 85 
 86 typedef enum LevelState
 87 {
 88   LEVEL_STATE_NONE,
 89   LEVEL_STATE_BUILDING,
 90   LEVEL_STATE_BATTLE,
 91   LEVEL_STATE_WON_WAVE,
 92   LEVEL_STATE_LOST_WAVE,
 93   LEVEL_STATE_WON_LEVEL,
 94   LEVEL_STATE_RESET,
 95 } LevelState;
 96 
 97 typedef struct EnemyWave {
 98   uint8_t enemyType;
 99   uint8_t wave;
100   uint16_t count;
101   float interval;
102   float delay;
103   Vector2 spawnPosition;
104 
105   uint16_t spawned;
106   float timeToSpawnNext;
107 } EnemyWave;
108 
109 #define ENEMY_MAX_WAVE_COUNT 10
110 
111 typedef struct Level
112 {
113   int seed;
114   LevelState state;
115   LevelState nextState;
116   Camera3D camera;
117   int placementMode;
118 
119   int initialGold;
120   int playerGold;
121 
122   EnemyWave waves[ENEMY_MAX_WAVE_COUNT];
123   int currentWave;
124   float waveEndTimer;
125 } Level;
126 
127 typedef struct DeltaSrc
128 {
129   char x, y;
130 } DeltaSrc;
131 
132 typedef struct PathfindingMap
133 {
134   int width, height;
135   float scale;
136   float *distances;
137   long *towerIndex; 
138   DeltaSrc *deltaSrc;
139   float maxDistance;
140   Matrix toMapSpace;
141   Matrix toWorldSpace;
142 } PathfindingMap;
143 
144 // when we execute the pathfinding algorithm, we need to store the active nodes
145 // in a queue. Each node has a position, a distance from the start, and the
146 // position of the node that we came from.
147 typedef struct PathfindingNode
148 {
149   int16_t x, y, fromX, fromY;
150   float distance;
151 } PathfindingNode;
152 
153 typedef struct EnemyId
154 {
155   uint16_t index;
156   uint16_t generation;
157 } EnemyId;
158 
159 typedef struct EnemyClassConfig
160 {
161   float speed;
162   float health;
163   float radius;
164   float maxAcceleration;
165   float requiredContactTime;
166   float explosionDamage;
167   float explosionRange;
168   float explosionPushbackPower;
169   int goldValue;
170 } EnemyClassConfig;
171 
172 typedef struct Enemy
173 {
174   int16_t currentX, currentY;
175   int16_t nextX, nextY;
176   Vector2 simPosition;
177   Vector2 simVelocity;
178   uint16_t generation;
179   float walkedDistance;
180   float startMovingTime;
181   float damage, futureDamage;
182   float contactTime;
183   uint8_t enemyType;
184   uint8_t movePathCount;
185   Vector2 movePath[ENEMY_MAX_PATH_COUNT];
186 } Enemy;
187 
188 // a unit that uses sprites to be drawn
189 #define SPRITE_UNIT_PHASE_WEAPON_IDLE 0
190 #define SPRITE_UNIT_PHASE_WEAPON_COOLDOWN 1
191 typedef struct SpriteUnit
192 {
193   Rectangle srcRect;
194   Vector2 offset;
195   int frameCount;
196   float frameDuration;
197   Rectangle srcWeaponIdleRect;
198   Vector2 srcWeaponIdleOffset;
199   Rectangle srcWeaponCooldownRect;
200   Vector2 srcWeaponCooldownOffset;
201 } SpriteUnit;
202 
203 #define PROJECTILE_MAX_COUNT 1200
204 #define PROJECTILE_TYPE_NONE 0
205 #define PROJECTILE_TYPE_ARROW 1
206 #define PROJECTILE_TYPE_CATAPULT 2
207 #define PROJECTILE_TYPE_BALLISTA 3
208 
209 typedef struct Projectile
210 {
211   uint8_t projectileType;
212   float shootTime;
213   float arrivalTime;
214   float distance;
215   Vector3 position;
216   Vector3 target;
217   Vector3 directionNormal;
218   EnemyId targetEnemy;
219   HitEffectConfig hitEffectConfig;
220 } Projectile;
221 
222 //# Function declarations
223 float TowerGetMaxHealth(Tower *tower);
224 int Button(const char *text, int x, int y, int width, int height, ButtonState *state);
225 int EnemyAddDamageRange(Vector2 position, float range, float damage);
226 int EnemyAddDamage(Enemy *enemy, float damage);
227 
228 //# Enemy functions
229 void EnemyInit();
230 void EnemyDraw();
231 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource);
232 void EnemyUpdate();
233 float EnemyGetCurrentMaxSpeed(Enemy *enemy);
234 float EnemyGetMaxHealth(Enemy *enemy);
235 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY);
236 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount);
237 EnemyId EnemyGetId(Enemy *enemy);
238 Enemy *EnemyTryResolve(EnemyId enemyId);
239 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY);
240 int EnemyAddDamage(Enemy *enemy, float damage);
241 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range);
242 int EnemyCount();
243 void EnemyDrawHealthbars(Camera3D camera);
244 
245 //# Tower functions
246 void TowerInit();
247 Tower *TowerGetAt(int16_t x, int16_t y);
248 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y);
249 Tower *GetTowerByType(uint8_t towerType);
250 int GetTowerCosts(uint8_t towerType);
251 float TowerGetMaxHealth(Tower *tower);
252 void TowerDraw();
253 void TowerUpdate();
254 void TowerDrawHealthBars(Camera3D camera);
255 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase);
256 
257 //# Particles
258 void ParticleInit();
259 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime);
260 void ParticleUpdate();
261 void ParticleDraw();
262 
263 //# Projectiles
264 void ProjectileInit();
265 void ProjectileDraw();
266 void ProjectileUpdate();
267 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig);
268 
269 //# Pathfinding map
270 void PathfindingMapInit(int width, int height, Vector3 translate, float scale);
271 float PathFindingGetDistance(int mapX, int mapY);
272 Vector2 PathFindingGetGradient(Vector3 world);
273 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY);
274 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells);
275 void PathFindingMapDraw();
276 
277 //# UI
278 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth);
279 
280 //# Level
281 void DrawLevelGround(Level *level);
282 void DrawEnemyPath(Level *level, Color arrowColor);
283 
284 //# variables
285 extern Level *currentLevel;
286 extern Enemy enemies[ENEMY_MAX_COUNT];
287 extern int enemyCount;
288 extern EnemyClassConfig enemyClassConfigs[];
289 
290 extern GUIState guiState;
291 extern GameTime gameTime;
292 extern Tower towers[TOWER_MAX_COUNT];
293 extern int towerCount;
294 
295 extern Texture2D palette, spriteSheet;
296 
297 #endif
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 
  4 static TowerTypeConfig towerTypeConfigs[TOWER_TYPE_COUNT] = {
  5     [TOWER_TYPE_BASE] = {
  6         .maxHealth = 10,
  7     },
  8     [TOWER_TYPE_ARCHER] = {
  9         .cooldown = 0.5f,
 10         .range = 3.0f,
 11         .cost = 6,
 12         .maxHealth = 10,
 13         .projectileSpeed = 4.0f,
 14         .projectileType = PROJECTILE_TYPE_ARROW,
 15         .hitEffect = {
 16           .damage = 3.0f,
 17         }
 18     },
 19     [TOWER_TYPE_BALLISTA] = {
 20         .cooldown = 1.5f,
 21         .range = 6.0f,
 22         .cost = 9,
 23         .maxHealth = 10,
 24         .projectileSpeed = 10.0f,
 25         .projectileType = PROJECTILE_TYPE_BALLISTA,
 26         .hitEffect = {
 27           .damage = 6.0f,
 28           .pushbackPowerDistance = 0.25f,
 29         }
 30     },
 31     [TOWER_TYPE_CATAPULT] = {
 32         .cooldown = 1.7f,
 33         .range = 5.0f,
 34         .cost = 10,
 35         .maxHealth = 10,
 36         .projectileSpeed = 3.0f,
 37         .projectileType = PROJECTILE_TYPE_CATAPULT,
 38         .hitEffect = {
 39           .damage = 2.0f,
 40           .areaDamageRadius = 1.75f,
 41         }
 42     },
 43     [TOWER_TYPE_WALL] = {
 44         .cost = 2,
 45         .maxHealth = 10,
 46     },
 47 };
 48 
 49 Tower towers[TOWER_MAX_COUNT];
 50 int towerCount = 0;
 51 
 52 Model towerModels[TOWER_TYPE_COUNT];
 53 
 54 // definition of our archer unit
 55 SpriteUnit archerUnit = {
 56     .srcRect = {0, 0, 16, 16},
 57     .offset = {7, 1},
 58     .frameCount = 1,
 59     .frameDuration = 0.0f,
 60     .srcWeaponIdleRect = {16, 0, 6, 16},
 61     .srcWeaponIdleOffset = {8, 0},
 62     .srcWeaponCooldownRect = {22, 0, 11, 16},
 63     .srcWeaponCooldownOffset = {10, 0},
 64 };
 65 
 66 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase)
 67 {
 68   float xScale = flip ? -1.0f : 1.0f;
 69   Camera3D camera = currentLevel->camera;
 70   float size = 0.5f;
 71   Vector2 offset = (Vector2){ unit.offset.x / 16.0f * size, unit.offset.y / 16.0f * size * xScale };
 72   Vector2 scale = (Vector2){ unit.srcRect.width / 16.0f * size, unit.srcRect.height / 16.0f * size };
 73   // we want the sprite to face the camera, so we need to calculate the up vector
 74   Vector3 forward = Vector3Subtract(camera.target, camera.position);
 75   Vector3 up = {0, 1, 0};
 76   Vector3 right = Vector3CrossProduct(forward, up);
 77   up = Vector3Normalize(Vector3CrossProduct(right, forward));
 78 
 79   Rectangle srcRect = unit.srcRect;
 80   if (unit.frameCount > 1)
 81   {
 82     srcRect.x += (int)(t / unit.frameDuration) % unit.frameCount * srcRect.width;
 83   }
 84   if (flip)
 85   {
 86     srcRect.x += srcRect.width;
 87     srcRect.width = -srcRect.width;
 88   }
 89   DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
 90 
 91   if (phase == SPRITE_UNIT_PHASE_WEAPON_COOLDOWN && unit.srcWeaponCooldownRect.width > 0)
 92   {
 93     offset = (Vector2){ unit.srcWeaponCooldownOffset.x / 16.0f * size, unit.srcWeaponCooldownOffset.y / 16.0f * size };
 94     scale = (Vector2){ unit.srcWeaponCooldownRect.width / 16.0f * size, unit.srcWeaponCooldownRect.height / 16.0f * size };
 95     srcRect = unit.srcWeaponCooldownRect;
 96     if (flip)
 97     {
 98       // position.x = flip * scale.x * 0.5f;
 99       srcRect.x += srcRect.width;
100       srcRect.width = -srcRect.width;
101       offset.x = scale.x - offset.x;
102     }
103     DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
104   }
105   else if (phase == SPRITE_UNIT_PHASE_WEAPON_IDLE && unit.srcWeaponIdleRect.width > 0)
106   {
107     offset = (Vector2){ unit.srcWeaponIdleOffset.x / 16.0f * size, unit.srcWeaponIdleOffset.y / 16.0f * size };
108     scale = (Vector2){ unit.srcWeaponIdleRect.width / 16.0f * size, unit.srcWeaponIdleRect.height / 16.0f * size };
109     srcRect = unit.srcWeaponIdleRect;
110     if (flip)
111     {
112       // position.x = flip * scale.x * 0.5f;
113       srcRect.x += srcRect.width;
114       srcRect.width = -srcRect.width;
115       offset.x = scale.x - offset.x;
116     }
117     DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
118   }
119 }
120 
121 void TowerInit()
122 {
123   for (int i = 0; i < TOWER_MAX_COUNT; i++)
124   {
125     towers[i] = (Tower){0};
126   }
127   towerCount = 0;
128 
129   towerModels[TOWER_TYPE_BASE] = LoadModel("data/keep.glb");
130   towerModels[TOWER_TYPE_WALL] = LoadModel("data/wall-0000.glb");
131 
132   for (int i = 0; i < TOWER_TYPE_COUNT; i++)
133   {
134     if (towerModels[i].materials)
135     {
136       // assign the palette texture to the material of the model (0 is not used afaik)
137       towerModels[i].materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
138     }
139   }
140 }
141 
142 static void TowerGunUpdate(Tower *tower)
143 {
144   TowerTypeConfig config = towerTypeConfigs[tower->towerType];
145   if (tower->cooldown <= 0.0f)
146   {
147     Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, config.range);
148     if (enemy)
149     {
150       tower->cooldown = config.cooldown;
151       // shoot the enemy; determine future position of the enemy
152       float bulletSpeed = config.projectileSpeed;
153       Vector2 velocity = enemy->simVelocity;
154       Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0);
155       Vector2 towerPosition = {tower->x, tower->y};
156       float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed;
157       for (int i = 0; i < 8; i++) {
158         velocity = enemy->simVelocity;
159         futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0);
160         float distance = Vector2Distance(towerPosition, futurePosition);
161         float eta2 = distance / bulletSpeed;
162         if (fabs(eta - eta2) < 0.01f) {
163           break;
164         }
165         eta = (eta2 + eta) * 0.5f;
166       }
167 
168       ProjectileTryAdd(config.projectileType, enemy, 
169         (Vector3){towerPosition.x, 1.33f, towerPosition.y}, 
170         (Vector3){futurePosition.x, 0.25f, futurePosition.y},
171         bulletSpeed, config.hitEffect);
172       enemy->futureDamage += config.hitEffect.damage;
173       tower->lastTargetPosition = futurePosition;
174     }
175   }
176   else
177   {
178     tower->cooldown -= gameTime.deltaTime;
179   }
180 }
181 
182 Tower *TowerGetAt(int16_t x, int16_t y)
183 {
184   for (int i = 0; i < towerCount; i++)
185   {
186     if (towers[i].x == x && towers[i].y == y && towers[i].towerType != TOWER_TYPE_NONE)
187     {
188       return &towers[i];
189     }
190   }
191   return 0;
192 }
193 
194 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y)
195 {
196   if (towerCount >= TOWER_MAX_COUNT)
197   {
198     return 0;
199   }
200 
201   Tower *tower = TowerGetAt(x, y);
202   if (tower)
203   {
204     return 0;
205   }
206 
207   tower = &towers[towerCount++];
208   tower->x = x;
209   tower->y = y;
210   tower->towerType = towerType;
211   tower->cooldown = 0.0f;
212   tower->damage = 0.0f;
213   return tower;
214 }
215 
216 Tower *GetTowerByType(uint8_t towerType)
217 {
218   for (int i = 0; i < towerCount; i++)
219   {
220     if (towers[i].towerType == towerType)
221     {
222       return &towers[i];
223     }
224   }
225   return 0;
226 }
227 
228 int GetTowerCosts(uint8_t towerType)
229 {
230   return towerTypeConfigs[towerType].cost;
231 }
232 
233 float TowerGetMaxHealth(Tower *tower)
234 {
235   return towerTypeConfigs[tower->towerType].maxHealth;
236 }
237 
238 void TowerDraw()
239 {
240   for (int i = 0; i < towerCount; i++)
241   {
242     Tower tower = towers[i];
243     if (tower.towerType == TOWER_TYPE_NONE)
244     {
245       continue;
246     }
247 
248     switch (tower.towerType)
249     {
250     case TOWER_TYPE_ARCHER:
251       {
252         Vector2 screenPosTower = GetWorldToScreen((Vector3){tower.x, 0.0f, tower.y}, currentLevel->camera);
253         Vector2 screenPosTarget = GetWorldToScreen((Vector3){tower.lastTargetPosition.x, 0.0f, tower.lastTargetPosition.y}, currentLevel->camera);
254         DrawModel(towerModels[TOWER_TYPE_WALL], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
255         DrawSpriteUnit(archerUnit, (Vector3){tower.x, 1.0f, tower.y}, 0, screenPosTarget.x > screenPosTower.x, 
256           tower.cooldown > 0.2f ? SPRITE_UNIT_PHASE_WEAPON_COOLDOWN : SPRITE_UNIT_PHASE_WEAPON_IDLE);
257       }
258       break;
259     case TOWER_TYPE_BALLISTA:
260       DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, BROWN);
261       break;
262     case TOWER_TYPE_CATAPULT:
263       DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, DARKGRAY);
264       break;
265     default:
266       if (towerModels[tower.towerType].materials)
267       {
268         DrawModel(towerModels[tower.towerType], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
269       } else {
270         DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY);
271       }
272       break;
273     }
274   }
275 }
276 
277 void TowerUpdate()
278 {
279   for (int i = 0; i < towerCount; i++)
280   {
281     Tower *tower = &towers[i];
282     switch (tower->towerType)
283     {
284     case TOWER_TYPE_CATAPULT:
285     case TOWER_TYPE_BALLISTA:
286     case TOWER_TYPE_ARCHER:
287       TowerGunUpdate(tower);
288       break;
289     }
290   }
291 }
292 
293 void TowerDrawHealthBars(Camera3D camera)
294 {
295   for (int i = 0; i < towerCount; i++)
296   {
297     Tower *tower = &towers[i];
298     if (tower->towerType == TOWER_TYPE_NONE || tower->damage <= 0.0f)
299     {
300       continue;
301     }
302     
303     Vector3 position = (Vector3){tower->x, 0.5f, tower->y};
304     float maxHealth = TowerGetMaxHealth(tower);
305     float health = maxHealth - tower->damage;
306     float healthRatio = health / maxHealth;
307     
308     DrawHealthBar(camera, position, healthRatio, GREEN, 35.0f);
309   }
310 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 #include <stdlib.h>
  4 #include <math.h>
  5 
  6 EnemyClassConfig enemyClassConfigs[] = {
  7     [ENEMY_TYPE_MINION] = {
  8       .health = 10.0f, 
  9       .speed = 0.6f, 
 10       .radius = 0.25f, 
 11       .maxAcceleration = 1.0f,
 12       .explosionDamage = 1.0f,
 13       .requiredContactTime = 0.5f,
 14       .explosionRange = 1.0f,
 15       .explosionPushbackPower = 0.25f,
 16       .goldValue = 1,
 17     },
 18 };
 19 
 20 Enemy enemies[ENEMY_MAX_COUNT];
 21 int enemyCount = 0;
 22 
 23 SpriteUnit enemySprites[] = {
 24     [ENEMY_TYPE_MINION] = {
 25       .srcRect = {0, 16, 16, 16},
 26       .offset = {8.0f, 0.0f},
 27       .frameCount = 6,
 28       .frameDuration = 0.1f,
 29     },
 30 };
 31 
 32 void EnemyInit()
 33 {
 34   for (int i = 0; i < ENEMY_MAX_COUNT; i++)
 35   {
 36     enemies[i] = (Enemy){0};
 37   }
 38   enemyCount = 0;
 39 }
 40 
 41 float EnemyGetCurrentMaxSpeed(Enemy *enemy)
 42 {
 43   return enemyClassConfigs[enemy->enemyType].speed;
 44 }
 45 
 46 float EnemyGetMaxHealth(Enemy *enemy)
 47 {
 48   return enemyClassConfigs[enemy->enemyType].health;
 49 }
 50 
 51 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY)
 52 {
 53   int16_t castleX = 0;
 54   int16_t castleY = 0;
 55   int16_t dx = castleX - currentX;
 56   int16_t dy = castleY - currentY;
 57   if (dx == 0 && dy == 0)
 58   {
 59     *nextX = currentX;
 60     *nextY = currentY;
 61     return 1;
 62   }
 63   Vector2 gradient = PathFindingGetGradient((Vector3){currentX, 0, currentY});
 64 
 65   if (gradient.x == 0 && gradient.y == 0)
 66   {
 67     *nextX = currentX;
 68     *nextY = currentY;
 69     return 1;
 70   }
 71 
 72   if (fabsf(gradient.x) > fabsf(gradient.y))
 73   {
 74     *nextX = currentX + (int16_t)(gradient.x > 0.0f ? 1 : -1);
 75     *nextY = currentY;
 76     return 0;
 77   }
 78   *nextX = currentX;
 79   *nextY = currentY + (int16_t)(gradient.y > 0.0f ? 1 : -1);
 80   return 0;
 81 }
 82 
 83 
 84 // this function predicts the movement of the unit for the next deltaT seconds
 85 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount)
 86 {
 87   const float pointReachedDistance = 0.25f;
 88   const float pointReachedDistance2 = pointReachedDistance * pointReachedDistance;
 89   const float maxSimStepTime = 0.015625f;
 90   
 91   float maxAcceleration = enemyClassConfigs[enemy->enemyType].maxAcceleration;
 92   float maxSpeed = EnemyGetCurrentMaxSpeed(enemy);
 93   int16_t nextX = enemy->nextX;
 94   int16_t nextY = enemy->nextY;
 95   Vector2 position = enemy->simPosition;
 96   int passedCount = 0;
 97   for (float t = 0.0f; t < deltaT; t += maxSimStepTime)
 98   {
 99     float stepTime = fminf(deltaT - t, maxSimStepTime);
100     Vector2 target = (Vector2){nextX, nextY};
101     float speed = Vector2Length(*velocity);
102     // draw the target position for debugging
103     //DrawCubeWires((Vector3){target.x, 0.2f, target.y}, 0.1f, 0.4f, 0.1f, RED);
104     Vector2 lookForwardPos = Vector2Add(position, Vector2Scale(*velocity, speed));
105     if (Vector2DistanceSqr(target, lookForwardPos) <= pointReachedDistance2)
106     {
107       // we reached the target position, let's move to the next waypoint
108       EnemyGetNextPosition(nextX, nextY, &nextX, &nextY);
109       target = (Vector2){nextX, nextY};
110       // track how many waypoints we passed
111       passedCount++;
112     }
113     
114     // acceleration towards the target
115     Vector2 unitDirection = Vector2Normalize(Vector2Subtract(target, lookForwardPos));
116     Vector2 acceleration = Vector2Scale(unitDirection, maxAcceleration * stepTime);
117     *velocity = Vector2Add(*velocity, acceleration);
118 
119     // limit the speed to the maximum speed
120     if (speed > maxSpeed)
121     {
122       *velocity = Vector2Scale(*velocity, maxSpeed / speed);
123     }
124 
125     // move the enemy
126     position = Vector2Add(position, Vector2Scale(*velocity, stepTime));
127   }
128 
129   if (waypointPassedCount)
130   {
131     (*waypointPassedCount) = passedCount;
132   }
133 
134   return position;
135 }
136 
137 void EnemyDraw()
138 {
139   for (int i = 0; i < enemyCount; i++)
140   {
141     Enemy enemy = enemies[i];
142     if (enemy.enemyType == ENEMY_TYPE_NONE)
143     {
144       continue;
145     }
146 
147     Vector2 position = EnemyGetPosition(&enemy, gameTime.time - enemy.startMovingTime, &enemy.simVelocity, 0);
148     
149     // don't draw any trails for now; might replace this with footprints later
150     // if (enemy.movePathCount > 0)
151     // {
152     //   Vector3 p = {enemy.movePath[0].x, 0.2f, enemy.movePath[0].y};
153     //   DrawLine3D(p, (Vector3){position.x, 0.2f, position.y}, GREEN);
154     // }
155     // for (int j = 1; j < enemy.movePathCount; j++)
156     // {
157     //   Vector3 p = {enemy.movePath[j - 1].x, 0.2f, enemy.movePath[j - 1].y};
158     //   Vector3 q = {enemy.movePath[j].x, 0.2f, enemy.movePath[j].y};
159     //   DrawLine3D(p, q, GREEN);
160     // }
161 
162     switch (enemy.enemyType)
163     {
164     case ENEMY_TYPE_MINION:
165       DrawSpriteUnit(enemySprites[ENEMY_TYPE_MINION], (Vector3){position.x, 0.0f, position.y}, 
166         enemy.walkedDistance, 0, 0);
167       break;
168     }
169   }
170 }
171 
172 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource)
173 {
174   // damage the tower
175   float explosionDamge = enemyClassConfigs[enemy->enemyType].explosionDamage;
176   float explosionRange = enemyClassConfigs[enemy->enemyType].explosionRange;
177   float explosionPushbackPower = enemyClassConfigs[enemy->enemyType].explosionPushbackPower;
178   float explosionRange2 = explosionRange * explosionRange;
179   tower->damage += enemyClassConfigs[enemy->enemyType].explosionDamage;
180   // explode the enemy
181   if (tower->damage >= TowerGetMaxHealth(tower))
182   {
183     tower->towerType = TOWER_TYPE_NONE;
184   }
185 
186   ParticleAdd(PARTICLE_TYPE_EXPLOSION, 
187     explosionSource, 
188     (Vector3){0, 0.1f, 0}, (Vector3){1.0f, 1.0f, 1.0f}, 1.0f);
189 
190   enemy->enemyType = ENEMY_TYPE_NONE;
191 
192   // push back enemies & dealing damage
193   for (int i = 0; i < enemyCount; i++)
194   {
195     Enemy *other = &enemies[i];
196     if (other->enemyType == ENEMY_TYPE_NONE)
197     {
198       continue;
199     }
200     float distanceSqr = Vector2DistanceSqr(enemy->simPosition, other->simPosition);
201     if (distanceSqr > 0 && distanceSqr < explosionRange2)
202     {
203       Vector2 direction = Vector2Normalize(Vector2Subtract(other->simPosition, enemy->simPosition));
204       other->simPosition = Vector2Add(other->simPosition, Vector2Scale(direction, explosionPushbackPower));
205       EnemyAddDamage(other, explosionDamge);
206     }
207   }
208 }
209 
210 void EnemyUpdate()
211 {
212   const float castleX = 0;
213   const float castleY = 0;
214   const float maxPathDistance2 = 0.25f * 0.25f;
215   
216   for (int i = 0; i < enemyCount; i++)
217   {
218     Enemy *enemy = &enemies[i];
219     if (enemy->enemyType == ENEMY_TYPE_NONE)
220     {
221       continue;
222     }
223 
224     int waypointPassedCount = 0;
225     Vector2 prevPosition = enemy->simPosition;
226     enemy->simPosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &enemy->simVelocity, &waypointPassedCount);
227     enemy->startMovingTime = gameTime.time;
228     enemy->walkedDistance += Vector2Distance(prevPosition, enemy->simPosition);
229     // track path of unit
230     if (enemy->movePathCount == 0 || Vector2DistanceSqr(enemy->simPosition, enemy->movePath[0]) > maxPathDistance2)
231     {
232       for (int j = ENEMY_MAX_PATH_COUNT - 1; j > 0; j--)
233       {
234         enemy->movePath[j] = enemy->movePath[j - 1];
235       }
236       enemy->movePath[0] = enemy->simPosition;
237       if (++enemy->movePathCount > ENEMY_MAX_PATH_COUNT)
238       {
239         enemy->movePathCount = ENEMY_MAX_PATH_COUNT;
240       }
241     }
242 
243     if (waypointPassedCount > 0)
244     {
245       enemy->currentX = enemy->nextX;
246       enemy->currentY = enemy->nextY;
247       if (EnemyGetNextPosition(enemy->currentX, enemy->currentY, &enemy->nextX, &enemy->nextY) &&
248         Vector2DistanceSqr(enemy->simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
249       {
250         // enemy reached the castle; remove it
251         enemy->enemyType = ENEMY_TYPE_NONE;
252         continue;
253       }
254     }
255   }
256 
257   // handle collisions between enemies
258   for (int i = 0; i < enemyCount - 1; i++)
259   {
260     Enemy *enemyA = &enemies[i];
261     if (enemyA->enemyType == ENEMY_TYPE_NONE)
262     {
263       continue;
264     }
265     for (int j = i + 1; j < enemyCount; j++)
266     {
267       Enemy *enemyB = &enemies[j];
268       if (enemyB->enemyType == ENEMY_TYPE_NONE)
269       {
270         continue;
271       }
272       float distanceSqr = Vector2DistanceSqr(enemyA->simPosition, enemyB->simPosition);
273       float radiusA = enemyClassConfigs[enemyA->enemyType].radius;
274       float radiusB = enemyClassConfigs[enemyB->enemyType].radius;
275       float radiusSum = radiusA + radiusB;
276       if (distanceSqr < radiusSum * radiusSum && distanceSqr > 0.001f)
277       {
278         // collision
279         float distance = sqrtf(distanceSqr);
280         float overlap = radiusSum - distance;
281         // move the enemies apart, but softly; if we have a clog of enemies,
282         // moving them perfectly apart can cause them to jitter
283         float positionCorrection = overlap / 5.0f;
284         Vector2 direction = (Vector2){
285             (enemyB->simPosition.x - enemyA->simPosition.x) / distance * positionCorrection,
286             (enemyB->simPosition.y - enemyA->simPosition.y) / distance * positionCorrection};
287         enemyA->simPosition = Vector2Subtract(enemyA->simPosition, direction);
288         enemyB->simPosition = Vector2Add(enemyB->simPosition, direction);
289       }
290     }
291   }
292 
293   // handle collisions between enemies and towers
294   for (int i = 0; i < enemyCount; i++)
295   {
296     Enemy *enemy = &enemies[i];
297     if (enemy->enemyType == ENEMY_TYPE_NONE)
298     {
299       continue;
300     }
301     enemy->contactTime -= gameTime.deltaTime;
302     if (enemy->contactTime < 0.0f)
303     {
304       enemy->contactTime = 0.0f;
305     }
306 
307     float enemyRadius = enemyClassConfigs[enemy->enemyType].radius;
308     // linear search over towers; could be optimized by using path finding tower map,
309     // but for now, we keep it simple
310     for (int j = 0; j < towerCount; j++)
311     {
312       Tower *tower = &towers[j];
313       if (tower->towerType == TOWER_TYPE_NONE)
314       {
315         continue;
316       }
317       float distanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){tower->x, tower->y});
318       float combinedRadius = enemyRadius + 0.708; // sqrt(0.5^2 + 0.5^2), corner-center distance of square with side length 1
319       if (distanceSqr > combinedRadius * combinedRadius)
320       {
321         continue;
322       }
323       // potential collision; square / circle intersection
324       float dx = tower->x - enemy->simPosition.x;
325       float dy = tower->y - enemy->simPosition.y;
326       float absDx = fabsf(dx);
327       float absDy = fabsf(dy);
328       Vector3 contactPoint = {0};
329       if (absDx <= 0.5f && absDx <= absDy) {
330         // vertical collision; push the enemy out horizontally
331         float overlap = enemyRadius + 0.5f - absDy;
332         if (overlap < 0.0f)
333         {
334           continue;
335         }
336         float direction = dy > 0.0f ? -1.0f : 1.0f;
337         enemy->simPosition.y += direction * overlap;
338         contactPoint = (Vector3){enemy->simPosition.x, 0.2f, tower->y + direction * 0.5f};
339       }
340       else if (absDy <= 0.5f && absDy <= absDx)
341       {
342         // horizontal collision; push the enemy out vertically
343         float overlap = enemyRadius + 0.5f - absDx;
344         if (overlap < 0.0f)
345         {
346           continue;
347         }
348         float direction = dx > 0.0f ? -1.0f : 1.0f;
349         enemy->simPosition.x += direction * overlap;
350         contactPoint = (Vector3){tower->x + direction * 0.5f, 0.2f, enemy->simPosition.y};
351       }
352       else
353       {
354         // possible collision with a corner
355         float cornerDX = dx > 0.0f ? -0.5f : 0.5f;
356         float cornerDY = dy > 0.0f ? -0.5f : 0.5f;
357         float cornerX = tower->x + cornerDX;
358         float cornerY = tower->y + cornerDY;
359         float cornerDistanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){cornerX, cornerY});
360         if (cornerDistanceSqr > enemyRadius * enemyRadius)
361         {
362           continue;
363         }
364         // push the enemy out along the diagonal
365         float cornerDistance = sqrtf(cornerDistanceSqr);
366         float overlap = enemyRadius - cornerDistance;
367         float directionX = cornerDistance > 0.0f ? (cornerX - enemy->simPosition.x) / cornerDistance : -cornerDX;
368         float directionY = cornerDistance > 0.0f ? (cornerY - enemy->simPosition.y) / cornerDistance : -cornerDY;
369         enemy->simPosition.x -= directionX * overlap;
370         enemy->simPosition.y -= directionY * overlap;
371         contactPoint = (Vector3){cornerX, 0.2f, cornerY};
372       }
373 
374       if (enemyClassConfigs[enemy->enemyType].explosionDamage > 0.0f)
375       {
376         enemy->contactTime += gameTime.deltaTime * 2.0f; // * 2 to undo the subtraction above
377         if (enemy->contactTime >= enemyClassConfigs[enemy->enemyType].requiredContactTime)
378         {
379           EnemyTriggerExplode(enemy, tower, contactPoint);
380         }
381       }
382     }
383   }
384 }
385 
386 EnemyId EnemyGetId(Enemy *enemy)
387 {
388   return (EnemyId){enemy - enemies, enemy->generation};
389 }
390 
391 Enemy *EnemyTryResolve(EnemyId enemyId)
392 {
393   if (enemyId.index >= ENEMY_MAX_COUNT)
394   {
395     return 0;
396   }
397   Enemy *enemy = &enemies[enemyId.index];
398   if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE)
399   {
400     return 0;
401   }
402   return enemy;
403 }
404 
405 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY)
406 {
407   Enemy *spawn = 0;
408   for (int i = 0; i < enemyCount; i++)
409   {
410     Enemy *enemy = &enemies[i];
411     if (enemy->enemyType == ENEMY_TYPE_NONE)
412     {
413       spawn = enemy;
414       break;
415     }
416   }
417 
418   if (enemyCount < ENEMY_MAX_COUNT && !spawn)
419   {
420     spawn = &enemies[enemyCount++];
421   }
422 
423   if (spawn)
424   {
425     spawn->currentX = currentX;
426     spawn->currentY = currentY;
427     spawn->nextX = currentX;
428     spawn->nextY = currentY;
429     spawn->simPosition = (Vector2){currentX, currentY};
430     spawn->simVelocity = (Vector2){0, 0};
431     spawn->enemyType = enemyType;
432     spawn->startMovingTime = gameTime.time;
433     spawn->damage = 0.0f;
434     spawn->futureDamage = 0.0f;
435     spawn->generation++;
436     spawn->movePathCount = 0;
437     spawn->walkedDistance = 0.0f;
438   }
439 
440   return spawn;
441 }
442 
443 int EnemyAddDamageRange(Vector2 position, float range, float damage)
444 {
445   int count = 0;
446   float range2 = range * range;
447   for (int i = 0; i < enemyCount; i++)
448   {
449     Enemy *enemy = &enemies[i];
450     if (enemy->enemyType == ENEMY_TYPE_NONE)
451     {
452       continue;
453     }
454     float distance2 = Vector2DistanceSqr(position, enemy->simPosition);
455     if (distance2 <= range2)
456     {
457       EnemyAddDamage(enemy, damage);
458       count++;
459     }
460   }
461   return count;
462 }
463 
464 int EnemyAddDamage(Enemy *enemy, float damage)
465 {
466   enemy->damage += damage;
467   if (enemy->damage >= EnemyGetMaxHealth(enemy))
468   {
469     currentLevel->playerGold += enemyClassConfigs[enemy->enemyType].goldValue;
470     enemy->enemyType = ENEMY_TYPE_NONE;
471     return 1;
472   }
473 
474   return 0;
475 }
476 
477 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
478 {
479   int16_t castleX = 0;
480   int16_t castleY = 0;
481   Enemy* closest = 0;
482   int16_t closestDistance = 0;
483   float range2 = range * range;
484   for (int i = 0; i < enemyCount; i++)
485   {
486     Enemy* enemy = &enemies[i];
487     if (enemy->enemyType == ENEMY_TYPE_NONE)
488     {
489       continue;
490     }
491     float maxHealth = EnemyGetMaxHealth(enemy);
492     if (enemy->futureDamage >= maxHealth)
493     {
494       // ignore enemies that will die soon
495       continue;
496     }
497     int16_t dx = castleX - enemy->currentX;
498     int16_t dy = castleY - enemy->currentY;
499     int16_t distance = abs(dx) + abs(dy);
500     if (!closest || distance < closestDistance)
501     {
502       float tdx = towerX - enemy->currentX;
503       float tdy = towerY - enemy->currentY;
504       float tdistance2 = tdx * tdx + tdy * tdy;
505       if (tdistance2 <= range2)
506       {
507         closest = enemy;
508         closestDistance = distance;
509       }
510     }
511   }
512   return closest;
513 }
514 
515 int EnemyCount()
516 {
517   int count = 0;
518   for (int i = 0; i < enemyCount; i++)
519   {
520     if (enemies[i].enemyType != ENEMY_TYPE_NONE)
521     {
522       count++;
523     }
524   }
525   return count;
526 }
527 
528 void EnemyDrawHealthbars(Camera3D camera)
529 {
530   for (int i = 0; i < enemyCount; i++)
531   {
532     Enemy *enemy = &enemies[i];
533     if (enemy->enemyType == ENEMY_TYPE_NONE || enemy->damage == 0.0f)
534     {
535       continue;
536     }
537     Vector3 position = (Vector3){enemy->simPosition.x, 0.5f, enemy->simPosition.y};
538     float maxHealth = EnemyGetMaxHealth(enemy);
539     float health = maxHealth - enemy->damage;
540     float healthRatio = health / maxHealth;
541     
542     DrawHealthBar(camera, position, healthRatio, GREEN, 15.0f);
543   }
544 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 
  4 static Projectile projectiles[PROJECTILE_MAX_COUNT];
  5 static int projectileCount = 0;
  6 
  7 typedef struct ProjectileConfig
  8 {
  9   float arcFactor;
 10   Color color;
 11   Color trailColor;
 12 } ProjectileConfig;
 13 
 14 ProjectileConfig projectileConfigs[] = {
 15     [PROJECTILE_TYPE_ARROW] = {
 16         .arcFactor = 0.15f,
 17         .color = RED,
 18         .trailColor = BROWN,
 19     },
 20     [PROJECTILE_TYPE_CATAPULT] = {
 21         .arcFactor = 0.5f,
 22         .color = RED,
 23         .trailColor = GRAY,
 24     },
 25     [PROJECTILE_TYPE_BALLISTA] = {
 26         .arcFactor = 0.025f,
 27         .color = RED,
 28         .trailColor = BROWN,
 29     },
 30 };
 31 
 32 void ProjectileInit()
 33 {
 34   for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
 35   {
 36     projectiles[i] = (Projectile){0};
 37   }
 38 }
 39 
 40 void ProjectileDraw()
 41 {
 42   for (int i = 0; i < projectileCount; i++)
 43   {
 44     Projectile projectile = projectiles[i];
 45     if (projectile.projectileType == PROJECTILE_TYPE_NONE)
 46     {
 47       continue;
 48     }
 49     float transition = (gameTime.time - projectile.shootTime) / (projectile.arrivalTime - projectile.shootTime);
 50     if (transition >= 1.0f)
 51     {
 52       continue;
 53     }
 54 
 55     ProjectileConfig config = projectileConfigs[projectile.projectileType];
 56     for (float transitionOffset = 0.0f; transitionOffset < 1.0f; transitionOffset += 0.1f)
 57     {
 58       float t = transition + transitionOffset * 0.3f;
 59       if (t > 1.0f)
 60       {
 61         break;
 62       }
 63       Vector3 position = Vector3Lerp(projectile.position, projectile.target, t);
 64       Color color = config.color;
 65       color = ColorLerp(config.trailColor, config.color, transitionOffset * transitionOffset);
 66       // fake a ballista flight path using parabola equation
 67       float parabolaT = t - 0.5f;
 68       parabolaT = 1.0f - 4.0f * parabolaT * parabolaT;
 69       position.y += config.arcFactor * parabolaT * projectile.distance;
 70       
 71       float size = 0.06f * (transitionOffset + 0.25f);
 72       DrawCube(position, size, size, size, color);
 73     }
 74   }
 75 }
 76 
 77 void ProjectileUpdate()
 78 {
 79   for (int i = 0; i < projectileCount; i++)
 80   {
 81     Projectile *projectile = &projectiles[i];
 82     if (projectile->projectileType == PROJECTILE_TYPE_NONE)
 83     {
 84       continue;
 85     }
 86     float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime);
 87     if (transition >= 1.0f)
 88     {
 89       projectile->projectileType = PROJECTILE_TYPE_NONE;
 90       Enemy *enemy = EnemyTryResolve(projectile->targetEnemy);
 91       if (enemy && projectile->hitEffectConfig.pushbackPowerDistance > 0.0f)
 92       {
 93           Vector2 direction = Vector2Normalize(Vector2Subtract((Vector2){projectile->target.x, projectile->target.z}, enemy->simPosition));
 94           enemy->simPosition = Vector2Add(enemy->simPosition, Vector2Scale(direction, projectile->hitEffectConfig.pushbackPowerDistance));
 95       }
 96       
 97       if (projectile->hitEffectConfig.areaDamageRadius > 0.0f)
 98       {
 99         EnemyAddDamageRange((Vector2){projectile->target.x, projectile->target.z}, projectile->hitEffectConfig.areaDamageRadius, projectile->hitEffectConfig.damage);
100         // pancaked sphere explosion
101         float r = projectile->hitEffectConfig.areaDamageRadius;
102         ParticleAdd(PARTICLE_TYPE_EXPLOSION, projectile->target, (Vector3){0}, (Vector3){r, r * 0.2f, r}, 0.33f);
103       }
104       else if (projectile->hitEffectConfig.damage > 0.0f && enemy)
105       {
106         EnemyAddDamage(enemy, projectile->hitEffectConfig.damage);
107       }
108       continue;
109     }
110   }
111 }
112 
113 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig)
114 {
115   for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
116   {
117     Projectile *projectile = &projectiles[i];
118     if (projectile->projectileType == PROJECTILE_TYPE_NONE)
119     {
120       projectile->projectileType = projectileType;
121       projectile->shootTime = gameTime.time;
122       float distance = Vector3Distance(position, target);
123       projectile->arrivalTime = gameTime.time + distance / speed;
124       projectile->position = position;
125       projectile->target = target;
126       projectile->directionNormal = Vector3Scale(Vector3Subtract(target, position), 1.0f / distance);
127       projectile->distance = distance;
128       projectile->targetEnemy = EnemyGetId(enemy);
129       projectileCount = projectileCount <= i ? i + 1 : projectileCount;
130       projectile->hitEffectConfig = hitEffectConfig;
131       return projectile;
132     }
133   }
134   return 0;
135 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 #include <rlgl.h>
  4 
  5 static Particle particles[PARTICLE_MAX_COUNT];
  6 static int particleCount = 0;
  7 
  8 void ParticleInit()
  9 {
 10   for (int i = 0; i < PARTICLE_MAX_COUNT; i++)
 11   {
 12     particles[i] = (Particle){0};
 13   }
 14   particleCount = 0;
 15 }
 16 
 17 static void DrawExplosionParticle(Particle *particle, float transition)
 18 {
 19   Vector3 scale = particle->scale;
 20   float size = 1.0f * (1.0f - transition);
 21   Color startColor = WHITE;
 22   Color endColor = RED;
 23   Color color = ColorLerp(startColor, endColor, transition);
 24 
 25   rlPushMatrix();
 26   rlTranslatef(particle->position.x, particle->position.y, particle->position.z);
 27   rlScalef(scale.x, scale.y, scale.z);
 28   DrawSphere(Vector3Zero(), size, color);
 29   rlPopMatrix();
 30 }
 31 
 32 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime)
 33 {
 34   if (particleCount >= PARTICLE_MAX_COUNT)
 35   {
 36     return;
 37   }
 38 
 39   int index = -1;
 40   for (int i = 0; i < particleCount; i++)
 41   {
 42     if (particles[i].particleType == PARTICLE_TYPE_NONE)
 43     {
 44       index = i;
 45       break;
 46     }
 47   }
 48 
 49   if (index == -1)
 50   {
 51     index = particleCount++;
 52   }
 53 
 54   Particle *particle = &particles[index];
 55   particle->particleType = particleType;
 56   particle->spawnTime = gameTime.time;
 57   particle->lifetime = lifetime;
 58   particle->position = position;
 59   particle->velocity = velocity;
 60   particle->scale = scale;
 61 }
 62 
 63 void ParticleUpdate()
 64 {
 65   for (int i = 0; i < particleCount; i++)
 66   {
 67     Particle *particle = &particles[i];
 68     if (particle->particleType == PARTICLE_TYPE_NONE)
 69     {
 70       continue;
 71     }
 72 
 73     float age = gameTime.time - particle->spawnTime;
 74 
 75     if (particle->lifetime > age)
 76     {
 77       particle->position = Vector3Add(particle->position, Vector3Scale(particle->velocity, gameTime.deltaTime));
 78     }
 79     else {
 80       particle->particleType = PARTICLE_TYPE_NONE;
 81     }
 82   }
 83 }
 84 
 85 void ParticleDraw()
 86 {
 87   for (int i = 0; i < particleCount; i++)
 88   {
 89     Particle particle = particles[i];
 90     if (particle.particleType == PARTICLE_TYPE_NONE)
 91     {
 92       continue;
 93     }
 94 
 95     float age = gameTime.time - particle.spawnTime;
 96     float transition = age / particle.lifetime;
 97     switch (particle.particleType)
 98     {
 99     case PARTICLE_TYPE_EXPLOSION:
100       DrawExplosionParticle(&particle, transition);
101       break;
102     default:
103       DrawCube(particle.position, 0.3f, 0.5f, 0.3f, RED);
104       break;
105     }
106   }
107 }
  1 #include "raylib.h"
  2 #include "preferred_size.h"
  3 
  4 // Since the canvas size is not known at compile time, we need to query it at runtime;
  5 // the following platform specific code obtains the canvas size and we will use this
  6 // size as the preferred size for the window at init time. We're ignoring here the
  7 // possibility of the canvas size changing during runtime - this would require to
  8 // poll the canvas size in the game loop or establishing a callback to be notified
  9 
 10 #ifdef PLATFORM_WEB
 11 #include <emscripten.h>
 12 EMSCRIPTEN_RESULT emscripten_get_element_css_size(const char *target, double *width, double *height);
 13 
 14 void GetPreferredSize(int *screenWidth, int *screenHeight)
 15 {
 16   double canvasWidth, canvasHeight;
 17   emscripten_get_element_css_size("#" CANVAS_NAME, &canvasWidth, &canvasHeight);
 18   *screenWidth = (int)canvasWidth;
 19   *screenHeight = (int)canvasHeight;
 20   TraceLog(LOG_INFO, "preferred size for %s: %d %d", CANVAS_NAME, *screenWidth, *screenHeight);
 21 }
 22 
 23 int IsPaused()
 24 {
 25   const char *js = "(function(){\n"
 26   "  var canvas = document.getElementById(\"" CANVAS_NAME "\");\n"
 27   "  var rect = canvas.getBoundingClientRect();\n"
 28   "  var isVisible = (\n"
 29   "    rect.top >= 0 &&\n"
 30   "    rect.left >= 0 &&\n"
 31   "    rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&\n"
 32   "    rect.right <= (window.innerWidth || document.documentElement.clientWidth)\n"
 33   "  );\n"
 34   "  return isVisible ? 0 : 1;\n"
 35   "})()";
 36   return emscripten_run_script_int(js);
 37 }
 38 
 39 #else
 40 void GetPreferredSize(int *screenWidth, int *screenHeight)
 41 {
 42   *screenWidth = 600;
 43   *screenHeight = 240;
 44 }
 45 int IsPaused()
 46 {
 47   return 0;
 48 }
 49 #endif
  1 #ifndef PREFERRED_SIZE_H
  2 #define PREFERRED_SIZE_H
  3 
  4 void GetPreferredSize(int *screenWidth, int *screenHeight);
  5 int IsPaused();
  6 
  7 #endif
Two enemy entry points with path previews merging
Two enemy entry points with path previews merging with visible Z-fighting

It kinda works, but there are some render artifacts called Z-fights. This happens when two objects are rendered at the same depth and floating point inaccuracies cause flickering pixels. There are two ways to handle this: Avoiding the occurrence of Z-fights or making them invisible by rendering the z-fighting triangles with the same color.

My favorite solution is usually the latter: Making sure that the Z-fighting triangles are rendered with the same color is quite easy in a situation without lighting effects. This is not possible when using textured geometry, but for flat colored geometry, it's very effective. This is also how I came to choose this art style for making prototypes and simple games: It's very easy to work with and some nasty technical problems become non-issues.

Let's look at the arrow mesh how it is right now:

Arrow model
The surface of the top of the arrow is completely flat.

Now let's modify this mesh by just moving the vertices of the outline a bit lower, also known as beveling:

Arrow model with beveled outline
The outline of the arrow are subtly beveled now.

It's barely visible, but let's see how it looks in the game:

  • 💾
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 #include <rlgl.h>
  4 #include <stdlib.h>
  5 #include <math.h>
  6 
  7 //# Variables
  8 GUIState guiState = {0};
  9 GameTime gameTime = {0};
 10 
 11 Model floorTileAModel = {0};
 12 Model floorTileBModel = {0};
 13 Model treeModel[2] = {0};
 14 Model firTreeModel[2] = {0};
 15 Model rockModels[5] = {0};
 16 Model grassPatchModel[1] = {0};
 17 
 18 Model pathArrowModel = {0};
 19 
 20 Texture2D palette, spriteSheet;
 21 
 22 Level levels[] = {
 23   [0] = {
 24     .state = LEVEL_STATE_BUILDING,
 25     .initialGold = 20,
 26     .waves[0] = {
 27       .enemyType = ENEMY_TYPE_MINION,
 28       .wave = 0,
 29       .count = 5,
 30       .interval = 2.5f,
 31       .delay = 1.0f,
 32       .spawnPosition = {2, 6},
 33     },
 34     .waves[1] = {
 35       .enemyType = ENEMY_TYPE_MINION,
 36       .wave = 0,
 37       .count = 5,
 38       .interval = 2.5f,
 39       .delay = 1.0f,
 40       .spawnPosition = {-2, 6},
 41     },
 42     .waves[2] = {
 43       .enemyType = ENEMY_TYPE_MINION,
 44       .wave = 1,
 45       .count = 20,
 46       .interval = 1.5f,
 47       .delay = 1.0f,
 48       .spawnPosition = {0, 6},
 49     },
 50     .waves[3] = {
 51       .enemyType = ENEMY_TYPE_MINION,
 52       .wave = 2,
 53       .count = 30,
 54       .interval = 1.2f,
 55       .delay = 1.0f,
 56       .spawnPosition = {0, 6},
 57     }
 58   },
 59 };
 60 
 61 Level *currentLevel = levels;
 62 
 63 //# Game
 64 
 65 static Model LoadGLBModel(char *filename)
 66 {
 67   Model model = LoadModel(TextFormat("data/%s.glb",filename));
 68   for (int i = 0; i < model.materialCount; i++)
 69   {
 70     model.materials[i].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
 71   }
 72   return model;
 73 }
 74 
 75 void LoadAssets()
 76 {
 77   // load a sprite sheet that contains all units
 78   spriteSheet = LoadTexture("data/spritesheet.png");
 79   SetTextureFilter(spriteSheet, TEXTURE_FILTER_BILINEAR);
 80 
 81   // we'll use a palette texture to colorize the all buildings and environment art
 82   palette = LoadTexture("data/palette.png");
 83   // The texture uses gradients on very small space, so we'll enable bilinear filtering
 84   SetTextureFilter(palette, TEXTURE_FILTER_BILINEAR);
 85 
 86   floorTileAModel = LoadGLBModel("floor-tile-a");
 87   floorTileBModel = LoadGLBModel("floor-tile-b");
 88   treeModel[0] = LoadGLBModel("leaftree-large-1-a");
 89   treeModel[1] = LoadGLBModel("leaftree-large-1-b");
 90   firTreeModel[0] = LoadGLBModel("firtree-1-a");
 91   firTreeModel[1] = LoadGLBModel("firtree-1-b");
 92   rockModels[0] = LoadGLBModel("rock-1");
 93   rockModels[1] = LoadGLBModel("rock-2");
 94   rockModels[2] = LoadGLBModel("rock-3");
 95   rockModels[3] = LoadGLBModel("rock-4");
 96   rockModels[4] = LoadGLBModel("rock-5");
 97   grassPatchModel[0] = LoadGLBModel("grass-patch-1");
 98 
 99   pathArrowModel = LoadGLBModel("direction-arrow-x");
100 }
101 
102 void InitLevel(Level *level)
103 {
104   level->seed = (int)(GetTime() * 100.0f);
105 
106   TowerInit();
107   EnemyInit();
108   ProjectileInit();
109   ParticleInit();
110   TowerTryAdd(TOWER_TYPE_BASE, 0, 0);
111 
112   level->placementMode = 0;
113   level->state = LEVEL_STATE_BUILDING;
114   level->nextState = LEVEL_STATE_NONE;
115   level->playerGold = level->initialGold;
116   level->currentWave = 0;
117 
118   Camera *camera = &level->camera;
119   camera->position = (Vector3){4.0f, 8.0f, 8.0f};
120   camera->target = (Vector3){0.0f, 0.0f, 0.0f};
121   camera->up = (Vector3){0.0f, 1.0f, 0.0f};
122   camera->fovy = 10.0f;
123   camera->projection = CAMERA_ORTHOGRAPHIC;
124 }
125 
126 void DrawLevelHud(Level *level)
127 {
128   const char *text = TextFormat("Gold: %d", level->playerGold);
129   Font font = GetFontDefault();
130   DrawTextEx(font, text, (Vector2){GetScreenWidth() - 120, 10}, font.baseSize * 2.0f, 2.0f, BLACK);
131   DrawTextEx(font, text, (Vector2){GetScreenWidth() - 122, 8}, font.baseSize * 2.0f, 2.0f, YELLOW);
132 }
133 
134 void DrawLevelReportLostWave(Level *level)
135 {
136   BeginMode3D(level->camera);
137   DrawLevelGround(level);
138   TowerDraw();
139   EnemyDraw();
140   ProjectileDraw();
141   ParticleDraw();
142   guiState.isBlocked = 0;
143   EndMode3D();
144 
145   TowerDrawHealthBars(level->camera);
146 
147   const char *text = "Wave lost";
148   int textWidth = MeasureText(text, 20);
149   DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
150 
151   if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
152   {
153     level->nextState = LEVEL_STATE_RESET;
154   }
155 }
156 
157 int HasLevelNextWave(Level *level)
158 {
159   for (int i = 0; i < 10; i++)
160   {
161     EnemyWave *wave = &level->waves[i];
162     if (wave->wave == level->currentWave)
163     {
164       return 1;
165     }
166   }
167   return 0;
168 }
169 
170 void DrawLevelReportWonWave(Level *level)
171 {
172   BeginMode3D(level->camera);
173   DrawLevelGround(level);
174   TowerDraw();
175   EnemyDraw();
176   ProjectileDraw();
177   ParticleDraw();
178   guiState.isBlocked = 0;
179   EndMode3D();
180 
181   TowerDrawHealthBars(level->camera);
182 
183   const char *text = "Wave won";
184   int textWidth = MeasureText(text, 20);
185   DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
186 
187 
188   if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
189   {
190     level->nextState = LEVEL_STATE_RESET;
191   }
192 
193   if (HasLevelNextWave(level))
194   {
195     if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
196     {
197       level->nextState = LEVEL_STATE_BUILDING;
198     }
199   }
200   else {
201     if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
202     {
203       level->nextState = LEVEL_STATE_WON_LEVEL;
204     }
205   }
206 }
207 
208 void DrawBuildingBuildButton(Level *level, int x, int y, int width, int height, uint8_t towerType, const char *name)
209 {
210   static ButtonState buttonStates[8] = {0};
211   int cost = GetTowerCosts(towerType);
212   const char *text = TextFormat("%s: %d", name, cost);
213   buttonStates[towerType].isSelected = level->placementMode == towerType;
214   buttonStates[towerType].isDisabled = level->playerGold < cost;
215   if (Button(text, x, y, width, height, &buttonStates[towerType]))
216   {
217     level->placementMode = buttonStates[towerType].isSelected ? 0 : towerType;
218   }
219 }
220 
221 float GetRandomFloat(float min, float max)
222 {
223   int random = GetRandomValue(0, 0xfffffff);
224   return ((float)random / (float)0xfffffff) * (max - min) + min;
225 }
226 
227 void DrawLevelGround(Level *level)
228 {
229   // draw checkerboard ground pattern
230   for (int x = -5; x <= 5; x += 1)
231   {
232     for (int y = -5; y <= 5; y += 1)
233     {
234       Model *model = (x + y) % 2 == 0 ? &floorTileAModel : &floorTileBModel;
235       DrawModel(*model, (Vector3){x, 0.0f, y}, 1.0f, WHITE);
236     }
237   }
238 
239   int oldSeed = GetRandomValue(0, 0xfffffff);
240   SetRandomSeed(level->seed);
241   // increase probability for trees via duplicated entries
242   Model borderModels[64];
243   int maxRockCount = GetRandomValue(2, 6);
244   int maxTreeCount = GetRandomValue(10, 20);
245   int maxFirTreeCount = GetRandomValue(5, 10);
246   int maxLeafTreeCount = maxTreeCount - maxFirTreeCount;
247   int grassPatchCount = GetRandomValue(5, 30);
248 
249   int modelCount = 0;
250   for (int i = 0; i < maxRockCount && modelCount < 63; i++)
251   {
252     borderModels[modelCount++] = rockModels[GetRandomValue(0, 5)];
253   }
254   for (int i = 0; i < maxLeafTreeCount && modelCount < 63; i++)
255   {
256     borderModels[modelCount++] = treeModel[GetRandomValue(0, 1)];
257   }
258   for (int i = 0; i < maxFirTreeCount && modelCount < 63; i++)
259   {
260     borderModels[modelCount++] = firTreeModel[GetRandomValue(0, 1)];
261   }
262   for (int i = 0; i < grassPatchCount && modelCount < 63; i++)
263   {
264     borderModels[modelCount++] = grassPatchModel[0];
265   }
266 
267   // draw some objects around the border of the map
268   Vector3 up = {0, 1, 0};
269   // a pseudo random number generator to get the same result every time
270   const float wiggle = 0.75f;
271   const int layerCount = 3;
272   for (int layer = 0; layer < layerCount; layer++)
273   {
274     int layerPos = 6 + layer;
275     for (int x = -6 + layer; x <= 6 + layer; x += 1)
276     {
277       DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)], 
278         (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, -layerPos + GetRandomFloat(0.0f, wiggle)}, 
279         up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
280       DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)], 
281         (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, layerPos + GetRandomFloat(0.0f, wiggle)}, 
282         up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
283     }
284 
285     for (int z = -5 + layer; z <= 5 + layer; z += 1)
286     {
287       DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)], 
288         (Vector3){-layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)}, 
289         up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
290       DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)], 
291         (Vector3){layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)}, 
292         up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
293     }
294   }
295 
296   SetRandomSeed(oldSeed);
297 }
298 
299 void DrawEnemyPath(Level *level, Color arrowColor)
300 {
301   const int castleX = 0, castleY = 0;
302   const int maxWaypointCount = 200;
303   const float timeStep = 1.0f;
304   Vector3 arrowScale = {0.75f, 0.75f, 0.75f};
305 
306   // we start with a time offset to simulate the path, 
307   // this way the arrows are animated in a forward moving direction
308   // The time is wrapped around the time step to get a smooth animation
309   float timeOffset = fmodf(GetTime(), timeStep);
310 
311   for (int i = 0; i < ENEMY_MAX_WAVE_COUNT; i++)
312   {
313     EnemyWave *wave = &level->waves[i];
314     if (wave->wave != level->currentWave)
315     {
316       continue;
317     }
318 
319     // use this dummy enemy to simulate the path
320     Enemy dummy = {
321       .enemyType = ENEMY_TYPE_MINION,
322       .simPosition = (Vector2){wave->spawnPosition.x, wave->spawnPosition.y},
323       .nextX = wave->spawnPosition.x,
324       .nextY = wave->spawnPosition.y,
325       .currentX = wave->spawnPosition.x,
326       .currentY = wave->spawnPosition.y,
327     };
328 
329     float deltaTime = timeOffset;
330     for (int j = 0; j < maxWaypointCount; j++)
331     {
332       int waypointPassedCount = 0;
333       Vector2 pos = EnemyGetPosition(&dummy, deltaTime, &dummy.simVelocity, &waypointPassedCount);
334       // after the initial variable starting offset, we use a fixed time step
335       deltaTime = timeStep;
336       dummy.simPosition = pos;
337 
338       // Update the dummy's position just like we do in the regular enemy update loop
339       for (int k = 0; k < waypointPassedCount; k++)
340       {
341         dummy.currentX = dummy.nextX;
342         dummy.currentY = dummy.nextY;
343         if (EnemyGetNextPosition(dummy.currentX, dummy.currentY, &dummy.nextX, &dummy.nextY) &&
344           Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
345         {
346           break;
347         }
348       }
349       if (Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
350       {
351         break;
352       }
353       
354       // get the angle we need to rotate the arrow model. The velocity is just fine for this.
355       float angle = atan2f(dummy.simVelocity.x, dummy.simVelocity.y) * RAD2DEG - 90.0f;
356       DrawModelEx(pathArrowModel, (Vector3){pos.x, 0.15f, pos.y}, (Vector3){0, 1, 0}, angle, arrowScale, arrowColor);
357     }
358   }
359 }
360 
361 void DrawLevelBuildingState(Level *level)
362 {
363   BeginMode3D(level->camera);
364   DrawLevelGround(level);
365 
366   int blockedCellCount = 0;
367   Vector2 blockedCells[1];
368   Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera);
369   float planeDistance = ray.position.y / -ray.direction.y;
370   float planeX = ray.direction.x * planeDistance + ray.position.x;
371   float planeY = ray.direction.z * planeDistance + ray.position.z;
372   int16_t mapX = (int16_t)floorf(planeX + 0.5f);
373   int16_t mapY = (int16_t)floorf(planeY + 0.5f);
374   if (level->placementMode && !guiState.isBlocked && mapX >= -5 && mapX <= 5 && mapY >= -5 && mapY <= 5)
375   {
376     DrawCubeWires((Vector3){mapX, 0.2f, mapY}, 1.0f, 0.4f, 1.0f, RED);
377     blockedCells[blockedCellCount++] = (Vector2){mapX, mapY};
378     if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
379     {
380       if (TowerTryAdd(level->placementMode, mapX, mapY))
381       {
382         level->playerGold -= GetTowerCosts(level->placementMode);
383         level->placementMode = TOWER_TYPE_NONE;
384       }
385     }
386   }
387 
388   PathFindingMapUpdate(blockedCellCount, blockedCells);
389   TowerDraw();
390   EnemyDraw();
391   ProjectileDraw();
392   ParticleDraw();
393 
394   // disable depth testing for the path arrows
395   // flush the 3D batch to draw the arrows on top of everything
396   rlDrawRenderBatchActive();
397   rlDisableDepthTest();
398   DrawEnemyPath(level, (Color){64, 64, 64, 160});
399 
400   rlDrawRenderBatchActive();
401   rlEnableDepthTest();
402   DrawEnemyPath(level, WHITE);
403 
404   guiState.isBlocked = 0;
405 
406   EndMode3D();
407 
408   TowerDrawHealthBars(level->camera);
409 
410   static ButtonState buildWallButtonState = {0};
411   static ButtonState buildGunButtonState = {0};
412   buildWallButtonState.isSelected = level->placementMode == TOWER_TYPE_WALL;
413   buildGunButtonState.isSelected = level->placementMode == TOWER_TYPE_ARCHER;
414 
415   DrawBuildingBuildButton(level, 10, 10, 110, 30, TOWER_TYPE_WALL, "Wall");
416   DrawBuildingBuildButton(level, 10, 50, 110, 30, TOWER_TYPE_ARCHER, "Archer");
417   DrawBuildingBuildButton(level, 10, 90, 110, 30, TOWER_TYPE_BALLISTA, "Ballista");
418   DrawBuildingBuildButton(level, 10, 130, 110, 30, TOWER_TYPE_CATAPULT, "Catapult");
419 
420   if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
421   {
422     level->nextState = LEVEL_STATE_RESET;
423   }
424   
425   if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0))
426   {
427     level->nextState = LEVEL_STATE_BATTLE;
428   }
429 
430   const char *text = "Building phase";
431   int textWidth = MeasureText(text, 20);
432   DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
433 }
434 
435 void InitBattleStateConditions(Level *level)
436 {
437   level->state = LEVEL_STATE_BATTLE;
438   level->nextState = LEVEL_STATE_NONE;
439   level->waveEndTimer = 0.0f;
440   for (int i = 0; i < 10; i++)
441   {
442     EnemyWave *wave = &level->waves[i];
443     wave->spawned = 0;
444     wave->timeToSpawnNext = wave->delay;
445   }
446 }
447 
448 void DrawLevelBattleState(Level *level)
449 {
450   BeginMode3D(level->camera);
451   DrawLevelGround(level);
452   TowerDraw();
453   EnemyDraw();
454   ProjectileDraw();
455   ParticleDraw();
456   guiState.isBlocked = 0;
457   EndMode3D();
458 
459   EnemyDrawHealthbars(level->camera);
460   TowerDrawHealthBars(level->camera);
461 
462   if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
463   {
464     level->nextState = LEVEL_STATE_RESET;
465   }
466 
467   int maxCount = 0;
468   int remainingCount = 0;
469   for (int i = 0; i < 10; i++)
470   {
471     EnemyWave *wave = &level->waves[i];
472     if (wave->wave != level->currentWave)
473     {
474       continue;
475     }
476     maxCount += wave->count;
477     remainingCount += wave->count - wave->spawned;
478   }
479   int aliveCount = EnemyCount();
480   remainingCount += aliveCount;
481 
482   const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount);
483   int textWidth = MeasureText(text, 20);
484   DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
485 }
486 
487 void DrawLevel(Level *level)
488 {
489   switch (level->state)
490   {
491     case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break;
492     case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break;
493     case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break;
494     case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break;
495     default: break;
496   }
497 
498   DrawLevelHud(level);
499 }
500 
501 void UpdateLevel(Level *level)
502 {
503   if (level->state == LEVEL_STATE_BATTLE)
504   {
505     int activeWaves = 0;
506     for (int i = 0; i < 10; i++)
507     {
508       EnemyWave *wave = &level->waves[i];
509       if (wave->spawned >= wave->count || wave->wave != level->currentWave)
510       {
511         continue;
512       }
513       activeWaves++;
514       wave->timeToSpawnNext -= gameTime.deltaTime;
515       if (wave->timeToSpawnNext <= 0.0f)
516       {
517         Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y);
518         if (enemy)
519         {
520           wave->timeToSpawnNext = wave->interval;
521           wave->spawned++;
522         }
523       }
524     }
525     if (GetTowerByType(TOWER_TYPE_BASE) == 0) {
526       level->waveEndTimer += gameTime.deltaTime;
527       if (level->waveEndTimer >= 2.0f)
528       {
529         level->nextState = LEVEL_STATE_LOST_WAVE;
530       }
531     }
532     else if (activeWaves == 0 && EnemyCount() == 0)
533     {
534       level->waveEndTimer += gameTime.deltaTime;
535       if (level->waveEndTimer >= 2.0f)
536       {
537         level->nextState = LEVEL_STATE_WON_WAVE;
538       }
539     }
540   }
541 
542   PathFindingMapUpdate(0, 0);
543   EnemyUpdate();
544   TowerUpdate();
545   ProjectileUpdate();
546   ParticleUpdate();
547 
548   if (level->nextState == LEVEL_STATE_RESET)
549   {
550     InitLevel(level);
551   }
552   
553   if (level->nextState == LEVEL_STATE_BATTLE)
554   {
555     InitBattleStateConditions(level);
556   }
557   
558   if (level->nextState == LEVEL_STATE_WON_WAVE)
559   {
560     level->currentWave++;
561     level->state = LEVEL_STATE_WON_WAVE;
562   }
563   
564   if (level->nextState == LEVEL_STATE_LOST_WAVE)
565   {
566     level->state = LEVEL_STATE_LOST_WAVE;
567   }
568 
569   if (level->nextState == LEVEL_STATE_BUILDING)
570   {
571     level->state = LEVEL_STATE_BUILDING;
572   }
573 
574   if (level->nextState == LEVEL_STATE_WON_LEVEL)
575   {
576     // make something of this later
577     InitLevel(level);
578   }
579 
580   level->nextState = LEVEL_STATE_NONE;
581 }
582 
583 float nextSpawnTime = 0.0f;
584 
585 void ResetGame()
586 {
587   InitLevel(currentLevel);
588 }
589 
590 void InitGame()
591 {
592   TowerInit();
593   EnemyInit();
594   ProjectileInit();
595   ParticleInit();
596   PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f);
597 
598   currentLevel = levels;
599   InitLevel(currentLevel);
600 }
601 
602 //# Immediate GUI functions
603 
604 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth)
605 {
606   const float healthBarHeight = 6.0f;
607   const float healthBarOffset = 15.0f;
608   const float inset = 2.0f;
609   const float innerWidth = healthBarWidth - inset * 2;
610   const float innerHeight = healthBarHeight - inset * 2;
611 
612   Vector2 screenPos = GetWorldToScreen(position, camera);
613   float centerX = screenPos.x - healthBarWidth * 0.5f;
614   float topY = screenPos.y - healthBarOffset;
615   DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK);
616   float healthWidth = innerWidth * healthRatio;
617   DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor);
618 }
619 
620 int Button(const char *text, int x, int y, int width, int height, ButtonState *state)
621 {
622   Rectangle bounds = {x, y, width, height};
623   int isPressed = 0;
624   int isSelected = state && state->isSelected;
625   int isDisabled = state && state->isDisabled;
626   if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !isDisabled)
627   {
628     Color color = isSelected ? DARKGRAY : GRAY;
629     DrawRectangle(x, y, width, height, color);
630     if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
631     {
632       isPressed = 1;
633     }
634     guiState.isBlocked = 1;
635   }
636   else
637   {
638     Color color = isSelected ? WHITE : LIGHTGRAY;
639     DrawRectangle(x, y, width, height, color);
640   }
641   Font font = GetFontDefault();
642   Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1);
643   Color textColor = isDisabled ? GRAY : BLACK;
644   DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, textColor);
645   return isPressed;
646 }
647 
648 //# Main game loop
649 
650 void GameUpdate()
651 {
652   float dt = GetFrameTime();
653   // cap maximum delta time to 0.1 seconds to prevent large time steps
654   if (dt > 0.1f) dt = 0.1f;
655   gameTime.time += dt;
656   gameTime.deltaTime = dt;
657 
658   UpdateLevel(currentLevel);
659 }
660 
661 int main(void)
662 {
663   int screenWidth, screenHeight;
664   GetPreferredSize(&screenWidth, &screenHeight);
665   InitWindow(screenWidth, screenHeight, "Tower defense");
666   SetTargetFPS(30);
667 
668   LoadAssets();
669   InitGame();
670 
671   while (!WindowShouldClose())
672   {
673     if (IsPaused()) {
674       // canvas is not visible in browser - do nothing
675       continue;
676     }
677 
678     BeginDrawing();
679     ClearBackground((Color){0x4E, 0x63, 0x26, 0xFF});
680 
681     GameUpdate();
682     DrawLevel(currentLevel);
683 
684     EndDrawing();
685   }
686 
687   CloseWindow();
688 
689   return 0;
690 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 
  4 // The queue is a simple array of nodes, we add nodes to the end and remove
  5 // nodes from the front. We keep the array around to avoid unnecessary allocations
  6 static PathfindingNode *pathfindingNodeQueue = 0;
  7 static int pathfindingNodeQueueCount = 0;
  8 static int pathfindingNodeQueueCapacity = 0;
  9 
 10 // The pathfinding map stores the distances from the castle to each cell in the map.
 11 static PathfindingMap pathfindingMap = {0};
 12 
 13 void PathfindingMapInit(int width, int height, Vector3 translate, float scale)
 14 {
 15   // transforming between map space and world space allows us to adapt 
 16   // position and scale of the map without changing the pathfinding data
 17   pathfindingMap.toWorldSpace = MatrixTranslate(translate.x, translate.y, translate.z);
 18   pathfindingMap.toWorldSpace = MatrixMultiply(pathfindingMap.toWorldSpace, MatrixScale(scale, scale, scale));
 19   pathfindingMap.toMapSpace = MatrixInvert(pathfindingMap.toWorldSpace);
 20   pathfindingMap.width = width;
 21   pathfindingMap.height = height;
 22   pathfindingMap.scale = scale;
 23   pathfindingMap.distances = (float *)MemAlloc(width * height * sizeof(float));
 24   for (int i = 0; i < width * height; i++)
 25   {
 26     pathfindingMap.distances[i] = -1.0f;
 27   }
 28 
 29   pathfindingMap.towerIndex = (long *)MemAlloc(width * height * sizeof(long));
 30   pathfindingMap.deltaSrc = (DeltaSrc *)MemAlloc(width * height * sizeof(DeltaSrc));
 31 }
 32 
 33 static void PathFindingNodePush(int16_t x, int16_t y, int16_t fromX, int16_t fromY, float distance)
 34 {
 35   if (pathfindingNodeQueueCount >= pathfindingNodeQueueCapacity)
 36   {
 37     pathfindingNodeQueueCapacity = pathfindingNodeQueueCapacity == 0 ? 256 : pathfindingNodeQueueCapacity * 2;
 38     // we use MemAlloc/MemRealloc to allocate memory for the queue
 39     // I am not entirely sure if MemRealloc allows passing a null pointer
 40     // so we check if the pointer is null and use MemAlloc in that case
 41     if (pathfindingNodeQueue == 0)
 42     {
 43       pathfindingNodeQueue = (PathfindingNode *)MemAlloc(pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
 44     }
 45     else
 46     {
 47       pathfindingNodeQueue = (PathfindingNode *)MemRealloc(pathfindingNodeQueue, pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
 48     }
 49   }
 50 
 51   PathfindingNode *node = &pathfindingNodeQueue[pathfindingNodeQueueCount++];
 52   node->x = x;
 53   node->y = y;
 54   node->fromX = fromX;
 55   node->fromY = fromY;
 56   node->distance = distance;
 57 }
 58 
 59 static PathfindingNode *PathFindingNodePop()
 60 {
 61   if (pathfindingNodeQueueCount == 0)
 62   {
 63     return 0;
 64   }
 65   // we return the first node in the queue; we want to return a pointer to the node
 66   // so we can return 0 if the queue is empty. 
 67   // We should _not_ return a pointer to the element in the list, because the list
 68   // may be reallocated and the pointer would become invalid. Or the 
 69   // popped element is overwritten by the next push operation.
 70   // Using static here means that the variable is permanently allocated.
 71   static PathfindingNode node;
 72   node = pathfindingNodeQueue[0];
 73   // we shift all nodes one position to the front
 74   for (int i = 1; i < pathfindingNodeQueueCount; i++)
 75   {
 76     pathfindingNodeQueue[i - 1] = pathfindingNodeQueue[i];
 77   }
 78   --pathfindingNodeQueueCount;
 79   return &node;
 80 }
 81 
 82 float PathFindingGetDistance(int mapX, int mapY)
 83 {
 84   if (mapX < 0 || mapX >= pathfindingMap.width || mapY < 0 || mapY >= pathfindingMap.height)
 85   {
 86     // when outside the map, we return the manhattan distance to the castle (0,0)
 87     return fabsf((float)mapX) + fabsf((float)mapY);
 88   }
 89 
 90   return pathfindingMap.distances[mapY * pathfindingMap.width + mapX];
 91 }
 92 
 93 // transform a world position to a map position in the array; 
 94 // returns true if the position is inside the map
 95 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY)
 96 {
 97   Vector3 mapPosition = Vector3Transform(worldPosition, pathfindingMap.toMapSpace);
 98   *mapX = (int16_t)mapPosition.x;
 99   *mapY = (int16_t)mapPosition.z;
100   return *mapX >= 0 && *mapX < pathfindingMap.width && *mapY >= 0 && *mapY < pathfindingMap.height;
101 }
102 
103 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells)
104 {
105   const int castleX = 0, castleY = 0;
106   int16_t castleMapX, castleMapY;
107   if (!PathFindingFromWorldToMapPosition((Vector3){castleX, 0.0f, castleY}, &castleMapX, &castleMapY))
108   {
109     return;
110   }
111   int width = pathfindingMap.width, height = pathfindingMap.height;
112 
113   // reset the distances to -1
114   for (int i = 0; i < width * height; i++)
115   {
116     pathfindingMap.distances[i] = -1.0f;
117   }
118   // reset the tower indices
119   for (int i = 0; i < width * height; i++)
120   {
121     pathfindingMap.towerIndex[i] = -1;
122   }
123   // reset the delta src
124   for (int i = 0; i < width * height; i++)
125   {
126     pathfindingMap.deltaSrc[i].x = 0;
127     pathfindingMap.deltaSrc[i].y = 0;
128   }
129 
130   for (int i = 0; i < blockedCellCount; i++)
131   {
132     int16_t mapX, mapY;
133     if (!PathFindingFromWorldToMapPosition((Vector3){blockedCells[i].x, 0.0f, blockedCells[i].y}, &mapX, &mapY))
134     {
135       continue;
136     }
137     int index = mapY * width + mapX;
138     pathfindingMap.towerIndex[index] = -2;
139   }
140 
141   for (int i = 0; i < towerCount; i++)
142   {
143     Tower *tower = &towers[i];
144     if (tower->towerType == TOWER_TYPE_NONE || tower->towerType == TOWER_TYPE_BASE)
145     {
146       continue;
147     }
148     int16_t mapX, mapY;
149     // technically, if the tower cell scale is not in sync with the pathfinding map scale,
150     // this would not work correctly and needs to be refined to allow towers covering multiple cells
151     // or having multiple towers in one cell; for simplicity, we assume that the tower covers exactly
152     // one cell. For now.
153     if (!PathFindingFromWorldToMapPosition((Vector3){tower->x, 0.0f, tower->y}, &mapX, &mapY))
154     {
155       continue;
156     }
157     int index = mapY * width + mapX;
158     pathfindingMap.towerIndex[index] = i;
159   }
160 
161   // we start at the castle and add the castle to the queue
162   pathfindingMap.maxDistance = 0.0f;
163   pathfindingNodeQueueCount = 0;
164   PathFindingNodePush(castleMapX, castleMapY, castleMapX, castleMapY, 0.0f);
165   PathfindingNode *node = 0;
166   while ((node = PathFindingNodePop()))
167   {
168     if (node->x < 0 || node->x >= width || node->y < 0 || node->y >= height)
169     {
170       continue;
171     }
172     int index = node->y * width + node->x;
173     if (pathfindingMap.distances[index] >= 0 && pathfindingMap.distances[index] <= node->distance)
174     {
175       continue;
176     }
177 
178     int deltaX = node->x - node->fromX;
179     int deltaY = node->y - node->fromY;
180     // even if the cell is blocked by a tower, we still may want to store the direction
181     // (though this might not be needed, IDK right now)
182     pathfindingMap.deltaSrc[index].x = (char) deltaX;
183     pathfindingMap.deltaSrc[index].y = (char) deltaY;
184 
185     // we skip nodes that are blocked by towers or by the provided blocked cells
186     if (pathfindingMap.towerIndex[index] != -1)
187     {
188       node->distance += 8.0f;
189     }
190     pathfindingMap.distances[index] = node->distance;
191     pathfindingMap.maxDistance = fmaxf(pathfindingMap.maxDistance, node->distance);
192     PathFindingNodePush(node->x, node->y + 1, node->x, node->y, node->distance + 1.0f);
193     PathFindingNodePush(node->x, node->y - 1, node->x, node->y, node->distance + 1.0f);
194     PathFindingNodePush(node->x + 1, node->y, node->x, node->y, node->distance + 1.0f);
195     PathFindingNodePush(node->x - 1, node->y, node->x, node->y, node->distance + 1.0f);
196   }
197 }
198 
199 void PathFindingMapDraw()
200 {
201   float cellSize = pathfindingMap.scale * 0.9f;
202   float highlightDistance = fmodf(GetTime() * 4.0f, pathfindingMap.maxDistance);
203   for (int x = 0; x < pathfindingMap.width; x++)
204   {
205     for (int y = 0; y < pathfindingMap.height; y++)
206     {
207       float distance = pathfindingMap.distances[y * pathfindingMap.width + x];
208       float colorV = distance < 0 ? 0 : fminf(distance / pathfindingMap.maxDistance, 1.0f);
209       Color color = distance < 0 ? BLUE : (Color){fminf(colorV, 1.0f) * 255, 0, 0, 255};
210       Vector3 position = Vector3Transform((Vector3){x, -0.25f, y}, pathfindingMap.toWorldSpace);
211       // animate the distance "wave" to show how the pathfinding algorithm expands
212       // from the castle
213       if (distance + 0.5f > highlightDistance && distance - 0.5f < highlightDistance)
214       {
215         color = BLACK;
216       }
217       DrawCube(position, cellSize, 0.1f, cellSize, color);
218     }
219   }
220 }
221 
222 Vector2 PathFindingGetGradient(Vector3 world)
223 {
224   int16_t mapX, mapY;
225   if (PathFindingFromWorldToMapPosition(world, &mapX, &mapY))
226   {
227     DeltaSrc delta = pathfindingMap.deltaSrc[mapY * pathfindingMap.width + mapX];
228     return (Vector2){(float)-delta.x, (float)-delta.y};
229   }
230   // fallback to a simple gradient calculation
231   float n = PathFindingGetDistance(mapX, mapY - 1);
232   float s = PathFindingGetDistance(mapX, mapY + 1);
233   float w = PathFindingGetDistance(mapX - 1, mapY);
234   float e = PathFindingGetDistance(mapX + 1, mapY);
235   return (Vector2){w - e + 0.25f, n - s + 0.125f};
236 }
  1 #ifndef TD_TUT_2_MAIN_H
  2 #define TD_TUT_2_MAIN_H
  3 
  4 #include <inttypes.h>
  5 
  6 #include "raylib.h"
  7 #include "preferred_size.h"
  8 
  9 //# Declarations
 10 
 11 #define ENEMY_MAX_PATH_COUNT 8
 12 #define ENEMY_MAX_COUNT 400
 13 #define ENEMY_TYPE_NONE 0
 14 #define ENEMY_TYPE_MINION 1
 15 
 16 #define PARTICLE_MAX_COUNT 400
 17 #define PARTICLE_TYPE_NONE 0
 18 #define PARTICLE_TYPE_EXPLOSION 1
 19 
 20 typedef struct Particle
 21 {
 22   uint8_t particleType;
 23   float spawnTime;
 24   float lifetime;
 25   Vector3 position;
 26   Vector3 velocity;
 27   Vector3 scale;
 28 } Particle;
 29 
 30 #define TOWER_MAX_COUNT 400
 31 enum TowerType
 32 {
 33   TOWER_TYPE_NONE,
 34   TOWER_TYPE_BASE,
 35   TOWER_TYPE_ARCHER,
 36   TOWER_TYPE_BALLISTA,
 37   TOWER_TYPE_CATAPULT,
 38   TOWER_TYPE_WALL,
 39   TOWER_TYPE_COUNT
 40 };
 41 
 42 typedef struct HitEffectConfig
 43 {
 44   float damage;
 45   float areaDamageRadius;
 46   float pushbackPowerDistance;
 47 } HitEffectConfig;
 48 
 49 typedef struct TowerTypeConfig
 50 {
 51   float cooldown;
 52   float range;
 53   float projectileSpeed;
 54   
 55   uint8_t cost;
 56   uint8_t projectileType;
 57   uint16_t maxHealth;
 58 
 59   HitEffectConfig hitEffect;
 60 } TowerTypeConfig;
 61 
 62 typedef struct Tower
 63 {
 64   int16_t x, y;
 65   uint8_t towerType;
 66   Vector2 lastTargetPosition;
 67   float cooldown;
 68   float damage;
 69 } Tower;
 70 
 71 typedef struct GameTime
 72 {
 73   float time;
 74   float deltaTime;
 75 } GameTime;
 76 
 77 typedef struct ButtonState {
 78   char isSelected;
 79   char isDisabled;
 80 } ButtonState;
 81 
 82 typedef struct GUIState {
 83   int isBlocked;
 84 } GUIState;
 85 
 86 typedef enum LevelState
 87 {
 88   LEVEL_STATE_NONE,
 89   LEVEL_STATE_BUILDING,
 90   LEVEL_STATE_BATTLE,
 91   LEVEL_STATE_WON_WAVE,
 92   LEVEL_STATE_LOST_WAVE,
 93   LEVEL_STATE_WON_LEVEL,
 94   LEVEL_STATE_RESET,
 95 } LevelState;
 96 
 97 typedef struct EnemyWave {
 98   uint8_t enemyType;
 99   uint8_t wave;
100   uint16_t count;
101   float interval;
102   float delay;
103   Vector2 spawnPosition;
104 
105   uint16_t spawned;
106   float timeToSpawnNext;
107 } EnemyWave;
108 
109 #define ENEMY_MAX_WAVE_COUNT 10
110 
111 typedef struct Level
112 {
113   int seed;
114   LevelState state;
115   LevelState nextState;
116   Camera3D camera;
117   int placementMode;
118 
119   int initialGold;
120   int playerGold;
121 
122   EnemyWave waves[ENEMY_MAX_WAVE_COUNT];
123   int currentWave;
124   float waveEndTimer;
125 } Level;
126 
127 typedef struct DeltaSrc
128 {
129   char x, y;
130 } DeltaSrc;
131 
132 typedef struct PathfindingMap
133 {
134   int width, height;
135   float scale;
136   float *distances;
137   long *towerIndex; 
138   DeltaSrc *deltaSrc;
139   float maxDistance;
140   Matrix toMapSpace;
141   Matrix toWorldSpace;
142 } PathfindingMap;
143 
144 // when we execute the pathfinding algorithm, we need to store the active nodes
145 // in a queue. Each node has a position, a distance from the start, and the
146 // position of the node that we came from.
147 typedef struct PathfindingNode
148 {
149   int16_t x, y, fromX, fromY;
150   float distance;
151 } PathfindingNode;
152 
153 typedef struct EnemyId
154 {
155   uint16_t index;
156   uint16_t generation;
157 } EnemyId;
158 
159 typedef struct EnemyClassConfig
160 {
161   float speed;
162   float health;
163   float radius;
164   float maxAcceleration;
165   float requiredContactTime;
166   float explosionDamage;
167   float explosionRange;
168   float explosionPushbackPower;
169   int goldValue;
170 } EnemyClassConfig;
171 
172 typedef struct Enemy
173 {
174   int16_t currentX, currentY;
175   int16_t nextX, nextY;
176   Vector2 simPosition;
177   Vector2 simVelocity;
178   uint16_t generation;
179   float walkedDistance;
180   float startMovingTime;
181   float damage, futureDamage;
182   float contactTime;
183   uint8_t enemyType;
184   uint8_t movePathCount;
185   Vector2 movePath[ENEMY_MAX_PATH_COUNT];
186 } Enemy;
187 
188 // a unit that uses sprites to be drawn
189 #define SPRITE_UNIT_PHASE_WEAPON_IDLE 0
190 #define SPRITE_UNIT_PHASE_WEAPON_COOLDOWN 1
191 typedef struct SpriteUnit
192 {
193   Rectangle srcRect;
194   Vector2 offset;
195   int frameCount;
196   float frameDuration;
197   Rectangle srcWeaponIdleRect;
198   Vector2 srcWeaponIdleOffset;
199   Rectangle srcWeaponCooldownRect;
200   Vector2 srcWeaponCooldownOffset;
201 } SpriteUnit;
202 
203 #define PROJECTILE_MAX_COUNT 1200
204 #define PROJECTILE_TYPE_NONE 0
205 #define PROJECTILE_TYPE_ARROW 1
206 #define PROJECTILE_TYPE_CATAPULT 2
207 #define PROJECTILE_TYPE_BALLISTA 3
208 
209 typedef struct Projectile
210 {
211   uint8_t projectileType;
212   float shootTime;
213   float arrivalTime;
214   float distance;
215   Vector3 position;
216   Vector3 target;
217   Vector3 directionNormal;
218   EnemyId targetEnemy;
219   HitEffectConfig hitEffectConfig;
220 } Projectile;
221 
222 //# Function declarations
223 float TowerGetMaxHealth(Tower *tower);
224 int Button(const char *text, int x, int y, int width, int height, ButtonState *state);
225 int EnemyAddDamageRange(Vector2 position, float range, float damage);
226 int EnemyAddDamage(Enemy *enemy, float damage);
227 
228 //# Enemy functions
229 void EnemyInit();
230 void EnemyDraw();
231 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource);
232 void EnemyUpdate();
233 float EnemyGetCurrentMaxSpeed(Enemy *enemy);
234 float EnemyGetMaxHealth(Enemy *enemy);
235 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY);
236 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount);
237 EnemyId EnemyGetId(Enemy *enemy);
238 Enemy *EnemyTryResolve(EnemyId enemyId);
239 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY);
240 int EnemyAddDamage(Enemy *enemy, float damage);
241 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range);
242 int EnemyCount();
243 void EnemyDrawHealthbars(Camera3D camera);
244 
245 //# Tower functions
246 void TowerInit();
247 Tower *TowerGetAt(int16_t x, int16_t y);
248 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y);
249 Tower *GetTowerByType(uint8_t towerType);
250 int GetTowerCosts(uint8_t towerType);
251 float TowerGetMaxHealth(Tower *tower);
252 void TowerDraw();
253 void TowerUpdate();
254 void TowerDrawHealthBars(Camera3D camera);
255 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase);
256 
257 //# Particles
258 void ParticleInit();
259 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime);
260 void ParticleUpdate();
261 void ParticleDraw();
262 
263 //# Projectiles
264 void ProjectileInit();
265 void ProjectileDraw();
266 void ProjectileUpdate();
267 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig);
268 
269 //# Pathfinding map
270 void PathfindingMapInit(int width, int height, Vector3 translate, float scale);
271 float PathFindingGetDistance(int mapX, int mapY);
272 Vector2 PathFindingGetGradient(Vector3 world);
273 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY);
274 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells);
275 void PathFindingMapDraw();
276 
277 //# UI
278 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth);
279 
280 //# Level
281 void DrawLevelGround(Level *level);
282 void DrawEnemyPath(Level *level, Color arrowColor);
283 
284 //# variables
285 extern Level *currentLevel;
286 extern Enemy enemies[ENEMY_MAX_COUNT];
287 extern int enemyCount;
288 extern EnemyClassConfig enemyClassConfigs[];
289 
290 extern GUIState guiState;
291 extern GameTime gameTime;
292 extern Tower towers[TOWER_MAX_COUNT];
293 extern int towerCount;
294 
295 extern Texture2D palette, spriteSheet;
296 
297 #endif
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 
  4 static TowerTypeConfig towerTypeConfigs[TOWER_TYPE_COUNT] = {
  5     [TOWER_TYPE_BASE] = {
  6         .maxHealth = 10,
  7     },
  8     [TOWER_TYPE_ARCHER] = {
  9         .cooldown = 0.5f,
 10         .range = 3.0f,
 11         .cost = 6,
 12         .maxHealth = 10,
 13         .projectileSpeed = 4.0f,
 14         .projectileType = PROJECTILE_TYPE_ARROW,
 15         .hitEffect = {
 16           .damage = 3.0f,
 17         }
 18     },
 19     [TOWER_TYPE_BALLISTA] = {
 20         .cooldown = 1.5f,
 21         .range = 6.0f,
 22         .cost = 9,
 23         .maxHealth = 10,
 24         .projectileSpeed = 10.0f,
 25         .projectileType = PROJECTILE_TYPE_BALLISTA,
 26         .hitEffect = {
 27           .damage = 6.0f,
 28           .pushbackPowerDistance = 0.25f,
 29         }
 30     },
 31     [TOWER_TYPE_CATAPULT] = {
 32         .cooldown = 1.7f,
 33         .range = 5.0f,
 34         .cost = 10,
 35         .maxHealth = 10,
 36         .projectileSpeed = 3.0f,
 37         .projectileType = PROJECTILE_TYPE_CATAPULT,
 38         .hitEffect = {
 39           .damage = 2.0f,
 40           .areaDamageRadius = 1.75f,
 41         }
 42     },
 43     [TOWER_TYPE_WALL] = {
 44         .cost = 2,
 45         .maxHealth = 10,
 46     },
 47 };
 48 
 49 Tower towers[TOWER_MAX_COUNT];
 50 int towerCount = 0;
 51 
 52 Model towerModels[TOWER_TYPE_COUNT];
 53 
 54 // definition of our archer unit
 55 SpriteUnit archerUnit = {
 56     .srcRect = {0, 0, 16, 16},
 57     .offset = {7, 1},
 58     .frameCount = 1,
 59     .frameDuration = 0.0f,
 60     .srcWeaponIdleRect = {16, 0, 6, 16},
 61     .srcWeaponIdleOffset = {8, 0},
 62     .srcWeaponCooldownRect = {22, 0, 11, 16},
 63     .srcWeaponCooldownOffset = {10, 0},
 64 };
 65 
 66 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase)
 67 {
 68   float xScale = flip ? -1.0f : 1.0f;
 69   Camera3D camera = currentLevel->camera;
 70   float size = 0.5f;
 71   Vector2 offset = (Vector2){ unit.offset.x / 16.0f * size, unit.offset.y / 16.0f * size * xScale };
 72   Vector2 scale = (Vector2){ unit.srcRect.width / 16.0f * size, unit.srcRect.height / 16.0f * size };
 73   // we want the sprite to face the camera, so we need to calculate the up vector
 74   Vector3 forward = Vector3Subtract(camera.target, camera.position);
 75   Vector3 up = {0, 1, 0};
 76   Vector3 right = Vector3CrossProduct(forward, up);
 77   up = Vector3Normalize(Vector3CrossProduct(right, forward));
 78 
 79   Rectangle srcRect = unit.srcRect;
 80   if (unit.frameCount > 1)
 81   {
 82     srcRect.x += (int)(t / unit.frameDuration) % unit.frameCount * srcRect.width;
 83   }
 84   if (flip)
 85   {
 86     srcRect.x += srcRect.width;
 87     srcRect.width = -srcRect.width;
 88   }
 89   DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
 90 
 91   if (phase == SPRITE_UNIT_PHASE_WEAPON_COOLDOWN && unit.srcWeaponCooldownRect.width > 0)
 92   {
 93     offset = (Vector2){ unit.srcWeaponCooldownOffset.x / 16.0f * size, unit.srcWeaponCooldownOffset.y / 16.0f * size };
 94     scale = (Vector2){ unit.srcWeaponCooldownRect.width / 16.0f * size, unit.srcWeaponCooldownRect.height / 16.0f * size };
 95     srcRect = unit.srcWeaponCooldownRect;
 96     if (flip)
 97     {
 98       // position.x = flip * scale.x * 0.5f;
 99       srcRect.x += srcRect.width;
100       srcRect.width = -srcRect.width;
101       offset.x = scale.x - offset.x;
102     }
103     DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
104   }
105   else if (phase == SPRITE_UNIT_PHASE_WEAPON_IDLE && unit.srcWeaponIdleRect.width > 0)
106   {
107     offset = (Vector2){ unit.srcWeaponIdleOffset.x / 16.0f * size, unit.srcWeaponIdleOffset.y / 16.0f * size };
108     scale = (Vector2){ unit.srcWeaponIdleRect.width / 16.0f * size, unit.srcWeaponIdleRect.height / 16.0f * size };
109     srcRect = unit.srcWeaponIdleRect;
110     if (flip)
111     {
112       // position.x = flip * scale.x * 0.5f;
113       srcRect.x += srcRect.width;
114       srcRect.width = -srcRect.width;
115       offset.x = scale.x - offset.x;
116     }
117     DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
118   }
119 }
120 
121 void TowerInit()
122 {
123   for (int i = 0; i < TOWER_MAX_COUNT; i++)
124   {
125     towers[i] = (Tower){0};
126   }
127   towerCount = 0;
128 
129   towerModels[TOWER_TYPE_BASE] = LoadModel("data/keep.glb");
130   towerModels[TOWER_TYPE_WALL] = LoadModel("data/wall-0000.glb");
131 
132   for (int i = 0; i < TOWER_TYPE_COUNT; i++)
133   {
134     if (towerModels[i].materials)
135     {
136       // assign the palette texture to the material of the model (0 is not used afaik)
137       towerModels[i].materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
138     }
139   }
140 }
141 
142 static void TowerGunUpdate(Tower *tower)
143 {
144   TowerTypeConfig config = towerTypeConfigs[tower->towerType];
145   if (tower->cooldown <= 0.0f)
146   {
147     Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, config.range);
148     if (enemy)
149     {
150       tower->cooldown = config.cooldown;
151       // shoot the enemy; determine future position of the enemy
152       float bulletSpeed = config.projectileSpeed;
153       Vector2 velocity = enemy->simVelocity;
154       Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0);
155       Vector2 towerPosition = {tower->x, tower->y};
156       float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed;
157       for (int i = 0; i < 8; i++) {
158         velocity = enemy->simVelocity;
159         futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0);
160         float distance = Vector2Distance(towerPosition, futurePosition);
161         float eta2 = distance / bulletSpeed;
162         if (fabs(eta - eta2) < 0.01f) {
163           break;
164         }
165         eta = (eta2 + eta) * 0.5f;
166       }
167 
168       ProjectileTryAdd(config.projectileType, enemy, 
169         (Vector3){towerPosition.x, 1.33f, towerPosition.y}, 
170         (Vector3){futurePosition.x, 0.25f, futurePosition.y},
171         bulletSpeed, config.hitEffect);
172       enemy->futureDamage += config.hitEffect.damage;
173       tower->lastTargetPosition = futurePosition;
174     }
175   }
176   else
177   {
178     tower->cooldown -= gameTime.deltaTime;
179   }
180 }
181 
182 Tower *TowerGetAt(int16_t x, int16_t y)
183 {
184   for (int i = 0; i < towerCount; i++)
185   {
186     if (towers[i].x == x && towers[i].y == y && towers[i].towerType != TOWER_TYPE_NONE)
187     {
188       return &towers[i];
189     }
190   }
191   return 0;
192 }
193 
194 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y)
195 {
196   if (towerCount >= TOWER_MAX_COUNT)
197   {
198     return 0;
199   }
200 
201   Tower *tower = TowerGetAt(x, y);
202   if (tower)
203   {
204     return 0;
205   }
206 
207   tower = &towers[towerCount++];
208   tower->x = x;
209   tower->y = y;
210   tower->towerType = towerType;
211   tower->cooldown = 0.0f;
212   tower->damage = 0.0f;
213   return tower;
214 }
215 
216 Tower *GetTowerByType(uint8_t towerType)
217 {
218   for (int i = 0; i < towerCount; i++)
219   {
220     if (towers[i].towerType == towerType)
221     {
222       return &towers[i];
223     }
224   }
225   return 0;
226 }
227 
228 int GetTowerCosts(uint8_t towerType)
229 {
230   return towerTypeConfigs[towerType].cost;
231 }
232 
233 float TowerGetMaxHealth(Tower *tower)
234 {
235   return towerTypeConfigs[tower->towerType].maxHealth;
236 }
237 
238 void TowerDraw()
239 {
240   for (int i = 0; i < towerCount; i++)
241   {
242     Tower tower = towers[i];
243     if (tower.towerType == TOWER_TYPE_NONE)
244     {
245       continue;
246     }
247 
248     switch (tower.towerType)
249     {
250     case TOWER_TYPE_ARCHER:
251       {
252         Vector2 screenPosTower = GetWorldToScreen((Vector3){tower.x, 0.0f, tower.y}, currentLevel->camera);
253         Vector2 screenPosTarget = GetWorldToScreen((Vector3){tower.lastTargetPosition.x, 0.0f, tower.lastTargetPosition.y}, currentLevel->camera);
254         DrawModel(towerModels[TOWER_TYPE_WALL], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
255         DrawSpriteUnit(archerUnit, (Vector3){tower.x, 1.0f, tower.y}, 0, screenPosTarget.x > screenPosTower.x, 
256           tower.cooldown > 0.2f ? SPRITE_UNIT_PHASE_WEAPON_COOLDOWN : SPRITE_UNIT_PHASE_WEAPON_IDLE);
257       }
258       break;
259     case TOWER_TYPE_BALLISTA:
260       DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, BROWN);
261       break;
262     case TOWER_TYPE_CATAPULT:
263       DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, DARKGRAY);
264       break;
265     default:
266       if (towerModels[tower.towerType].materials)
267       {
268         DrawModel(towerModels[tower.towerType], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
269       } else {
270         DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY);
271       }
272       break;
273     }
274   }
275 }
276 
277 void TowerUpdate()
278 {
279   for (int i = 0; i < towerCount; i++)
280   {
281     Tower *tower = &towers[i];
282     switch (tower->towerType)
283     {
284     case TOWER_TYPE_CATAPULT:
285     case TOWER_TYPE_BALLISTA:
286     case TOWER_TYPE_ARCHER:
287       TowerGunUpdate(tower);
288       break;
289     }
290   }
291 }
292 
293 void TowerDrawHealthBars(Camera3D camera)
294 {
295   for (int i = 0; i < towerCount; i++)
296   {
297     Tower *tower = &towers[i];
298     if (tower->towerType == TOWER_TYPE_NONE || tower->damage <= 0.0f)
299     {
300       continue;
301     }
302     
303     Vector3 position = (Vector3){tower->x, 0.5f, tower->y};
304     float maxHealth = TowerGetMaxHealth(tower);
305     float health = maxHealth - tower->damage;
306     float healthRatio = health / maxHealth;
307     
308     DrawHealthBar(camera, position, healthRatio, GREEN, 35.0f);
309   }
310 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 #include <stdlib.h>
  4 #include <math.h>
  5 
  6 EnemyClassConfig enemyClassConfigs[] = {
  7     [ENEMY_TYPE_MINION] = {
  8       .health = 10.0f, 
  9       .speed = 0.6f, 
 10       .radius = 0.25f, 
 11       .maxAcceleration = 1.0f,
 12       .explosionDamage = 1.0f,
 13       .requiredContactTime = 0.5f,
 14       .explosionRange = 1.0f,
 15       .explosionPushbackPower = 0.25f,
 16       .goldValue = 1,
 17     },
 18 };
 19 
 20 Enemy enemies[ENEMY_MAX_COUNT];
 21 int enemyCount = 0;
 22 
 23 SpriteUnit enemySprites[] = {
 24     [ENEMY_TYPE_MINION] = {
 25       .srcRect = {0, 16, 16, 16},
 26       .offset = {8.0f, 0.0f},
 27       .frameCount = 6,
 28       .frameDuration = 0.1f,
 29     },
 30 };
 31 
 32 void EnemyInit()
 33 {
 34   for (int i = 0; i < ENEMY_MAX_COUNT; i++)
 35   {
 36     enemies[i] = (Enemy){0};
 37   }
 38   enemyCount = 0;
 39 }
 40 
 41 float EnemyGetCurrentMaxSpeed(Enemy *enemy)
 42 {
 43   return enemyClassConfigs[enemy->enemyType].speed;
 44 }
 45 
 46 float EnemyGetMaxHealth(Enemy *enemy)
 47 {
 48   return enemyClassConfigs[enemy->enemyType].health;
 49 }
 50 
 51 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY)
 52 {
 53   int16_t castleX = 0;
 54   int16_t castleY = 0;
 55   int16_t dx = castleX - currentX;
 56   int16_t dy = castleY - currentY;
 57   if (dx == 0 && dy == 0)
 58   {
 59     *nextX = currentX;
 60     *nextY = currentY;
 61     return 1;
 62   }
 63   Vector2 gradient = PathFindingGetGradient((Vector3){currentX, 0, currentY});
 64 
 65   if (gradient.x == 0 && gradient.y == 0)
 66   {
 67     *nextX = currentX;
 68     *nextY = currentY;
 69     return 1;
 70   }
 71 
 72   if (fabsf(gradient.x) > fabsf(gradient.y))
 73   {
 74     *nextX = currentX + (int16_t)(gradient.x > 0.0f ? 1 : -1);
 75     *nextY = currentY;
 76     return 0;
 77   }
 78   *nextX = currentX;
 79   *nextY = currentY + (int16_t)(gradient.y > 0.0f ? 1 : -1);
 80   return 0;
 81 }
 82 
 83 
 84 // this function predicts the movement of the unit for the next deltaT seconds
 85 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount)
 86 {
 87   const float pointReachedDistance = 0.25f;
 88   const float pointReachedDistance2 = pointReachedDistance * pointReachedDistance;
 89   const float maxSimStepTime = 0.015625f;
 90   
 91   float maxAcceleration = enemyClassConfigs[enemy->enemyType].maxAcceleration;
 92   float maxSpeed = EnemyGetCurrentMaxSpeed(enemy);
 93   int16_t nextX = enemy->nextX;
 94   int16_t nextY = enemy->nextY;
 95   Vector2 position = enemy->simPosition;
 96   int passedCount = 0;
 97   for (float t = 0.0f; t < deltaT; t += maxSimStepTime)
 98   {
 99     float stepTime = fminf(deltaT - t, maxSimStepTime);
100     Vector2 target = (Vector2){nextX, nextY};
101     float speed = Vector2Length(*velocity);
102     // draw the target position for debugging
103     //DrawCubeWires((Vector3){target.x, 0.2f, target.y}, 0.1f, 0.4f, 0.1f, RED);
104     Vector2 lookForwardPos = Vector2Add(position, Vector2Scale(*velocity, speed));
105     if (Vector2DistanceSqr(target, lookForwardPos) <= pointReachedDistance2)
106     {
107       // we reached the target position, let's move to the next waypoint
108       EnemyGetNextPosition(nextX, nextY, &nextX, &nextY);
109       target = (Vector2){nextX, nextY};
110       // track how many waypoints we passed
111       passedCount++;
112     }
113     
114     // acceleration towards the target
115     Vector2 unitDirection = Vector2Normalize(Vector2Subtract(target, lookForwardPos));
116     Vector2 acceleration = Vector2Scale(unitDirection, maxAcceleration * stepTime);
117     *velocity = Vector2Add(*velocity, acceleration);
118 
119     // limit the speed to the maximum speed
120     if (speed > maxSpeed)
121     {
122       *velocity = Vector2Scale(*velocity, maxSpeed / speed);
123     }
124 
125     // move the enemy
126     position = Vector2Add(position, Vector2Scale(*velocity, stepTime));
127   }
128 
129   if (waypointPassedCount)
130   {
131     (*waypointPassedCount) = passedCount;
132   }
133 
134   return position;
135 }
136 
137 void EnemyDraw()
138 {
139   for (int i = 0; i < enemyCount; i++)
140   {
141     Enemy enemy = enemies[i];
142     if (enemy.enemyType == ENEMY_TYPE_NONE)
143     {
144       continue;
145     }
146 
147     Vector2 position = EnemyGetPosition(&enemy, gameTime.time - enemy.startMovingTime, &enemy.simVelocity, 0);
148     
149     // don't draw any trails for now; might replace this with footprints later
150     // if (enemy.movePathCount > 0)
151     // {
152     //   Vector3 p = {enemy.movePath[0].x, 0.2f, enemy.movePath[0].y};
153     //   DrawLine3D(p, (Vector3){position.x, 0.2f, position.y}, GREEN);
154     // }
155     // for (int j = 1; j < enemy.movePathCount; j++)
156     // {
157     //   Vector3 p = {enemy.movePath[j - 1].x, 0.2f, enemy.movePath[j - 1].y};
158     //   Vector3 q = {enemy.movePath[j].x, 0.2f, enemy.movePath[j].y};
159     //   DrawLine3D(p, q, GREEN);
160     // }
161 
162     switch (enemy.enemyType)
163     {
164     case ENEMY_TYPE_MINION:
165       DrawSpriteUnit(enemySprites[ENEMY_TYPE_MINION], (Vector3){position.x, 0.0f, position.y}, 
166         enemy.walkedDistance, 0, 0);
167       break;
168     }
169   }
170 }
171 
172 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource)
173 {
174   // damage the tower
175   float explosionDamge = enemyClassConfigs[enemy->enemyType].explosionDamage;
176   float explosionRange = enemyClassConfigs[enemy->enemyType].explosionRange;
177   float explosionPushbackPower = enemyClassConfigs[enemy->enemyType].explosionPushbackPower;
178   float explosionRange2 = explosionRange * explosionRange;
179   tower->damage += enemyClassConfigs[enemy->enemyType].explosionDamage;
180   // explode the enemy
181   if (tower->damage >= TowerGetMaxHealth(tower))
182   {
183     tower->towerType = TOWER_TYPE_NONE;
184   }
185 
186   ParticleAdd(PARTICLE_TYPE_EXPLOSION, 
187     explosionSource, 
188     (Vector3){0, 0.1f, 0}, (Vector3){1.0f, 1.0f, 1.0f}, 1.0f);
189 
190   enemy->enemyType = ENEMY_TYPE_NONE;
191 
192   // push back enemies & dealing damage
193   for (int i = 0; i < enemyCount; i++)
194   {
195     Enemy *other = &enemies[i];
196     if (other->enemyType == ENEMY_TYPE_NONE)
197     {
198       continue;
199     }
200     float distanceSqr = Vector2DistanceSqr(enemy->simPosition, other->simPosition);
201     if (distanceSqr > 0 && distanceSqr < explosionRange2)
202     {
203       Vector2 direction = Vector2Normalize(Vector2Subtract(other->simPosition, enemy->simPosition));
204       other->simPosition = Vector2Add(other->simPosition, Vector2Scale(direction, explosionPushbackPower));
205       EnemyAddDamage(other, explosionDamge);
206     }
207   }
208 }
209 
210 void EnemyUpdate()
211 {
212   const float castleX = 0;
213   const float castleY = 0;
214   const float maxPathDistance2 = 0.25f * 0.25f;
215   
216   for (int i = 0; i < enemyCount; i++)
217   {
218     Enemy *enemy = &enemies[i];
219     if (enemy->enemyType == ENEMY_TYPE_NONE)
220     {
221       continue;
222     }
223 
224     int waypointPassedCount = 0;
225     Vector2 prevPosition = enemy->simPosition;
226     enemy->simPosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &enemy->simVelocity, &waypointPassedCount);
227     enemy->startMovingTime = gameTime.time;
228     enemy->walkedDistance += Vector2Distance(prevPosition, enemy->simPosition);
229     // track path of unit
230     if (enemy->movePathCount == 0 || Vector2DistanceSqr(enemy->simPosition, enemy->movePath[0]) > maxPathDistance2)
231     {
232       for (int j = ENEMY_MAX_PATH_COUNT - 1; j > 0; j--)
233       {
234         enemy->movePath[j] = enemy->movePath[j - 1];
235       }
236       enemy->movePath[0] = enemy->simPosition;
237       if (++enemy->movePathCount > ENEMY_MAX_PATH_COUNT)
238       {
239         enemy->movePathCount = ENEMY_MAX_PATH_COUNT;
240       }
241     }
242 
243     if (waypointPassedCount > 0)
244     {
245       enemy->currentX = enemy->nextX;
246       enemy->currentY = enemy->nextY;
247       if (EnemyGetNextPosition(enemy->currentX, enemy->currentY, &enemy->nextX, &enemy->nextY) &&
248         Vector2DistanceSqr(enemy->simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
249       {
250         // enemy reached the castle; remove it
251         enemy->enemyType = ENEMY_TYPE_NONE;
252         continue;
253       }
254     }
255   }
256 
257   // handle collisions between enemies
258   for (int i = 0; i < enemyCount - 1; i++)
259   {
260     Enemy *enemyA = &enemies[i];
261     if (enemyA->enemyType == ENEMY_TYPE_NONE)
262     {
263       continue;
264     }
265     for (int j = i + 1; j < enemyCount; j++)
266     {
267       Enemy *enemyB = &enemies[j];
268       if (enemyB->enemyType == ENEMY_TYPE_NONE)
269       {
270         continue;
271       }
272       float distanceSqr = Vector2DistanceSqr(enemyA->simPosition, enemyB->simPosition);
273       float radiusA = enemyClassConfigs[enemyA->enemyType].radius;
274       float radiusB = enemyClassConfigs[enemyB->enemyType].radius;
275       float radiusSum = radiusA + radiusB;
276       if (distanceSqr < radiusSum * radiusSum && distanceSqr > 0.001f)
277       {
278         // collision
279         float distance = sqrtf(distanceSqr);
280         float overlap = radiusSum - distance;
281         // move the enemies apart, but softly; if we have a clog of enemies,
282         // moving them perfectly apart can cause them to jitter
283         float positionCorrection = overlap / 5.0f;
284         Vector2 direction = (Vector2){
285             (enemyB->simPosition.x - enemyA->simPosition.x) / distance * positionCorrection,
286             (enemyB->simPosition.y - enemyA->simPosition.y) / distance * positionCorrection};
287         enemyA->simPosition = Vector2Subtract(enemyA->simPosition, direction);
288         enemyB->simPosition = Vector2Add(enemyB->simPosition, direction);
289       }
290     }
291   }
292 
293   // handle collisions between enemies and towers
294   for (int i = 0; i < enemyCount; i++)
295   {
296     Enemy *enemy = &enemies[i];
297     if (enemy->enemyType == ENEMY_TYPE_NONE)
298     {
299       continue;
300     }
301     enemy->contactTime -= gameTime.deltaTime;
302     if (enemy->contactTime < 0.0f)
303     {
304       enemy->contactTime = 0.0f;
305     }
306 
307     float enemyRadius = enemyClassConfigs[enemy->enemyType].radius;
308     // linear search over towers; could be optimized by using path finding tower map,
309     // but for now, we keep it simple
310     for (int j = 0; j < towerCount; j++)
311     {
312       Tower *tower = &towers[j];
313       if (tower->towerType == TOWER_TYPE_NONE)
314       {
315         continue;
316       }
317       float distanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){tower->x, tower->y});
318       float combinedRadius = enemyRadius + 0.708; // sqrt(0.5^2 + 0.5^2), corner-center distance of square with side length 1
319       if (distanceSqr > combinedRadius * combinedRadius)
320       {
321         continue;
322       }
323       // potential collision; square / circle intersection
324       float dx = tower->x - enemy->simPosition.x;
325       float dy = tower->y - enemy->simPosition.y;
326       float absDx = fabsf(dx);
327       float absDy = fabsf(dy);
328       Vector3 contactPoint = {0};
329       if (absDx <= 0.5f && absDx <= absDy) {
330         // vertical collision; push the enemy out horizontally
331         float overlap = enemyRadius + 0.5f - absDy;
332         if (overlap < 0.0f)
333         {
334           continue;
335         }
336         float direction = dy > 0.0f ? -1.0f : 1.0f;
337         enemy->simPosition.y += direction * overlap;
338         contactPoint = (Vector3){enemy->simPosition.x, 0.2f, tower->y + direction * 0.5f};
339       }
340       else if (absDy <= 0.5f && absDy <= absDx)
341       {
342         // horizontal collision; push the enemy out vertically
343         float overlap = enemyRadius + 0.5f - absDx;
344         if (overlap < 0.0f)
345         {
346           continue;
347         }
348         float direction = dx > 0.0f ? -1.0f : 1.0f;
349         enemy->simPosition.x += direction * overlap;
350         contactPoint = (Vector3){tower->x + direction * 0.5f, 0.2f, enemy->simPosition.y};
351       }
352       else
353       {
354         // possible collision with a corner
355         float cornerDX = dx > 0.0f ? -0.5f : 0.5f;
356         float cornerDY = dy > 0.0f ? -0.5f : 0.5f;
357         float cornerX = tower->x + cornerDX;
358         float cornerY = tower->y + cornerDY;
359         float cornerDistanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){cornerX, cornerY});
360         if (cornerDistanceSqr > enemyRadius * enemyRadius)
361         {
362           continue;
363         }
364         // push the enemy out along the diagonal
365         float cornerDistance = sqrtf(cornerDistanceSqr);
366         float overlap = enemyRadius - cornerDistance;
367         float directionX = cornerDistance > 0.0f ? (cornerX - enemy->simPosition.x) / cornerDistance : -cornerDX;
368         float directionY = cornerDistance > 0.0f ? (cornerY - enemy->simPosition.y) / cornerDistance : -cornerDY;
369         enemy->simPosition.x -= directionX * overlap;
370         enemy->simPosition.y -= directionY * overlap;
371         contactPoint = (Vector3){cornerX, 0.2f, cornerY};
372       }
373 
374       if (enemyClassConfigs[enemy->enemyType].explosionDamage > 0.0f)
375       {
376         enemy->contactTime += gameTime.deltaTime * 2.0f; // * 2 to undo the subtraction above
377         if (enemy->contactTime >= enemyClassConfigs[enemy->enemyType].requiredContactTime)
378         {
379           EnemyTriggerExplode(enemy, tower, contactPoint);
380         }
381       }
382     }
383   }
384 }
385 
386 EnemyId EnemyGetId(Enemy *enemy)
387 {
388   return (EnemyId){enemy - enemies, enemy->generation};
389 }
390 
391 Enemy *EnemyTryResolve(EnemyId enemyId)
392 {
393   if (enemyId.index >= ENEMY_MAX_COUNT)
394   {
395     return 0;
396   }
397   Enemy *enemy = &enemies[enemyId.index];
398   if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE)
399   {
400     return 0;
401   }
402   return enemy;
403 }
404 
405 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY)
406 {
407   Enemy *spawn = 0;
408   for (int i = 0; i < enemyCount; i++)
409   {
410     Enemy *enemy = &enemies[i];
411     if (enemy->enemyType == ENEMY_TYPE_NONE)
412     {
413       spawn = enemy;
414       break;
415     }
416   }
417 
418   if (enemyCount < ENEMY_MAX_COUNT && !spawn)
419   {
420     spawn = &enemies[enemyCount++];
421   }
422 
423   if (spawn)
424   {
425     spawn->currentX = currentX;
426     spawn->currentY = currentY;
427     spawn->nextX = currentX;
428     spawn->nextY = currentY;
429     spawn->simPosition = (Vector2){currentX, currentY};
430     spawn->simVelocity = (Vector2){0, 0};
431     spawn->enemyType = enemyType;
432     spawn->startMovingTime = gameTime.time;
433     spawn->damage = 0.0f;
434     spawn->futureDamage = 0.0f;
435     spawn->generation++;
436     spawn->movePathCount = 0;
437     spawn->walkedDistance = 0.0f;
438   }
439 
440   return spawn;
441 }
442 
443 int EnemyAddDamageRange(Vector2 position, float range, float damage)
444 {
445   int count = 0;
446   float range2 = range * range;
447   for (int i = 0; i < enemyCount; i++)
448   {
449     Enemy *enemy = &enemies[i];
450     if (enemy->enemyType == ENEMY_TYPE_NONE)
451     {
452       continue;
453     }
454     float distance2 = Vector2DistanceSqr(position, enemy->simPosition);
455     if (distance2 <= range2)
456     {
457       EnemyAddDamage(enemy, damage);
458       count++;
459     }
460   }
461   return count;
462 }
463 
464 int EnemyAddDamage(Enemy *enemy, float damage)
465 {
466   enemy->damage += damage;
467   if (enemy->damage >= EnemyGetMaxHealth(enemy))
468   {
469     currentLevel->playerGold += enemyClassConfigs[enemy->enemyType].goldValue;
470     enemy->enemyType = ENEMY_TYPE_NONE;
471     return 1;
472   }
473 
474   return 0;
475 }
476 
477 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
478 {
479   int16_t castleX = 0;
480   int16_t castleY = 0;
481   Enemy* closest = 0;
482   int16_t closestDistance = 0;
483   float range2 = range * range;
484   for (int i = 0; i < enemyCount; i++)
485   {
486     Enemy* enemy = &enemies[i];
487     if (enemy->enemyType == ENEMY_TYPE_NONE)
488     {
489       continue;
490     }
491     float maxHealth = EnemyGetMaxHealth(enemy);
492     if (enemy->futureDamage >= maxHealth)
493     {
494       // ignore enemies that will die soon
495       continue;
496     }
497     int16_t dx = castleX - enemy->currentX;
498     int16_t dy = castleY - enemy->currentY;
499     int16_t distance = abs(dx) + abs(dy);
500     if (!closest || distance < closestDistance)
501     {
502       float tdx = towerX - enemy->currentX;
503       float tdy = towerY - enemy->currentY;
504       float tdistance2 = tdx * tdx + tdy * tdy;
505       if (tdistance2 <= range2)
506       {
507         closest = enemy;
508         closestDistance = distance;
509       }
510     }
511   }
512   return closest;
513 }
514 
515 int EnemyCount()
516 {
517   int count = 0;
518   for (int i = 0; i < enemyCount; i++)
519   {
520     if (enemies[i].enemyType != ENEMY_TYPE_NONE)
521     {
522       count++;
523     }
524   }
525   return count;
526 }
527 
528 void EnemyDrawHealthbars(Camera3D camera)
529 {
530   for (int i = 0; i < enemyCount; i++)
531   {
532     Enemy *enemy = &enemies[i];
533     if (enemy->enemyType == ENEMY_TYPE_NONE || enemy->damage == 0.0f)
534     {
535       continue;
536     }
537     Vector3 position = (Vector3){enemy->simPosition.x, 0.5f, enemy->simPosition.y};
538     float maxHealth = EnemyGetMaxHealth(enemy);
539     float health = maxHealth - enemy->damage;
540     float healthRatio = health / maxHealth;
541     
542     DrawHealthBar(camera, position, healthRatio, GREEN, 15.0f);
543   }
544 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 
  4 static Projectile projectiles[PROJECTILE_MAX_COUNT];
  5 static int projectileCount = 0;
  6 
  7 typedef struct ProjectileConfig
  8 {
  9   float arcFactor;
 10   Color color;
 11   Color trailColor;
 12 } ProjectileConfig;
 13 
 14 ProjectileConfig projectileConfigs[] = {
 15     [PROJECTILE_TYPE_ARROW] = {
 16         .arcFactor = 0.15f,
 17         .color = RED,
 18         .trailColor = BROWN,
 19     },
 20     [PROJECTILE_TYPE_CATAPULT] = {
 21         .arcFactor = 0.5f,
 22         .color = RED,
 23         .trailColor = GRAY,
 24     },
 25     [PROJECTILE_TYPE_BALLISTA] = {
 26         .arcFactor = 0.025f,
 27         .color = RED,
 28         .trailColor = BROWN,
 29     },
 30 };
 31 
 32 void ProjectileInit()
 33 {
 34   for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
 35   {
 36     projectiles[i] = (Projectile){0};
 37   }
 38 }
 39 
 40 void ProjectileDraw()
 41 {
 42   for (int i = 0; i < projectileCount; i++)
 43   {
 44     Projectile projectile = projectiles[i];
 45     if (projectile.projectileType == PROJECTILE_TYPE_NONE)
 46     {
 47       continue;
 48     }
 49     float transition = (gameTime.time - projectile.shootTime) / (projectile.arrivalTime - projectile.shootTime);
 50     if (transition >= 1.0f)
 51     {
 52       continue;
 53     }
 54 
 55     ProjectileConfig config = projectileConfigs[projectile.projectileType];
 56     for (float transitionOffset = 0.0f; transitionOffset < 1.0f; transitionOffset += 0.1f)
 57     {
 58       float t = transition + transitionOffset * 0.3f;
 59       if (t > 1.0f)
 60       {
 61         break;
 62       }
 63       Vector3 position = Vector3Lerp(projectile.position, projectile.target, t);
 64       Color color = config.color;
 65       color = ColorLerp(config.trailColor, config.color, transitionOffset * transitionOffset);
 66       // fake a ballista flight path using parabola equation
 67       float parabolaT = t - 0.5f;
 68       parabolaT = 1.0f - 4.0f * parabolaT * parabolaT;
 69       position.y += config.arcFactor * parabolaT * projectile.distance;
 70       
 71       float size = 0.06f * (transitionOffset + 0.25f);
 72       DrawCube(position, size, size, size, color);
 73     }
 74   }
 75 }
 76 
 77 void ProjectileUpdate()
 78 {
 79   for (int i = 0; i < projectileCount; i++)
 80   {
 81     Projectile *projectile = &projectiles[i];
 82     if (projectile->projectileType == PROJECTILE_TYPE_NONE)
 83     {
 84       continue;
 85     }
 86     float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime);
 87     if (transition >= 1.0f)
 88     {
 89       projectile->projectileType = PROJECTILE_TYPE_NONE;
 90       Enemy *enemy = EnemyTryResolve(projectile->targetEnemy);
 91       if (enemy && projectile->hitEffectConfig.pushbackPowerDistance > 0.0f)
 92       {
 93           Vector2 direction = Vector2Normalize(Vector2Subtract((Vector2){projectile->target.x, projectile->target.z}, enemy->simPosition));
 94           enemy->simPosition = Vector2Add(enemy->simPosition, Vector2Scale(direction, projectile->hitEffectConfig.pushbackPowerDistance));
 95       }
 96       
 97       if (projectile->hitEffectConfig.areaDamageRadius > 0.0f)
 98       {
 99         EnemyAddDamageRange((Vector2){projectile->target.x, projectile->target.z}, projectile->hitEffectConfig.areaDamageRadius, projectile->hitEffectConfig.damage);
100         // pancaked sphere explosion
101         float r = projectile->hitEffectConfig.areaDamageRadius;
102         ParticleAdd(PARTICLE_TYPE_EXPLOSION, projectile->target, (Vector3){0}, (Vector3){r, r * 0.2f, r}, 0.33f);
103       }
104       else if (projectile->hitEffectConfig.damage > 0.0f && enemy)
105       {
106         EnemyAddDamage(enemy, projectile->hitEffectConfig.damage);
107       }
108       continue;
109     }
110   }
111 }
112 
113 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig)
114 {
115   for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
116   {
117     Projectile *projectile = &projectiles[i];
118     if (projectile->projectileType == PROJECTILE_TYPE_NONE)
119     {
120       projectile->projectileType = projectileType;
121       projectile->shootTime = gameTime.time;
122       float distance = Vector3Distance(position, target);
123       projectile->arrivalTime = gameTime.time + distance / speed;
124       projectile->position = position;
125       projectile->target = target;
126       projectile->directionNormal = Vector3Scale(Vector3Subtract(target, position), 1.0f / distance);
127       projectile->distance = distance;
128       projectile->targetEnemy = EnemyGetId(enemy);
129       projectileCount = projectileCount <= i ? i + 1 : projectileCount;
130       projectile->hitEffectConfig = hitEffectConfig;
131       return projectile;
132     }
133   }
134   return 0;
135 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 #include <rlgl.h>
  4 
  5 static Particle particles[PARTICLE_MAX_COUNT];
  6 static int particleCount = 0;
  7 
  8 void ParticleInit()
  9 {
 10   for (int i = 0; i < PARTICLE_MAX_COUNT; i++)
 11   {
 12     particles[i] = (Particle){0};
 13   }
 14   particleCount = 0;
 15 }
 16 
 17 static void DrawExplosionParticle(Particle *particle, float transition)
 18 {
 19   Vector3 scale = particle->scale;
 20   float size = 1.0f * (1.0f - transition);
 21   Color startColor = WHITE;
 22   Color endColor = RED;
 23   Color color = ColorLerp(startColor, endColor, transition);
 24 
 25   rlPushMatrix();
 26   rlTranslatef(particle->position.x, particle->position.y, particle->position.z);
 27   rlScalef(scale.x, scale.y, scale.z);
 28   DrawSphere(Vector3Zero(), size, color);
 29   rlPopMatrix();
 30 }
 31 
 32 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime)
 33 {
 34   if (particleCount >= PARTICLE_MAX_COUNT)
 35   {
 36     return;
 37   }
 38 
 39   int index = -1;
 40   for (int i = 0; i < particleCount; i++)
 41   {
 42     if (particles[i].particleType == PARTICLE_TYPE_NONE)
 43     {
 44       index = i;
 45       break;
 46     }
 47   }
 48 
 49   if (index == -1)
 50   {
 51     index = particleCount++;
 52   }
 53 
 54   Particle *particle = &particles[index];
 55   particle->particleType = particleType;
 56   particle->spawnTime = gameTime.time;
 57   particle->lifetime = lifetime;
 58   particle->position = position;
 59   particle->velocity = velocity;
 60   particle->scale = scale;
 61 }
 62 
 63 void ParticleUpdate()
 64 {
 65   for (int i = 0; i < particleCount; i++)
 66   {
 67     Particle *particle = &particles[i];
 68     if (particle->particleType == PARTICLE_TYPE_NONE)
 69     {
 70       continue;
 71     }
 72 
 73     float age = gameTime.time - particle->spawnTime;
 74 
 75     if (particle->lifetime > age)
 76     {
 77       particle->position = Vector3Add(particle->position, Vector3Scale(particle->velocity, gameTime.deltaTime));
 78     }
 79     else {
 80       particle->particleType = PARTICLE_TYPE_NONE;
 81     }
 82   }
 83 }
 84 
 85 void ParticleDraw()
 86 {
 87   for (int i = 0; i < particleCount; i++)
 88   {
 89     Particle particle = particles[i];
 90     if (particle.particleType == PARTICLE_TYPE_NONE)
 91     {
 92       continue;
 93     }
 94 
 95     float age = gameTime.time - particle.spawnTime;
 96     float transition = age / particle.lifetime;
 97     switch (particle.particleType)
 98     {
 99     case PARTICLE_TYPE_EXPLOSION:
100       DrawExplosionParticle(&particle, transition);
101       break;
102     default:
103       DrawCube(particle.position, 0.3f, 0.5f, 0.3f, RED);
104       break;
105     }
106   }
107 }
  1 #include "raylib.h"
  2 #include "preferred_size.h"
  3 
  4 // Since the canvas size is not known at compile time, we need to query it at runtime;
  5 // the following platform specific code obtains the canvas size and we will use this
  6 // size as the preferred size for the window at init time. We're ignoring here the
  7 // possibility of the canvas size changing during runtime - this would require to
  8 // poll the canvas size in the game loop or establishing a callback to be notified
  9 
 10 #ifdef PLATFORM_WEB
 11 #include <emscripten.h>
 12 EMSCRIPTEN_RESULT emscripten_get_element_css_size(const char *target, double *width, double *height);
 13 
 14 void GetPreferredSize(int *screenWidth, int *screenHeight)
 15 {
 16   double canvasWidth, canvasHeight;
 17   emscripten_get_element_css_size("#" CANVAS_NAME, &canvasWidth, &canvasHeight);
 18   *screenWidth = (int)canvasWidth;
 19   *screenHeight = (int)canvasHeight;
 20   TraceLog(LOG_INFO, "preferred size for %s: %d %d", CANVAS_NAME, *screenWidth, *screenHeight);
 21 }
 22 
 23 int IsPaused()
 24 {
 25   const char *js = "(function(){\n"
 26   "  var canvas = document.getElementById(\"" CANVAS_NAME "\");\n"
 27   "  var rect = canvas.getBoundingClientRect();\n"
 28   "  var isVisible = (\n"
 29   "    rect.top >= 0 &&\n"
 30   "    rect.left >= 0 &&\n"
 31   "    rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&\n"
 32   "    rect.right <= (window.innerWidth || document.documentElement.clientWidth)\n"
 33   "  );\n"
 34   "  return isVisible ? 0 : 1;\n"
 35   "})()";
 36   return emscripten_run_script_int(js);
 37 }
 38 
 39 #else
 40 void GetPreferredSize(int *screenWidth, int *screenHeight)
 41 {
 42   *screenWidth = 600;
 43   *screenHeight = 240;
 44 }
 45 int IsPaused()
 46 {
 47   return 0;
 48 }
 49 #endif
  1 #ifndef PREFERRED_SIZE_H
  2 #define PREFERRED_SIZE_H
  3 
  4 void GetPreferredSize(int *screenWidth, int *screenHeight);
  5 int IsPaused();
  6 
  7 #endif
Two enemy entry points with path previews merging without Z-fighting with the arrows
Two enemy entry points with path previews merging without Z-fighting
Two enemy entry points with path previews merging without Z-fighting

The result looks good! There's no z-fighting anymore - though ... you have to be aware that this only works when the precision of the depth buffer is high enough. Most modern GPUs use a 24 or 32 bit depth buffer, which is more than enough for this kind of scene. But especially on mobile devices and web browsers, the depth buffer can be as low as 16 bit where this technique may not work well and z-fighting will still be visible. For an effect like this where the z-fights are not very noticeable, it's an acceptable trade-off. And a lot of times, the camera clipping planes can be optimized to avoid this issue: The smaller the near and far clipping planes of the camera are, the more bits of the depth buffer are used during rendering, increasing the overall precision.

Summary

We have now a nice looking path preview that shows the path the enemies will take and the path preview is updated when hovering over a tower and when placing new towers.

Visually and gameplay-wise, this is quite a good improvement, making it easier to anticipate how the enemies will move through the level.

On touch only devices, the building placement is however almost broken and needs to be fixed. This will be the next topic to tackle.

🍪