Simple tower defense tutorial, part 10: Different towers

Originally, I wanted to make first the graphics for the towers and handle the animations. This is however not so trivial in raylib and I need a bit more time for writing a blog post that is explaining this well. I am therefore postponing that step and do something that makes actually more sense as well, which is implementing the game logic first.

There's currently only one type of shooting tower in the game. There are 2 tower types that I want to add: A slow sniper tower (a ballista) and a slow area damage tower (a catapult).

So let's jump right into it and add 2 new tower types to the game!

I found and fixed the reason why touch screen inputs didn't work at all. It is now sort of working, but it isn't a good experience. One of the next parts will be about how to improve this.
  • 💾
  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 DrawLevelBuildingState(Level *level)
287 {
288   BeginMode3D(level->camera);
289   DrawLevelGround(level);
290   TowerDraw();
291   EnemyDraw();
292   ProjectileDraw();
293   ParticleDraw();
294 
295   Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera);
296   float planeDistance = ray.position.y / -ray.direction.y;
297   float planeX = ray.direction.x * planeDistance + ray.position.x;
298   float planeY = ray.direction.z * planeDistance + ray.position.z;
299   int16_t mapX = (int16_t)floorf(planeX + 0.5f);
300   int16_t mapY = (int16_t)floorf(planeY + 0.5f);
301   if (level->placementMode && !guiState.isBlocked && mapX >= -5 && mapX <= 5 && mapY >= -5 && mapY <= 5)
302   {
303     DrawCubeWires((Vector3){mapX, 0.2f, mapY}, 1.0f, 0.4f, 1.0f, RED);
304     if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
305     {
306       if (TowerTryAdd(level->placementMode, mapX, mapY))
307       {
308         level->playerGold -= GetTowerCosts(level->placementMode);
309         level->placementMode = TOWER_TYPE_NONE;
310       }
311     }
312   }
313 
314   guiState.isBlocked = 0;
315 
316   EndMode3D();
317 
318   TowerDrawHealthBars(level->camera);
319 
320   static ButtonState buildWallButtonState = {0};
321   static ButtonState buildGunButtonState = {0};
322   buildWallButtonState.isSelected = level->placementMode == TOWER_TYPE_WALL;
323   buildGunButtonState.isSelected = level->placementMode == TOWER_TYPE_GUN;
324 
325   DrawBuildingBuildButton(level, 10, 10, 110, 30, TOWER_TYPE_WALL, "Wall");
326   DrawBuildingBuildButton(level, 10, 50, 110, 30, TOWER_TYPE_GUN, "Archer");
327 DrawBuildingBuildButton(level, 10, 90, 110, 30, TOWER_TYPE_BALLISTA, "Ballista"); 328 DrawBuildingBuildButton(level, 10, 130, 110, 30, TOWER_TYPE_CATAPULT, "Catapult");
329 330 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 331 { 332 level->nextState = LEVEL_STATE_RESET; 333 } 334 335 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0)) 336 { 337 level->nextState = LEVEL_STATE_BATTLE; 338 } 339 340 const char *text = "Building phase"; 341 int textWidth = MeasureText(text, 20); 342 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 343 } 344 345 void InitBattleStateConditions(Level *level) 346 { 347 level->state = LEVEL_STATE_BATTLE; 348 level->nextState = LEVEL_STATE_NONE; 349 level->waveEndTimer = 0.0f; 350 for (int i = 0; i < 10; i++) 351 { 352 EnemyWave *wave = &level->waves[i]; 353 wave->spawned = 0; 354 wave->timeToSpawnNext = wave->delay; 355 } 356 } 357 358 void DrawLevelBattleState(Level *level) 359 { 360 BeginMode3D(level->camera); 361 DrawLevelGround(level); 362 TowerDraw(); 363 EnemyDraw(); 364 ProjectileDraw(); 365 ParticleDraw(); 366 guiState.isBlocked = 0; 367 EndMode3D(); 368 369 EnemyDrawHealthbars(level->camera); 370 TowerDrawHealthBars(level->camera); 371 372 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 373 { 374 level->nextState = LEVEL_STATE_RESET; 375 } 376 377 int maxCount = 0; 378 int remainingCount = 0; 379 for (int i = 0; i < 10; i++) 380 { 381 EnemyWave *wave = &level->waves[i]; 382 if (wave->wave != level->currentWave) 383 { 384 continue; 385 } 386 maxCount += wave->count; 387 remainingCount += wave->count - wave->spawned; 388 } 389 int aliveCount = EnemyCount(); 390 remainingCount += aliveCount; 391 392 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount); 393 int textWidth = MeasureText(text, 20); 394 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 395 } 396 397 void DrawLevel(Level *level) 398 { 399 switch (level->state) 400 { 401 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break; 402 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break; 403 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break; 404 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break; 405 default: break; 406 } 407 408 DrawLevelHud(level); 409 } 410 411 void UpdateLevel(Level *level) 412 { 413 if (level->state == LEVEL_STATE_BATTLE) 414 { 415 int activeWaves = 0; 416 for (int i = 0; i < 10; i++) 417 { 418 EnemyWave *wave = &level->waves[i]; 419 if (wave->spawned >= wave->count || wave->wave != level->currentWave) 420 { 421 continue; 422 } 423 activeWaves++; 424 wave->timeToSpawnNext -= gameTime.deltaTime; 425 if (wave->timeToSpawnNext <= 0.0f) 426 { 427 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y); 428 if (enemy) 429 { 430 wave->timeToSpawnNext = wave->interval; 431 wave->spawned++; 432 } 433 } 434 } 435 if (GetTowerByType(TOWER_TYPE_BASE) == 0) { 436 level->waveEndTimer += gameTime.deltaTime; 437 if (level->waveEndTimer >= 2.0f) 438 { 439 level->nextState = LEVEL_STATE_LOST_WAVE; 440 } 441 } 442 else if (activeWaves == 0 && EnemyCount() == 0) 443 { 444 level->waveEndTimer += gameTime.deltaTime; 445 if (level->waveEndTimer >= 2.0f) 446 { 447 level->nextState = LEVEL_STATE_WON_WAVE; 448 } 449 } 450 } 451 452 PathFindingMapUpdate(); 453 EnemyUpdate(); 454 TowerUpdate(); 455 ProjectileUpdate(); 456 ParticleUpdate(); 457 458 if (level->nextState == LEVEL_STATE_RESET) 459 { 460 InitLevel(level); 461 } 462 463 if (level->nextState == LEVEL_STATE_BATTLE) 464 { 465 InitBattleStateConditions(level); 466 } 467 468 if (level->nextState == LEVEL_STATE_WON_WAVE) 469 { 470 level->currentWave++; 471 level->state = LEVEL_STATE_WON_WAVE; 472 } 473 474 if (level->nextState == LEVEL_STATE_LOST_WAVE) 475 { 476 level->state = LEVEL_STATE_LOST_WAVE; 477 } 478 479 if (level->nextState == LEVEL_STATE_BUILDING) 480 { 481 level->state = LEVEL_STATE_BUILDING; 482 } 483 484 if (level->nextState == LEVEL_STATE_WON_LEVEL) 485 { 486 // make something of this later 487 InitLevel(level); 488 } 489 490 level->nextState = LEVEL_STATE_NONE; 491 } 492 493 float nextSpawnTime = 0.0f; 494 495 void ResetGame() 496 { 497 InitLevel(currentLevel); 498 } 499 500 void InitGame() 501 { 502 TowerInit(); 503 EnemyInit(); 504 ProjectileInit(); 505 ParticleInit(); 506 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f); 507 508 currentLevel = levels; 509 InitLevel(currentLevel); 510 } 511 512 //# Immediate GUI functions 513 514 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth) 515 { 516 const float healthBarHeight = 6.0f; 517 const float healthBarOffset = 15.0f; 518 const float inset = 2.0f; 519 const float innerWidth = healthBarWidth - inset * 2; 520 const float innerHeight = healthBarHeight - inset * 2; 521 522 Vector2 screenPos = GetWorldToScreen(position, camera); 523 float centerX = screenPos.x - healthBarWidth * 0.5f; 524 float topY = screenPos.y - healthBarOffset; 525 DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK); 526 float healthWidth = innerWidth * healthRatio; 527 DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor); 528 } 529 530 int Button(const char *text, int x, int y, int width, int height, ButtonState *state) 531 { 532 Rectangle bounds = {x, y, width, height}; 533 int isPressed = 0; 534 int isSelected = state && state->isSelected; 535 int isDisabled = state && state->isDisabled; 536 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !isDisabled) 537 { 538 Color color = isSelected ? DARKGRAY : GRAY; 539 DrawRectangle(x, y, width, height, color); 540 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) 541 { 542 isPressed = 1; 543 } 544 guiState.isBlocked = 1; 545 } 546 else 547 { 548 Color color = isSelected ? WHITE : LIGHTGRAY; 549 DrawRectangle(x, y, width, height, color); 550 } 551 Font font = GetFontDefault(); 552 Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1); 553 Color textColor = isDisabled ? GRAY : BLACK; 554 DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, textColor); 555 return isPressed; 556 } 557 558 //# Main game loop 559 560 void GameUpdate() 561 { 562 float dt = GetFrameTime(); 563 // cap maximum delta time to 0.1 seconds to prevent large time steps 564 if (dt > 0.1f) dt = 0.1f; 565 gameTime.time += dt; 566 gameTime.deltaTime = dt; 567 568 UpdateLevel(currentLevel); 569 } 570 571 int main(void) 572 { 573 int screenWidth, screenHeight; 574 GetPreferredSize(&screenWidth, &screenHeight); 575 InitWindow(screenWidth, screenHeight, "Tower defense"); 576 SetTargetFPS(30); 577 578 LoadAssets(); 579 InitGame(); 580 581 while (!WindowShouldClose()) 582 { 583 if (IsPaused()) { 584 // canvas is not visible in browser - do nothing 585 continue; 586 } 587 588 BeginDrawing(); 589 ClearBackground((Color){0x4E, 0x63, 0x26, 0xFF}); 590 591 GameUpdate(); 592 DrawLevel(currentLevel); 593 594 EndDrawing(); 595 } 596 597 CloseWindow(); 598 599 return 0; 600 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 
  4 Tower towers[TOWER_MAX_COUNT];
  5 int towerCount = 0;
  6 
  7 Model towerModels[TOWER_TYPE_COUNT];
  8 
  9 // definition of our archer unit
 10 SpriteUnit archerUnit = {
 11     .srcRect = {0, 0, 16, 16},
 12     .offset = {7, 1},
 13     .frameCount = 1,
 14     .frameDuration = 0.0f,
 15     .srcWeaponIdleRect = {16, 0, 6, 16},
 16     .srcWeaponIdleOffset = {8, 0},
 17     .srcWeaponCooldownRect = {22, 0, 11, 16},
 18     .srcWeaponCooldownOffset = {10, 0},
 19 };
 20 
 21 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase)
 22 {
 23   float xScale = flip ? -1.0f : 1.0f;
 24   Camera3D camera = currentLevel->camera;
 25   float size = 0.5f;
 26   Vector2 offset = (Vector2){ unit.offset.x / 16.0f * size, unit.offset.y / 16.0f * size * xScale };
 27   Vector2 scale = (Vector2){ unit.srcRect.width / 16.0f * size, unit.srcRect.height / 16.0f * size };
 28   // we want the sprite to face the camera, so we need to calculate the up vector
 29   Vector3 forward = Vector3Subtract(camera.target, camera.position);
 30   Vector3 up = {0, 1, 0};
 31   Vector3 right = Vector3CrossProduct(forward, up);
 32   up = Vector3Normalize(Vector3CrossProduct(right, forward));
 33 
 34   Rectangle srcRect = unit.srcRect;
 35   if (unit.frameCount > 1)
 36   {
 37     srcRect.x += (int)(t / unit.frameDuration) % unit.frameCount * srcRect.width;
 38   }
 39   if (flip)
 40   {
 41     srcRect.x += srcRect.width;
 42     srcRect.width = -srcRect.width;
 43   }
 44   DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
 45 
 46   if (phase == SPRITE_UNIT_PHASE_WEAPON_COOLDOWN && unit.srcWeaponCooldownRect.width > 0)
 47   {
 48     offset = (Vector2){ unit.srcWeaponCooldownOffset.x / 16.0f * size, unit.srcWeaponCooldownOffset.y / 16.0f * size };
 49     scale = (Vector2){ unit.srcWeaponCooldownRect.width / 16.0f * size, unit.srcWeaponCooldownRect.height / 16.0f * size };
 50     srcRect = unit.srcWeaponCooldownRect;
 51     if (flip)
 52     {
 53       // position.x = flip * scale.x * 0.5f;
 54       srcRect.x += srcRect.width;
 55       srcRect.width = -srcRect.width;
 56       offset.x = scale.x - offset.x;
 57     }
 58     DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
 59   }
 60   else if (phase == SPRITE_UNIT_PHASE_WEAPON_IDLE && unit.srcWeaponIdleRect.width > 0)
 61   {
 62     offset = (Vector2){ unit.srcWeaponIdleOffset.x / 16.0f * size, unit.srcWeaponIdleOffset.y / 16.0f * size };
 63     scale = (Vector2){ unit.srcWeaponIdleRect.width / 16.0f * size, unit.srcWeaponIdleRect.height / 16.0f * size };
 64     srcRect = unit.srcWeaponIdleRect;
 65     if (flip)
 66     {
 67       // position.x = flip * scale.x * 0.5f;
 68       srcRect.x += srcRect.width;
 69       srcRect.width = -srcRect.width;
 70       offset.x = scale.x - offset.x;
 71     }
 72     DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
 73   }
 74 }
 75 
 76 void TowerInit()
 77 {
 78   for (int i = 0; i < TOWER_MAX_COUNT; i++)
 79   {
 80     towers[i] = (Tower){0};
 81   }
 82   towerCount = 0;
 83 
 84   towerModels[TOWER_TYPE_BASE] = LoadModel("data/keep.glb");
 85   towerModels[TOWER_TYPE_WALL] = LoadModel("data/wall-0000.glb");
 86 
 87   for (int i = 0; i < TOWER_TYPE_COUNT; i++)
 88   {
 89     if (towerModels[i].materials)
 90     {
 91       // assign the palette texture to the material of the model (0 is not used afaik)
 92       towerModels[i].materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
 93     }
 94   }
 95 }
 96 
 97 static void TowerGunUpdate(Tower *tower)
 98 {
 99   if (tower->cooldown <= 0)
100   {
101     Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, 3.0f);
102     if (enemy)
103     {
104       tower->cooldown = 0.5f;
105       // shoot the enemy; determine future position of the enemy
106       float bulletSpeed = 4.0f;
107       float bulletDamage = 3.0f;
108       Vector2 velocity = enemy->simVelocity;
109       Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0);
110       Vector2 towerPosition = {tower->x, tower->y};
111       float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed;
112       for (int i = 0; i < 8; i++) {
113         velocity = enemy->simVelocity;
114         futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0);
115         float distance = Vector2Distance(towerPosition, futurePosition);
116         float eta2 = distance / bulletSpeed;
117         if (fabs(eta - eta2) < 0.01f) {
118           break;
119         }
120         eta = (eta2 + eta) * 0.5f;
121       }
122       ProjectileTryAdd(PROJECTILE_TYPE_ARROW, enemy, 
123         (Vector3){towerPosition.x, 1.33f, towerPosition.y}, 
124         (Vector3){futurePosition.x, 0.25f, futurePosition.y},
125         bulletSpeed, bulletDamage);
126       enemy->futureDamage += bulletDamage;
127       tower->lastTargetPosition = futurePosition;
128     }
129   }
130   else
131   {
132     tower->cooldown -= gameTime.deltaTime;
133   }
134 }
135 
136 Tower *TowerGetAt(int16_t x, int16_t y)
137 {
138   for (int i = 0; i < towerCount; i++)
139   {
140     if (towers[i].x == x && towers[i].y == y && towers[i].towerType != TOWER_TYPE_NONE)
141     {
142       return &towers[i];
143     }
144   }
145   return 0;
146 }
147 
148 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y)
149 {
150   if (towerCount >= TOWER_MAX_COUNT)
151   {
152     return 0;
153   }
154 
155   Tower *tower = TowerGetAt(x, y);
156   if (tower)
157   {
158     return 0;
159   }
160 
161   tower = &towers[towerCount++];
162   tower->x = x;
163   tower->y = y;
164   tower->towerType = towerType;
165   tower->cooldown = 0.0f;
166   tower->damage = 0.0f;
167   return tower;
168 }
169 
170 Tower *GetTowerByType(uint8_t towerType)
171 {
172   for (int i = 0; i < towerCount; i++)
173   {
174     if (towers[i].towerType == towerType)
175     {
176       return &towers[i];
177     }
178   }
179   return 0;
180 }
181 
182 int GetTowerCosts(uint8_t towerType)
183 {
184   switch (towerType)
185   {
186   case TOWER_TYPE_BASE:
187     return 0;
188   case TOWER_TYPE_GUN:
189     return 6;
190   case TOWER_TYPE_WALL:
191     return 2;
192 case TOWER_TYPE_BALLISTA: 193 return 9; 194 case TOWER_TYPE_CATAPULT: 195 return 9;
196 } 197 return 0; 198 } 199 200 float TowerGetMaxHealth(Tower *tower) 201 { 202 switch (tower->towerType) 203 { 204 case TOWER_TYPE_BASE: 205 return 10.0f; 206 case TOWER_TYPE_GUN: 207 return 3.0f;
208 case TOWER_TYPE_WALL: 209 return 5.0f; 210 case TOWER_TYPE_BALLISTA: 211 return 5.0f; 212 case TOWER_TYPE_CATAPULT:
213 return 5.0f; 214 } 215 return 0.0f; 216 } 217 218 void TowerDraw() 219 { 220 for (int i = 0; i < towerCount; i++) 221 { 222 Tower tower = towers[i]; 223 if (tower.towerType == TOWER_TYPE_NONE) 224 { 225 continue; 226 } 227 228 switch (tower.towerType) 229 { 230 case TOWER_TYPE_GUN: 231 { 232 Vector2 screenPosTower = GetWorldToScreen((Vector3){tower.x, 0.0f, tower.y}, currentLevel->camera); 233 Vector2 screenPosTarget = GetWorldToScreen((Vector3){tower.lastTargetPosition.x, 0.0f, tower.lastTargetPosition.y}, currentLevel->camera); 234 DrawModel(towerModels[TOWER_TYPE_WALL], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE); 235 DrawSpriteUnit(archerUnit, (Vector3){tower.x, 1.0f, tower.y}, 0, screenPosTarget.x > screenPosTower.x, 236 tower.cooldown > 0.2f ? SPRITE_UNIT_PHASE_WEAPON_COOLDOWN : SPRITE_UNIT_PHASE_WEAPON_IDLE);
237 } 238 break; 239 case TOWER_TYPE_BALLISTA: 240 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, BROWN); 241 break; 242 case TOWER_TYPE_CATAPULT: 243 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, DARKGRAY);
244 break; 245 default: 246 if (towerModels[tower.towerType].materials) 247 { 248 DrawModel(towerModels[tower.towerType], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE); 249 } else { 250 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY); 251 } 252 break; 253 } 254 } 255 } 256 257 void TowerUpdate() 258 { 259 for (int i = 0; i < towerCount; i++) 260 { 261 Tower *tower = &towers[i]; 262 switch (tower->towerType) 263 { 264 case TOWER_TYPE_GUN: 265 TowerGunUpdate(tower); 266 break; 267 } 268 } 269 } 270 271 void TowerDrawHealthBars(Camera3D camera) 272 { 273 for (int i = 0; i < towerCount; i++) 274 { 275 Tower *tower = &towers[i]; 276 if (tower->towerType == TOWER_TYPE_NONE || tower->damage <= 0.0f) 277 { 278 continue; 279 } 280 281 Vector3 position = (Vector3){tower->x, 0.5f, tower->y}; 282 float maxHealth = TowerGetMaxHealth(tower); 283 float health = maxHealth - tower->damage; 284 float healthRatio = health / maxHealth; 285 286 DrawHealthBar(camera, position, healthRatio, GREEN, 35.0f); 287 } 288 }
  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 } Particle;
 28 
 29 #define TOWER_MAX_COUNT 400
30 enum TowerType 31 { 32 TOWER_TYPE_NONE, 33 TOWER_TYPE_BASE, 34 TOWER_TYPE_GUN, 35 TOWER_TYPE_BALLISTA, 36 TOWER_TYPE_CATAPULT, 37 TOWER_TYPE_WALL, 38 TOWER_TYPE_COUNT 39 };
40 41 typedef struct Tower 42 { 43 int16_t x, y; 44 uint8_t towerType; 45 Vector2 lastTargetPosition; 46 float cooldown; 47 float damage; 48 } Tower; 49 50 typedef struct GameTime 51 { 52 float time; 53 float deltaTime; 54 } GameTime; 55 56 typedef struct ButtonState { 57 char isSelected; 58 char isDisabled; 59 } ButtonState; 60 61 typedef struct GUIState { 62 int isBlocked; 63 } GUIState; 64 65 typedef enum LevelState 66 { 67 LEVEL_STATE_NONE, 68 LEVEL_STATE_BUILDING, 69 LEVEL_STATE_BATTLE, 70 LEVEL_STATE_WON_WAVE, 71 LEVEL_STATE_LOST_WAVE, 72 LEVEL_STATE_WON_LEVEL, 73 LEVEL_STATE_RESET, 74 } LevelState; 75 76 typedef struct EnemyWave { 77 uint8_t enemyType; 78 uint8_t wave; 79 uint16_t count; 80 float interval; 81 float delay; 82 Vector2 spawnPosition; 83 84 uint16_t spawned; 85 float timeToSpawnNext; 86 } EnemyWave; 87 88 typedef struct Level 89 { 90 int seed; 91 LevelState state; 92 LevelState nextState; 93 Camera3D camera; 94 int placementMode; 95 96 int initialGold; 97 int playerGold; 98 99 EnemyWave waves[10]; 100 int currentWave; 101 float waveEndTimer; 102 } Level; 103 104 typedef struct DeltaSrc 105 { 106 char x, y; 107 } DeltaSrc; 108 109 typedef struct PathfindingMap 110 { 111 int width, height; 112 float scale; 113 float *distances; 114 long *towerIndex; 115 DeltaSrc *deltaSrc; 116 float maxDistance; 117 Matrix toMapSpace; 118 Matrix toWorldSpace; 119 } PathfindingMap; 120 121 // when we execute the pathfinding algorithm, we need to store the active nodes 122 // in a queue. Each node has a position, a distance from the start, and the 123 // position of the node that we came from. 124 typedef struct PathfindingNode 125 { 126 int16_t x, y, fromX, fromY; 127 float distance; 128 } PathfindingNode; 129 130 typedef struct EnemyId 131 { 132 uint16_t index; 133 uint16_t generation; 134 } EnemyId; 135 136 typedef struct EnemyClassConfig 137 { 138 float speed; 139 float health; 140 float radius; 141 float maxAcceleration; 142 float requiredContactTime; 143 float explosionDamage; 144 float explosionRange; 145 float explosionPushbackPower; 146 int goldValue; 147 } EnemyClassConfig; 148 149 typedef struct Enemy 150 { 151 int16_t currentX, currentY; 152 int16_t nextX, nextY; 153 Vector2 simPosition; 154 Vector2 simVelocity; 155 uint16_t generation; 156 float walkedDistance; 157 float startMovingTime; 158 float damage, futureDamage; 159 float contactTime; 160 uint8_t enemyType; 161 uint8_t movePathCount; 162 Vector2 movePath[ENEMY_MAX_PATH_COUNT]; 163 } Enemy; 164 165 // a unit that uses sprites to be drawn 166 #define SPRITE_UNIT_PHASE_WEAPON_IDLE 0 167 #define SPRITE_UNIT_PHASE_WEAPON_COOLDOWN 1 168 typedef struct SpriteUnit 169 { 170 Rectangle srcRect; 171 Vector2 offset; 172 int frameCount; 173 float frameDuration; 174 Rectangle srcWeaponIdleRect; 175 Vector2 srcWeaponIdleOffset; 176 Rectangle srcWeaponCooldownRect; 177 Vector2 srcWeaponCooldownOffset; 178 } SpriteUnit; 179 180 #define PROJECTILE_MAX_COUNT 1200 181 #define PROJECTILE_TYPE_NONE 0 182 #define PROJECTILE_TYPE_ARROW 1 183 184 typedef struct Projectile 185 { 186 uint8_t projectileType; 187 float shootTime; 188 float arrivalTime; 189 float distance; 190 float damage; 191 Vector3 position; 192 Vector3 target; 193 Vector3 directionNormal; 194 EnemyId targetEnemy; 195 } Projectile; 196 197 //# Function declarations 198 float TowerGetMaxHealth(Tower *tower); 199 int Button(const char *text, int x, int y, int width, int height, ButtonState *state); 200 int EnemyAddDamage(Enemy *enemy, float damage); 201 202 //# Enemy functions 203 void EnemyInit(); 204 void EnemyDraw(); 205 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource); 206 void EnemyUpdate(); 207 float EnemyGetCurrentMaxSpeed(Enemy *enemy); 208 float EnemyGetMaxHealth(Enemy *enemy); 209 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY); 210 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount); 211 EnemyId EnemyGetId(Enemy *enemy); 212 Enemy *EnemyTryResolve(EnemyId enemyId); 213 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY); 214 int EnemyAddDamage(Enemy *enemy, float damage); 215 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range); 216 int EnemyCount(); 217 void EnemyDrawHealthbars(Camera3D camera); 218 219 //# Tower functions 220 void TowerInit(); 221 Tower *TowerGetAt(int16_t x, int16_t y); 222 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y); 223 Tower *GetTowerByType(uint8_t towerType); 224 int GetTowerCosts(uint8_t towerType); 225 float TowerGetMaxHealth(Tower *tower); 226 void TowerDraw(); 227 void TowerUpdate(); 228 void TowerDrawHealthBars(Camera3D camera); 229 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase); 230 231 //# Particles 232 void ParticleInit(); 233 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, float lifetime); 234 void ParticleUpdate(); 235 void ParticleDraw(); 236 237 //# Projectiles 238 void ProjectileInit(); 239 void ProjectileDraw(); 240 void ProjectileUpdate(); 241 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, float damage); 242 243 //# Pathfinding map 244 void PathfindingMapInit(int width, int height, Vector3 translate, float scale); 245 float PathFindingGetDistance(int mapX, int mapY); 246 Vector2 PathFindingGetGradient(Vector3 world); 247 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY); 248 void PathFindingMapUpdate(); 249 void PathFindingMapDraw(); 250 251 //# UI 252 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth); 253 254 //# Level 255 void DrawLevelGround(Level *level); 256 257 //# variables 258 extern Level *currentLevel; 259 extern Enemy enemies[ENEMY_MAX_COUNT]; 260 extern int enemyCount; 261 extern EnemyClassConfig enemyClassConfigs[]; 262 263 extern GUIState guiState; 264 extern GameTime gameTime; 265 extern Tower towers[TOWER_MAX_COUNT]; 266 extern int towerCount; 267 268 extern Texture2D palette, spriteSheet; 269 270 #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}, 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 EnemyAddDamage(Enemy *enemy, float damage)
444 {
445   enemy->damage += damage;
446   if (enemy->damage >= EnemyGetMaxHealth(enemy))
447   {
448     currentLevel->playerGold += enemyClassConfigs[enemy->enemyType].goldValue;
449     enemy->enemyType = ENEMY_TYPE_NONE;
450     return 1;
451   }
452 
453   return 0;
454 }
455 
456 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
457 {
458   int16_t castleX = 0;
459   int16_t castleY = 0;
460   Enemy* closest = 0;
461   int16_t closestDistance = 0;
462   float range2 = range * range;
463   for (int i = 0; i < enemyCount; i++)
464   {
465     Enemy* enemy = &enemies[i];
466     if (enemy->enemyType == ENEMY_TYPE_NONE)
467     {
468       continue;
469     }
470     float maxHealth = EnemyGetMaxHealth(enemy);
471     if (enemy->futureDamage >= maxHealth)
472     {
473       // ignore enemies that will die soon
474       continue;
475     }
476     int16_t dx = castleX - enemy->currentX;
477     int16_t dy = castleY - enemy->currentY;
478     int16_t distance = abs(dx) + abs(dy);
479     if (!closest || distance < closestDistance)
480     {
481       float tdx = towerX - enemy->currentX;
482       float tdy = towerY - enemy->currentY;
483       float tdistance2 = tdx * tdx + tdy * tdy;
484       if (tdistance2 <= range2)
485       {
486         closest = enemy;
487         closestDistance = distance;
488       }
489     }
490   }
491   return closest;
492 }
493 
494 int EnemyCount()
495 {
496   int count = 0;
497   for (int i = 0; i < enemyCount; i++)
498   {
499     if (enemies[i].enemyType != ENEMY_TYPE_NONE)
500     {
501       count++;
502     }
503   }
504   return count;
505 }
506 
507 void EnemyDrawHealthbars(Camera3D camera)
508 {
509   for (int i = 0; i < enemyCount; i++)
510   {
511     Enemy *enemy = &enemies[i];
512     if (enemy->enemyType == ENEMY_TYPE_NONE || enemy->damage == 0.0f)
513     {
514       continue;
515     }
516     Vector3 position = (Vector3){enemy->simPosition.x, 0.5f, enemy->simPosition.y};
517     float maxHealth = EnemyGetMaxHealth(enemy);
518     float health = maxHealth - enemy->damage;
519     float healthRatio = health / maxHealth;
520     
521     DrawHealthBar(camera, position, healthRatio, GREEN, 15.0f);
522   }
523 }
  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 void ProjectileInit()
  8 {
  9   for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
 10   {
 11     projectiles[i] = (Projectile){0};
 12   }
 13 }
 14 
 15 void ProjectileDraw()
 16 {
 17   for (int i = 0; i < projectileCount; i++)
 18   {
 19     Projectile projectile = projectiles[i];
 20     if (projectile.projectileType == PROJECTILE_TYPE_NONE)
 21     {
 22       continue;
 23     }
 24     float transition = (gameTime.time - projectile.shootTime) / (projectile.arrivalTime - projectile.shootTime);
 25     if (transition >= 1.0f)
 26     {
 27       continue;
 28     }
 29     for (float transitionOffset = 0.0f; transitionOffset < 1.0f; transitionOffset += 0.1f)
 30     {
 31       float t = transition + transitionOffset * 0.3f;
 32       if (t > 1.0f)
 33       {
 34         break;
 35       }
 36       Vector3 position = Vector3Lerp(projectile.position, projectile.target, t);
 37       Color color = RED;
 38       if (projectile.projectileType == PROJECTILE_TYPE_ARROW)
 39       {
 40         // make tip red but quickly fade to brown
 41         color = ColorLerp(BROWN, RED, transitionOffset * transitionOffset);
 42         // fake a ballista flight path using parabola equation
 43         float parabolaT = t - 0.5f;
 44         parabolaT = 1.0f - 4.0f * parabolaT * parabolaT;
 45         position.y += 0.15f * parabolaT * projectile.distance;
 46       }
 47 
 48       float size = 0.06f * (transitionOffset + 0.25f);
 49       DrawCube(position, size, size, size, color);
 50     }
 51   }
 52 }
 53 
 54 void ProjectileUpdate()
 55 {
 56   for (int i = 0; i < projectileCount; i++)
 57   {
 58     Projectile *projectile = &projectiles[i];
 59     if (projectile->projectileType == PROJECTILE_TYPE_NONE)
 60     {
 61       continue;
 62     }
 63     float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime);
 64     if (transition >= 1.0f)
 65     {
 66       projectile->projectileType = PROJECTILE_TYPE_NONE;
 67       Enemy *enemy = EnemyTryResolve(projectile->targetEnemy);
 68       if (enemy)
 69       {
 70         EnemyAddDamage(enemy, projectile->damage);
 71       }
 72       continue;
 73     }
 74   }
 75 }
 76 
 77 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, float damage)
 78 {
 79   for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
 80   {
 81     Projectile *projectile = &projectiles[i];
 82     if (projectile->projectileType == PROJECTILE_TYPE_NONE)
 83     {
 84       projectile->projectileType = projectileType;
 85       projectile->shootTime = gameTime.time;
 86       float distance = Vector3Distance(position, target);
 87       projectile->arrivalTime = gameTime.time + distance / speed;
 88       projectile->damage = damage;
 89       projectile->position = position;
 90       projectile->target = target;
 91       projectile->directionNormal = Vector3Scale(Vector3Subtract(target, position), 1.0f / distance);
 92       projectile->distance = distance;
 93       projectile->targetEnemy = EnemyGetId(enemy);
 94       projectileCount = projectileCount <= i ? i + 1 : projectileCount;
 95       return projectile;
 96     }
 97   }
 98   return 0;
 99 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 
  4 static Particle particles[PARTICLE_MAX_COUNT];
  5 static int particleCount = 0;
  6 
  7 void ParticleInit()
  8 {
  9   for (int i = 0; i < PARTICLE_MAX_COUNT; i++)
 10   {
 11     particles[i] = (Particle){0};
 12   }
 13   particleCount = 0;
 14 }
 15 
 16 static void DrawExplosionParticle(Particle *particle, float transition)
 17 {
 18   float size = 1.2f * (1.0f - transition);
 19   Color startColor = WHITE;
 20   Color endColor = RED;
 21   Color color = ColorLerp(startColor, endColor, transition);
 22   DrawCube(particle->position, size, size, size, color);
 23 }
 24 
 25 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, float lifetime)
 26 {
 27   if (particleCount >= PARTICLE_MAX_COUNT)
 28   {
 29     return;
 30   }
 31 
 32   int index = -1;
 33   for (int i = 0; i < particleCount; i++)
 34   {
 35     if (particles[i].particleType == PARTICLE_TYPE_NONE)
 36     {
 37       index = i;
 38       break;
 39     }
 40   }
 41 
 42   if (index == -1)
 43   {
 44     index = particleCount++;
 45   }
 46 
 47   Particle *particle = &particles[index];
 48   particle->particleType = particleType;
 49   particle->spawnTime = gameTime.time;
 50   particle->lifetime = lifetime;
 51   particle->position = position;
 52   particle->velocity = velocity;
 53 }
 54 
 55 void ParticleUpdate()
 56 {
 57   for (int i = 0; i < particleCount; i++)
 58   {
 59     Particle *particle = &particles[i];
 60     if (particle->particleType == PARTICLE_TYPE_NONE)
 61     {
 62       continue;
 63     }
 64 
 65     float age = gameTime.time - particle->spawnTime;
 66 
 67     if (particle->lifetime > age)
 68     {
 69       particle->position = Vector3Add(particle->position, Vector3Scale(particle->velocity, gameTime.deltaTime));
 70     }
 71     else {
 72       particle->particleType = PARTICLE_TYPE_NONE;
 73     }
 74   }
 75 }
 76 
 77 void ParticleDraw()
 78 {
 79   for (int i = 0; i < particleCount; i++)
 80   {
 81     Particle particle = particles[i];
 82     if (particle.particleType == PARTICLE_TYPE_NONE)
 83     {
 84       continue;
 85     }
 86 
 87     float age = gameTime.time - particle.spawnTime;
 88     float transition = age / particle.lifetime;
 89     switch (particle.particleType)
 90     {
 91     case PARTICLE_TYPE_EXPLOSION:
 92       DrawExplosionParticle(&particle, transition);
 93       break;
 94     default:
 95       DrawCube(particle.position, 0.3f, 0.5f, 0.3f, RED);
 96       break;
 97     }
 98   }
 99 }
  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 level now looks different each time you reset it.

The two towers are now in game with just a few changes to the existing code. They are just static boxes so far without any functionality:

The two new towers in the game
How the two new towers look in the game.

The next step is to make the towers functional. When looking at the code, we see that the tower system uses only a few functions and quite a lot of hardcoded values to retrieve configuration values for the tower types, like range and damage.

Having function getters for values is not a bad approach - I think they could be useful when the towers have levels and upgrades. However, it would be nice to have a configuration struct for the tower types. This trades a bit of flexibility for a bit of clarity and ease of use. So let's refactor the tower system a little and set up a configuration for all tower types.

  • 💾
  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 DrawLevelBuildingState(Level *level)
287 {
288   BeginMode3D(level->camera);
289   DrawLevelGround(level);
290   TowerDraw();
291   EnemyDraw();
292   ProjectileDraw();
293   ParticleDraw();
294 
295   Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera);
296   float planeDistance = ray.position.y / -ray.direction.y;
297   float planeX = ray.direction.x * planeDistance + ray.position.x;
298   float planeY = ray.direction.z * planeDistance + ray.position.z;
299   int16_t mapX = (int16_t)floorf(planeX + 0.5f);
300   int16_t mapY = (int16_t)floorf(planeY + 0.5f);
301   if (level->placementMode && !guiState.isBlocked && mapX >= -5 && mapX <= 5 && mapY >= -5 && mapY <= 5)
302   {
303     DrawCubeWires((Vector3){mapX, 0.2f, mapY}, 1.0f, 0.4f, 1.0f, RED);
304     if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
305     {
306       if (TowerTryAdd(level->placementMode, mapX, mapY))
307       {
308         level->playerGold -= GetTowerCosts(level->placementMode);
309         level->placementMode = TOWER_TYPE_NONE;
310       }
311     }
312   }
313 
314   guiState.isBlocked = 0;
315 
316   EndMode3D();
317 
318   TowerDrawHealthBars(level->camera);
319 
320   static ButtonState buildWallButtonState = {0};
321   static ButtonState buildGunButtonState = {0};
322   buildWallButtonState.isSelected = level->placementMode == TOWER_TYPE_WALL;
323 buildGunButtonState.isSelected = level->placementMode == TOWER_TYPE_ARCHER;
324 325 DrawBuildingBuildButton(level, 10, 10, 110, 30, TOWER_TYPE_WALL, "Wall");
326 DrawBuildingBuildButton(level, 10, 50, 110, 30, TOWER_TYPE_ARCHER, "Archer");
327 DrawBuildingBuildButton(level, 10, 90, 110, 30, TOWER_TYPE_BALLISTA, "Ballista"); 328 DrawBuildingBuildButton(level, 10, 130, 110, 30, TOWER_TYPE_CATAPULT, "Catapult"); 329 330 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 331 { 332 level->nextState = LEVEL_STATE_RESET; 333 } 334 335 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0)) 336 { 337 level->nextState = LEVEL_STATE_BATTLE; 338 } 339 340 const char *text = "Building phase"; 341 int textWidth = MeasureText(text, 20); 342 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 343 } 344 345 void InitBattleStateConditions(Level *level) 346 { 347 level->state = LEVEL_STATE_BATTLE; 348 level->nextState = LEVEL_STATE_NONE; 349 level->waveEndTimer = 0.0f; 350 for (int i = 0; i < 10; i++) 351 { 352 EnemyWave *wave = &level->waves[i]; 353 wave->spawned = 0; 354 wave->timeToSpawnNext = wave->delay; 355 } 356 } 357 358 void DrawLevelBattleState(Level *level) 359 { 360 BeginMode3D(level->camera); 361 DrawLevelGround(level); 362 TowerDraw(); 363 EnemyDraw(); 364 ProjectileDraw(); 365 ParticleDraw(); 366 guiState.isBlocked = 0; 367 EndMode3D(); 368 369 EnemyDrawHealthbars(level->camera); 370 TowerDrawHealthBars(level->camera); 371 372 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 373 { 374 level->nextState = LEVEL_STATE_RESET; 375 } 376 377 int maxCount = 0; 378 int remainingCount = 0; 379 for (int i = 0; i < 10; i++) 380 { 381 EnemyWave *wave = &level->waves[i]; 382 if (wave->wave != level->currentWave) 383 { 384 continue; 385 } 386 maxCount += wave->count; 387 remainingCount += wave->count - wave->spawned; 388 } 389 int aliveCount = EnemyCount(); 390 remainingCount += aliveCount; 391 392 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount); 393 int textWidth = MeasureText(text, 20); 394 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 395 } 396 397 void DrawLevel(Level *level) 398 { 399 switch (level->state) 400 { 401 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break; 402 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break; 403 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break; 404 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break; 405 default: break; 406 } 407 408 DrawLevelHud(level); 409 } 410 411 void UpdateLevel(Level *level) 412 { 413 if (level->state == LEVEL_STATE_BATTLE) 414 { 415 int activeWaves = 0; 416 for (int i = 0; i < 10; i++) 417 { 418 EnemyWave *wave = &level->waves[i]; 419 if (wave->spawned >= wave->count || wave->wave != level->currentWave) 420 { 421 continue; 422 } 423 activeWaves++; 424 wave->timeToSpawnNext -= gameTime.deltaTime; 425 if (wave->timeToSpawnNext <= 0.0f) 426 { 427 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y); 428 if (enemy) 429 { 430 wave->timeToSpawnNext = wave->interval; 431 wave->spawned++; 432 } 433 } 434 } 435 if (GetTowerByType(TOWER_TYPE_BASE) == 0) { 436 level->waveEndTimer += gameTime.deltaTime; 437 if (level->waveEndTimer >= 2.0f) 438 { 439 level->nextState = LEVEL_STATE_LOST_WAVE; 440 } 441 } 442 else if (activeWaves == 0 && EnemyCount() == 0) 443 { 444 level->waveEndTimer += gameTime.deltaTime; 445 if (level->waveEndTimer >= 2.0f) 446 { 447 level->nextState = LEVEL_STATE_WON_WAVE; 448 } 449 } 450 } 451 452 PathFindingMapUpdate(); 453 EnemyUpdate(); 454 TowerUpdate(); 455 ProjectileUpdate(); 456 ParticleUpdate(); 457 458 if (level->nextState == LEVEL_STATE_RESET) 459 { 460 InitLevel(level); 461 } 462 463 if (level->nextState == LEVEL_STATE_BATTLE) 464 { 465 InitBattleStateConditions(level); 466 } 467 468 if (level->nextState == LEVEL_STATE_WON_WAVE) 469 { 470 level->currentWave++; 471 level->state = LEVEL_STATE_WON_WAVE; 472 } 473 474 if (level->nextState == LEVEL_STATE_LOST_WAVE) 475 { 476 level->state = LEVEL_STATE_LOST_WAVE; 477 } 478 479 if (level->nextState == LEVEL_STATE_BUILDING) 480 { 481 level->state = LEVEL_STATE_BUILDING; 482 } 483 484 if (level->nextState == LEVEL_STATE_WON_LEVEL) 485 { 486 // make something of this later 487 InitLevel(level); 488 } 489 490 level->nextState = LEVEL_STATE_NONE; 491 } 492 493 float nextSpawnTime = 0.0f; 494 495 void ResetGame() 496 { 497 InitLevel(currentLevel); 498 } 499 500 void InitGame() 501 { 502 TowerInit(); 503 EnemyInit(); 504 ProjectileInit(); 505 ParticleInit(); 506 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f); 507 508 currentLevel = levels; 509 InitLevel(currentLevel); 510 } 511 512 //# Immediate GUI functions 513 514 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth) 515 { 516 const float healthBarHeight = 6.0f; 517 const float healthBarOffset = 15.0f; 518 const float inset = 2.0f; 519 const float innerWidth = healthBarWidth - inset * 2; 520 const float innerHeight = healthBarHeight - inset * 2; 521 522 Vector2 screenPos = GetWorldToScreen(position, camera); 523 float centerX = screenPos.x - healthBarWidth * 0.5f; 524 float topY = screenPos.y - healthBarOffset; 525 DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK); 526 float healthWidth = innerWidth * healthRatio; 527 DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor); 528 } 529 530 int Button(const char *text, int x, int y, int width, int height, ButtonState *state) 531 { 532 Rectangle bounds = {x, y, width, height}; 533 int isPressed = 0; 534 int isSelected = state && state->isSelected; 535 int isDisabled = state && state->isDisabled; 536 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !isDisabled) 537 { 538 Color color = isSelected ? DARKGRAY : GRAY; 539 DrawRectangle(x, y, width, height, color); 540 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) 541 { 542 isPressed = 1; 543 } 544 guiState.isBlocked = 1; 545 } 546 else 547 { 548 Color color = isSelected ? WHITE : LIGHTGRAY; 549 DrawRectangle(x, y, width, height, color); 550 } 551 Font font = GetFontDefault(); 552 Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1); 553 Color textColor = isDisabled ? GRAY : BLACK; 554 DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, textColor); 555 return isPressed; 556 } 557 558 //# Main game loop 559 560 void GameUpdate() 561 { 562 float dt = GetFrameTime(); 563 // cap maximum delta time to 0.1 seconds to prevent large time steps 564 if (dt > 0.1f) dt = 0.1f; 565 gameTime.time += dt; 566 gameTime.deltaTime = dt; 567 568 UpdateLevel(currentLevel); 569 } 570 571 int main(void) 572 { 573 int screenWidth, screenHeight; 574 GetPreferredSize(&screenWidth, &screenHeight); 575 InitWindow(screenWidth, screenHeight, "Tower defense"); 576 SetTargetFPS(30); 577 578 LoadAssets(); 579 InitGame(); 580 581 while (!WindowShouldClose()) 582 { 583 if (IsPaused()) { 584 // canvas is not visible in browser - do nothing 585 continue; 586 } 587 588 BeginDrawing(); 589 ClearBackground((Color){0x4E, 0x63, 0x26, 0xFF}); 590 591 GameUpdate(); 592 DrawLevel(currentLevel); 593 594 EndDrawing(); 595 } 596 597 CloseWindow(); 598 599 return 0; 600 }
  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 .damage = 3.0f, 11 .range = 3.0f, 12 .cost = 6, 13 .maxHealth = 10, 14 .projectileSpeed = 4.0f, 15 .projectileType = PROJECTILE_TYPE_ARROW, 16 }, 17 [TOWER_TYPE_BALLISTA] = { 18 .cooldown = 1.5f, 19 .damage = 6.0f, 20 .range = 6.0f, 21 .cost = 9, 22 .maxHealth = 10, 23 .projectileSpeed = 6.0f, 24 .projectileType = PROJECTILE_TYPE_ARROW, 25 }, 26 [TOWER_TYPE_CATAPULT] = { 27 .cooldown = 1.7f, 28 .damage = 2.0f, 29 .range = 5.0f, 30 .areaDamageRadius = 1.0f, 31 .cost = 10, 32 .maxHealth = 10, 33 .projectileSpeed = 3.0f, 34 .projectileType = PROJECTILE_TYPE_ARROW, 35 }, 36 [TOWER_TYPE_WALL] = { 37 .cost = 2, 38 .maxHealth = 10, 39 }, 40 }; 41
42 Tower towers[TOWER_MAX_COUNT]; 43 int towerCount = 0; 44 45 Model towerModels[TOWER_TYPE_COUNT]; 46 47 // definition of our archer unit 48 SpriteUnit archerUnit = { 49 .srcRect = {0, 0, 16, 16}, 50 .offset = {7, 1}, 51 .frameCount = 1, 52 .frameDuration = 0.0f, 53 .srcWeaponIdleRect = {16, 0, 6, 16}, 54 .srcWeaponIdleOffset = {8, 0}, 55 .srcWeaponCooldownRect = {22, 0, 11, 16}, 56 .srcWeaponCooldownOffset = {10, 0}, 57 }; 58 59 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase) 60 { 61 float xScale = flip ? -1.0f : 1.0f; 62 Camera3D camera = currentLevel->camera; 63 float size = 0.5f; 64 Vector2 offset = (Vector2){ unit.offset.x / 16.0f * size, unit.offset.y / 16.0f * size * xScale }; 65 Vector2 scale = (Vector2){ unit.srcRect.width / 16.0f * size, unit.srcRect.height / 16.0f * size }; 66 // we want the sprite to face the camera, so we need to calculate the up vector 67 Vector3 forward = Vector3Subtract(camera.target, camera.position); 68 Vector3 up = {0, 1, 0}; 69 Vector3 right = Vector3CrossProduct(forward, up); 70 up = Vector3Normalize(Vector3CrossProduct(right, forward)); 71 72 Rectangle srcRect = unit.srcRect; 73 if (unit.frameCount > 1) 74 { 75 srcRect.x += (int)(t / unit.frameDuration) % unit.frameCount * srcRect.width; 76 } 77 if (flip) 78 { 79 srcRect.x += srcRect.width; 80 srcRect.width = -srcRect.width; 81 } 82 DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE); 83 84 if (phase == SPRITE_UNIT_PHASE_WEAPON_COOLDOWN && unit.srcWeaponCooldownRect.width > 0) 85 { 86 offset = (Vector2){ unit.srcWeaponCooldownOffset.x / 16.0f * size, unit.srcWeaponCooldownOffset.y / 16.0f * size }; 87 scale = (Vector2){ unit.srcWeaponCooldownRect.width / 16.0f * size, unit.srcWeaponCooldownRect.height / 16.0f * size }; 88 srcRect = unit.srcWeaponCooldownRect; 89 if (flip) 90 { 91 // position.x = flip * scale.x * 0.5f; 92 srcRect.x += srcRect.width; 93 srcRect.width = -srcRect.width; 94 offset.x = scale.x - offset.x; 95 } 96 DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE); 97 } 98 else if (phase == SPRITE_UNIT_PHASE_WEAPON_IDLE && unit.srcWeaponIdleRect.width > 0) 99 { 100 offset = (Vector2){ unit.srcWeaponIdleOffset.x / 16.0f * size, unit.srcWeaponIdleOffset.y / 16.0f * size }; 101 scale = (Vector2){ unit.srcWeaponIdleRect.width / 16.0f * size, unit.srcWeaponIdleRect.height / 16.0f * size }; 102 srcRect = unit.srcWeaponIdleRect; 103 if (flip) 104 { 105 // position.x = flip * scale.x * 0.5f; 106 srcRect.x += srcRect.width; 107 srcRect.width = -srcRect.width; 108 offset.x = scale.x - offset.x; 109 } 110 DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE); 111 } 112 } 113 114 void TowerInit() 115 { 116 for (int i = 0; i < TOWER_MAX_COUNT; i++) 117 { 118 towers[i] = (Tower){0}; 119 } 120 towerCount = 0; 121 122 towerModels[TOWER_TYPE_BASE] = LoadModel("data/keep.glb"); 123 towerModels[TOWER_TYPE_WALL] = LoadModel("data/wall-0000.glb"); 124 125 for (int i = 0; i < TOWER_TYPE_COUNT; i++) 126 { 127 if (towerModels[i].materials) 128 { 129 // assign the palette texture to the material of the model (0 is not used afaik) 130 towerModels[i].materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette; 131 } 132 } 133 } 134 135 static void TowerGunUpdate(Tower *tower) 136 {
137 TowerTypeConfig config = towerTypeConfigs[tower->towerType]; 138 if (tower->cooldown <= 0.0f)
139 {
140 Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, config.range);
141 if (enemy) 142 {
143 tower->cooldown = config.cooldown;
144 // shoot the enemy; determine future position of the enemy
145 float bulletSpeed = config.projectileSpeed; 146 float bulletDamage = config.damage;
147 Vector2 velocity = enemy->simVelocity; 148 Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0); 149 Vector2 towerPosition = {tower->x, tower->y}; 150 float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed; 151 for (int i = 0; i < 8; i++) { 152 velocity = enemy->simVelocity; 153 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0); 154 float distance = Vector2Distance(towerPosition, futurePosition); 155 float eta2 = distance / bulletSpeed; 156 if (fabs(eta - eta2) < 0.01f) { 157 break; 158 } 159 eta = (eta2 + eta) * 0.5f; 160 } 161 ProjectileTryAdd(PROJECTILE_TYPE_ARROW, enemy, 162 (Vector3){towerPosition.x, 1.33f, towerPosition.y}, 163 (Vector3){futurePosition.x, 0.25f, futurePosition.y}, 164 bulletSpeed, bulletDamage); 165 enemy->futureDamage += bulletDamage; 166 tower->lastTargetPosition = futurePosition; 167 } 168 } 169 else 170 { 171 tower->cooldown -= gameTime.deltaTime; 172 } 173 } 174 175 Tower *TowerGetAt(int16_t x, int16_t y) 176 { 177 for (int i = 0; i < towerCount; i++) 178 { 179 if (towers[i].x == x && towers[i].y == y && towers[i].towerType != TOWER_TYPE_NONE) 180 { 181 return &towers[i]; 182 } 183 } 184 return 0; 185 } 186 187 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y) 188 { 189 if (towerCount >= TOWER_MAX_COUNT) 190 { 191 return 0; 192 } 193 194 Tower *tower = TowerGetAt(x, y); 195 if (tower) 196 { 197 return 0; 198 } 199 200 tower = &towers[towerCount++]; 201 tower->x = x;
202 tower->y = y; 203 tower->towerType = towerType; 204 tower->cooldown = 0.0f; 205 tower->damage = 0.0f; 206 return tower; 207 } 208 209 Tower *GetTowerByType(uint8_t towerType) 210 { 211 for (int i = 0; i < towerCount; i++) 212 { 213 if (towers[i].towerType == towerType) 214 { 215 return &towers[i]; 216 } 217 } 218 return 0; 219 } 220 221 int GetTowerCosts(uint8_t towerType) 222 { 223 return towerTypeConfigs[towerType].cost;
224 } 225 226 float TowerGetMaxHealth(Tower *tower) 227 { 228 return towerTypeConfigs[tower->towerType].maxHealth; 229 } 230 231 void TowerDraw() 232 { 233 for (int i = 0; i < towerCount; i++) 234 { 235 Tower tower = towers[i]; 236 if (tower.towerType == TOWER_TYPE_NONE) 237 { 238 continue; 239 } 240 241 switch (tower.towerType) 242 { 243 case TOWER_TYPE_ARCHER: 244 { 245 Vector2 screenPosTower = GetWorldToScreen((Vector3){tower.x, 0.0f, tower.y}, currentLevel->camera); 246 Vector2 screenPosTarget = GetWorldToScreen((Vector3){tower.lastTargetPosition.x, 0.0f, tower.lastTargetPosition.y}, currentLevel->camera); 247 DrawModel(towerModels[TOWER_TYPE_WALL], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE); 248 DrawSpriteUnit(archerUnit, (Vector3){tower.x, 1.0f, tower.y}, 0, screenPosTarget.x > screenPosTower.x, 249 tower.cooldown > 0.2f ? SPRITE_UNIT_PHASE_WEAPON_COOLDOWN : SPRITE_UNIT_PHASE_WEAPON_IDLE); 250 }
251 break; 252 case TOWER_TYPE_BALLISTA: 253 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, BROWN);
254 break; 255 case TOWER_TYPE_CATAPULT: 256 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, DARKGRAY); 257 break; 258 default: 259 if (towerModels[tower.towerType].materials) 260 { 261 DrawModel(towerModels[tower.towerType], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE); 262 } else { 263 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY); 264 } 265 break; 266 } 267 } 268 } 269 270 void TowerUpdate() 271 { 272 for (int i = 0; i < towerCount; i++) 273 { 274 Tower *tower = &towers[i]; 275 switch (tower->towerType) 276 { 277 case TOWER_TYPE_CATAPULT: 278 case TOWER_TYPE_BALLISTA: 279 case TOWER_TYPE_ARCHER: 280 TowerGunUpdate(tower); 281 break; 282 } 283 } 284 } 285 286 void TowerDrawHealthBars(Camera3D camera) 287 { 288 for (int i = 0; i < towerCount; i++) 289 { 290 Tower *tower = &towers[i]; 291 if (tower->towerType == TOWER_TYPE_NONE || tower->damage <= 0.0f) 292 { 293 continue; 294 } 295 296 Vector3 position = (Vector3){tower->x, 0.5f, tower->y}; 297 float maxHealth = TowerGetMaxHealth(tower); 298 float health = maxHealth - tower->damage; 299 float healthRatio = health / maxHealth; 300 301 DrawHealthBar(camera, position, healthRatio, GREEN, 35.0f); 302 } 303 }
  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 } Particle;
 28 
 29 #define TOWER_MAX_COUNT 400
 30 enum TowerType
 31 {
 32   TOWER_TYPE_NONE,
 33   TOWER_TYPE_BASE,
34 TOWER_TYPE_ARCHER,
35 TOWER_TYPE_BALLISTA, 36 TOWER_TYPE_CATAPULT, 37 TOWER_TYPE_WALL, 38 TOWER_TYPE_COUNT
39 }; 40 41 typedef struct TowerTypeConfig 42 { 43 float cooldown; 44 float damage; 45 float range; 46 float areaDamageRadius; 47 float projectileSpeed; 48 uint8_t cost; 49 uint8_t projectileType; 50 uint16_t maxHealth; 51 } TowerTypeConfig;
52 53 typedef struct Tower 54 { 55 int16_t x, y; 56 uint8_t towerType; 57 Vector2 lastTargetPosition; 58 float cooldown; 59 float damage; 60 } Tower; 61 62 typedef struct GameTime 63 { 64 float time; 65 float deltaTime; 66 } GameTime; 67 68 typedef struct ButtonState { 69 char isSelected; 70 char isDisabled; 71 } ButtonState; 72 73 typedef struct GUIState { 74 int isBlocked; 75 } GUIState; 76 77 typedef enum LevelState 78 { 79 LEVEL_STATE_NONE, 80 LEVEL_STATE_BUILDING, 81 LEVEL_STATE_BATTLE, 82 LEVEL_STATE_WON_WAVE, 83 LEVEL_STATE_LOST_WAVE, 84 LEVEL_STATE_WON_LEVEL, 85 LEVEL_STATE_RESET, 86 } LevelState; 87 88 typedef struct EnemyWave { 89 uint8_t enemyType; 90 uint8_t wave; 91 uint16_t count; 92 float interval; 93 float delay; 94 Vector2 spawnPosition; 95 96 uint16_t spawned; 97 float timeToSpawnNext; 98 } EnemyWave; 99 100 typedef struct Level 101 { 102 int seed; 103 LevelState state; 104 LevelState nextState; 105 Camera3D camera; 106 int placementMode; 107 108 int initialGold; 109 int playerGold; 110 111 EnemyWave waves[10]; 112 int currentWave; 113 float waveEndTimer; 114 } Level; 115 116 typedef struct DeltaSrc 117 { 118 char x, y; 119 } DeltaSrc; 120 121 typedef struct PathfindingMap 122 { 123 int width, height; 124 float scale; 125 float *distances; 126 long *towerIndex; 127 DeltaSrc *deltaSrc; 128 float maxDistance; 129 Matrix toMapSpace; 130 Matrix toWorldSpace; 131 } PathfindingMap; 132 133 // when we execute the pathfinding algorithm, we need to store the active nodes 134 // in a queue. Each node has a position, a distance from the start, and the 135 // position of the node that we came from. 136 typedef struct PathfindingNode 137 { 138 int16_t x, y, fromX, fromY; 139 float distance; 140 } PathfindingNode; 141 142 typedef struct EnemyId 143 { 144 uint16_t index; 145 uint16_t generation; 146 } EnemyId; 147 148 typedef struct EnemyClassConfig 149 { 150 float speed; 151 float health; 152 float radius; 153 float maxAcceleration; 154 float requiredContactTime; 155 float explosionDamage; 156 float explosionRange; 157 float explosionPushbackPower; 158 int goldValue; 159 } EnemyClassConfig; 160 161 typedef struct Enemy 162 { 163 int16_t currentX, currentY; 164 int16_t nextX, nextY; 165 Vector2 simPosition; 166 Vector2 simVelocity; 167 uint16_t generation; 168 float walkedDistance; 169 float startMovingTime; 170 float damage, futureDamage; 171 float contactTime; 172 uint8_t enemyType; 173 uint8_t movePathCount; 174 Vector2 movePath[ENEMY_MAX_PATH_COUNT]; 175 } Enemy; 176 177 // a unit that uses sprites to be drawn 178 #define SPRITE_UNIT_PHASE_WEAPON_IDLE 0 179 #define SPRITE_UNIT_PHASE_WEAPON_COOLDOWN 1 180 typedef struct SpriteUnit 181 { 182 Rectangle srcRect; 183 Vector2 offset; 184 int frameCount; 185 float frameDuration; 186 Rectangle srcWeaponIdleRect; 187 Vector2 srcWeaponIdleOffset; 188 Rectangle srcWeaponCooldownRect; 189 Vector2 srcWeaponCooldownOffset; 190 } SpriteUnit; 191 192 #define PROJECTILE_MAX_COUNT 1200 193 #define PROJECTILE_TYPE_NONE 0 194 #define PROJECTILE_TYPE_ARROW 1 195 196 typedef struct Projectile 197 { 198 uint8_t projectileType; 199 float shootTime; 200 float arrivalTime; 201 float distance; 202 float damage; 203 Vector3 position; 204 Vector3 target; 205 Vector3 directionNormal; 206 EnemyId targetEnemy; 207 } Projectile; 208 209 //# Function declarations 210 float TowerGetMaxHealth(Tower *tower); 211 int Button(const char *text, int x, int y, int width, int height, ButtonState *state); 212 int EnemyAddDamage(Enemy *enemy, float damage); 213 214 //# Enemy functions 215 void EnemyInit(); 216 void EnemyDraw(); 217 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource); 218 void EnemyUpdate(); 219 float EnemyGetCurrentMaxSpeed(Enemy *enemy); 220 float EnemyGetMaxHealth(Enemy *enemy); 221 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY); 222 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount); 223 EnemyId EnemyGetId(Enemy *enemy); 224 Enemy *EnemyTryResolve(EnemyId enemyId); 225 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY); 226 int EnemyAddDamage(Enemy *enemy, float damage); 227 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range); 228 int EnemyCount(); 229 void EnemyDrawHealthbars(Camera3D camera); 230 231 //# Tower functions 232 void TowerInit(); 233 Tower *TowerGetAt(int16_t x, int16_t y); 234 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y); 235 Tower *GetTowerByType(uint8_t towerType); 236 int GetTowerCosts(uint8_t towerType); 237 float TowerGetMaxHealth(Tower *tower); 238 void TowerDraw(); 239 void TowerUpdate(); 240 void TowerDrawHealthBars(Camera3D camera); 241 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase); 242 243 //# Particles 244 void ParticleInit(); 245 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, float lifetime); 246 void ParticleUpdate(); 247 void ParticleDraw(); 248 249 //# Projectiles 250 void ProjectileInit(); 251 void ProjectileDraw(); 252 void ProjectileUpdate(); 253 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, float damage); 254 255 //# Pathfinding map 256 void PathfindingMapInit(int width, int height, Vector3 translate, float scale); 257 float PathFindingGetDistance(int mapX, int mapY); 258 Vector2 PathFindingGetGradient(Vector3 world); 259 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY); 260 void PathFindingMapUpdate(); 261 void PathFindingMapDraw(); 262 263 //# UI 264 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth); 265 266 //# Level 267 void DrawLevelGround(Level *level); 268 269 //# variables 270 extern Level *currentLevel; 271 extern Enemy enemies[ENEMY_MAX_COUNT]; 272 extern int enemyCount; 273 extern EnemyClassConfig enemyClassConfigs[]; 274 275 extern GUIState guiState; 276 extern GameTime gameTime; 277 extern Tower towers[TOWER_MAX_COUNT]; 278 extern int towerCount; 279 280 extern Texture2D palette, spriteSheet; 281 282 #endif
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 
  4 static Projectile projectiles[PROJECTILE_MAX_COUNT];
  5 static int projectileCount = 0;
  6 
  7 void ProjectileInit()
  8 {
  9   for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
 10   {
 11     projectiles[i] = (Projectile){0};
 12   }
 13 }
 14 
 15 void ProjectileDraw()
 16 {
 17   for (int i = 0; i < projectileCount; i++)
 18   {
 19     Projectile projectile = projectiles[i];
 20     if (projectile.projectileType == PROJECTILE_TYPE_NONE)
 21     {
 22       continue;
 23     }
 24     float transition = (gameTime.time - projectile.shootTime) / (projectile.arrivalTime - projectile.shootTime);
 25     if (transition >= 1.0f)
 26     {
 27       continue;
 28     }
 29     for (float transitionOffset = 0.0f; transitionOffset < 1.0f; transitionOffset += 0.1f)
 30     {
 31       float t = transition + transitionOffset * 0.3f;
 32       if (t > 1.0f)
 33       {
 34         break;
 35       }
 36       Vector3 position = Vector3Lerp(projectile.position, projectile.target, t);
 37       Color color = RED;
 38       if (projectile.projectileType == PROJECTILE_TYPE_ARROW)
 39       {
 40         // make tip red but quickly fade to brown
 41         color = ColorLerp(BROWN, RED, transitionOffset * transitionOffset);
 42         // fake a ballista flight path using parabola equation
 43         float parabolaT = t - 0.5f;
 44         parabolaT = 1.0f - 4.0f * parabolaT * parabolaT;
 45         position.y += 0.15f * parabolaT * projectile.distance;
 46       }
 47 
 48       float size = 0.06f * (transitionOffset + 0.25f);
 49       DrawCube(position, size, size, size, color);
 50     }
 51   }
 52 }
 53 
 54 void ProjectileUpdate()
 55 {
 56   for (int i = 0; i < projectileCount; i++)
 57   {
 58     Projectile *projectile = &projectiles[i];
 59     if (projectile->projectileType == PROJECTILE_TYPE_NONE)
 60     {
 61       continue;
 62     }
 63     float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime);
 64     if (transition >= 1.0f)
 65     {
 66       projectile->projectileType = PROJECTILE_TYPE_NONE;
 67       Enemy *enemy = EnemyTryResolve(projectile->targetEnemy);
 68       if (enemy)
 69       {
 70         EnemyAddDamage(enemy, projectile->damage);
 71       }
 72       continue;
 73     }
 74   }
 75 }
 76 
 77 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, float damage)
 78 {
 79   for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
 80   {
 81     Projectile *projectile = &projectiles[i];
 82     if (projectile->projectileType == PROJECTILE_TYPE_NONE)
 83     {
 84       projectile->projectileType = projectileType;
 85       projectile->shootTime = gameTime.time;
 86       float distance = Vector3Distance(position, target);
 87       projectile->arrivalTime = gameTime.time + distance / speed;
 88       projectile->damage = damage;
 89       projectile->position = position;
 90       projectile->target = target;
 91       projectile->directionNormal = Vector3Scale(Vector3Subtract(target, position), 1.0f / distance);
 92       projectile->distance = distance;
 93       projectile->targetEnemy = EnemyGetId(enemy);
 94       projectileCount = projectileCount <= i ? i + 1 : projectileCount;
 95       return projectile;
 96     }
 97   }
 98   return 0;
 99 }
  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}, 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 EnemyAddDamage(Enemy *enemy, float damage)
444 {
445   enemy->damage += damage;
446   if (enemy->damage >= EnemyGetMaxHealth(enemy))
447   {
448     currentLevel->playerGold += enemyClassConfigs[enemy->enemyType].goldValue;
449     enemy->enemyType = ENEMY_TYPE_NONE;
450     return 1;
451   }
452 
453   return 0;
454 }
455 
456 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
457 {
458   int16_t castleX = 0;
459   int16_t castleY = 0;
460   Enemy* closest = 0;
461   int16_t closestDistance = 0;
462   float range2 = range * range;
463   for (int i = 0; i < enemyCount; i++)
464   {
465     Enemy* enemy = &enemies[i];
466     if (enemy->enemyType == ENEMY_TYPE_NONE)
467     {
468       continue;
469     }
470     float maxHealth = EnemyGetMaxHealth(enemy);
471     if (enemy->futureDamage >= maxHealth)
472     {
473       // ignore enemies that will die soon
474       continue;
475     }
476     int16_t dx = castleX - enemy->currentX;
477     int16_t dy = castleY - enemy->currentY;
478     int16_t distance = abs(dx) + abs(dy);
479     if (!closest || distance < closestDistance)
480     {
481       float tdx = towerX - enemy->currentX;
482       float tdy = towerY - enemy->currentY;
483       float tdistance2 = tdx * tdx + tdy * tdy;
484       if (tdistance2 <= range2)
485       {
486         closest = enemy;
487         closestDistance = distance;
488       }
489     }
490   }
491   return closest;
492 }
493 
494 int EnemyCount()
495 {
496   int count = 0;
497   for (int i = 0; i < enemyCount; i++)
498   {
499     if (enemies[i].enemyType != ENEMY_TYPE_NONE)
500     {
501       count++;
502     }
503   }
504   return count;
505 }
506 
507 void EnemyDrawHealthbars(Camera3D camera)
508 {
509   for (int i = 0; i < enemyCount; i++)
510   {
511     Enemy *enemy = &enemies[i];
512     if (enemy->enemyType == ENEMY_TYPE_NONE || enemy->damage == 0.0f)
513     {
514       continue;
515     }
516     Vector3 position = (Vector3){enemy->simPosition.x, 0.5f, enemy->simPosition.y};
517     float maxHealth = EnemyGetMaxHealth(enemy);
518     float health = maxHealth - enemy->damage;
519     float healthRatio = health / maxHealth;
520     
521     DrawHealthBar(camera, position, healthRatio, GREEN, 15.0f);
522   }
523 }
  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 Particle particles[PARTICLE_MAX_COUNT];
  5 static int particleCount = 0;
  6 
  7 void ParticleInit()
  8 {
  9   for (int i = 0; i < PARTICLE_MAX_COUNT; i++)
 10   {
 11     particles[i] = (Particle){0};
 12   }
 13   particleCount = 0;
 14 }
 15 
 16 static void DrawExplosionParticle(Particle *particle, float transition)
 17 {
 18   float size = 1.2f * (1.0f - transition);
 19   Color startColor = WHITE;
 20   Color endColor = RED;
 21   Color color = ColorLerp(startColor, endColor, transition);
 22   DrawCube(particle->position, size, size, size, color);
 23 }
 24 
 25 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, float lifetime)
 26 {
 27   if (particleCount >= PARTICLE_MAX_COUNT)
 28   {
 29     return;
 30   }
 31 
 32   int index = -1;
 33   for (int i = 0; i < particleCount; i++)
 34   {
 35     if (particles[i].particleType == PARTICLE_TYPE_NONE)
 36     {
 37       index = i;
 38       break;
 39     }
 40   }
 41 
 42   if (index == -1)
 43   {
 44     index = particleCount++;
 45   }
 46 
 47   Particle *particle = &particles[index];
 48   particle->particleType = particleType;
 49   particle->spawnTime = gameTime.time;
 50   particle->lifetime = lifetime;
 51   particle->position = position;
 52   particle->velocity = velocity;
 53 }
 54 
 55 void ParticleUpdate()
 56 {
 57   for (int i = 0; i < particleCount; i++)
 58   {
 59     Particle *particle = &particles[i];
 60     if (particle->particleType == PARTICLE_TYPE_NONE)
 61     {
 62       continue;
 63     }
 64 
 65     float age = gameTime.time - particle->spawnTime;
 66 
 67     if (particle->lifetime > age)
 68     {
 69       particle->position = Vector3Add(particle->position, Vector3Scale(particle->velocity, gameTime.deltaTime));
 70     }
 71     else {
 72       particle->particleType = PARTICLE_TYPE_NONE;
 73     }
 74   }
 75 }
 76 
 77 void ParticleDraw()
 78 {
 79   for (int i = 0; i < particleCount; i++)
 80   {
 81     Particle particle = particles[i];
 82     if (particle.particleType == PARTICLE_TYPE_NONE)
 83     {
 84       continue;
 85     }
 86 
 87     float age = gameTime.time - particle.spawnTime;
 88     float transition = age / particle.lifetime;
 89     switch (particle.particleType)
 90     {
 91     case PARTICLE_TYPE_EXPLOSION:
 92       DrawExplosionParticle(&particle, transition);
 93       break;
 94     default:
 95       DrawCube(particle.position, 0.3f, 0.5f, 0.3f, RED);
 96       break;
 97     }
 98   }
 99 }
  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 ballista tower is now shooting at the enemies and has a larger range than the basic tower.

This was fairly simple to do and adding the tower is easy too. The catapult tower is also working as well - after all, with the new structure, it was a matter of adding a the configuration and handling the tower type in the tower update function.

While I don't want to add graphics in this part, we can at least adjust the projectile effect so the catapult tower shoots a projectile that flies in a high arc while the ballista tower shoots in an almost straight line. For that change, we only need to create new projectile types and adjust the projectile system:

  • 💾
  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 DrawLevelBuildingState(Level *level)
287 {
288   BeginMode3D(level->camera);
289   DrawLevelGround(level);
290   TowerDraw();
291   EnemyDraw();
292   ProjectileDraw();
293   ParticleDraw();
294 
295   Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera);
296   float planeDistance = ray.position.y / -ray.direction.y;
297   float planeX = ray.direction.x * planeDistance + ray.position.x;
298   float planeY = ray.direction.z * planeDistance + ray.position.z;
299   int16_t mapX = (int16_t)floorf(planeX + 0.5f);
300   int16_t mapY = (int16_t)floorf(planeY + 0.5f);
301   if (level->placementMode && !guiState.isBlocked && mapX >= -5 && mapX <= 5 && mapY >= -5 && mapY <= 5)
302   {
303     DrawCubeWires((Vector3){mapX, 0.2f, mapY}, 1.0f, 0.4f, 1.0f, RED);
304     if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
305     {
306       if (TowerTryAdd(level->placementMode, mapX, mapY))
307       {
308         level->playerGold -= GetTowerCosts(level->placementMode);
309         level->placementMode = TOWER_TYPE_NONE;
310       }
311     }
312   }
313 
314   guiState.isBlocked = 0;
315 
316   EndMode3D();
317 
318   TowerDrawHealthBars(level->camera);
319 
320   static ButtonState buildWallButtonState = {0};
321   static ButtonState buildGunButtonState = {0};
322   buildWallButtonState.isSelected = level->placementMode == TOWER_TYPE_WALL;
323   buildGunButtonState.isSelected = level->placementMode == TOWER_TYPE_ARCHER;
324 
325   DrawBuildingBuildButton(level, 10, 10, 110, 30, TOWER_TYPE_WALL, "Wall");
326   DrawBuildingBuildButton(level, 10, 50, 110, 30, TOWER_TYPE_ARCHER, "Archer");
327   DrawBuildingBuildButton(level, 10, 90, 110, 30, TOWER_TYPE_BALLISTA, "Ballista");
328   DrawBuildingBuildButton(level, 10, 130, 110, 30, TOWER_TYPE_CATAPULT, "Catapult");
329 
330   if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
331   {
332     level->nextState = LEVEL_STATE_RESET;
333   }
334   
335   if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0))
336   {
337     level->nextState = LEVEL_STATE_BATTLE;
338   }
339 
340   const char *text = "Building phase";
341   int textWidth = MeasureText(text, 20);
342   DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
343 }
344 
345 void InitBattleStateConditions(Level *level)
346 {
347   level->state = LEVEL_STATE_BATTLE;
348   level->nextState = LEVEL_STATE_NONE;
349   level->waveEndTimer = 0.0f;
350   for (int i = 0; i < 10; i++)
351   {
352     EnemyWave *wave = &level->waves[i];
353     wave->spawned = 0;
354     wave->timeToSpawnNext = wave->delay;
355   }
356 }
357 
358 void DrawLevelBattleState(Level *level)
359 {
360   BeginMode3D(level->camera);
361   DrawLevelGround(level);
362   TowerDraw();
363   EnemyDraw();
364   ProjectileDraw();
365   ParticleDraw();
366   guiState.isBlocked = 0;
367   EndMode3D();
368 
369   EnemyDrawHealthbars(level->camera);
370   TowerDrawHealthBars(level->camera);
371 
372   if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
373   {
374     level->nextState = LEVEL_STATE_RESET;
375   }
376 
377   int maxCount = 0;
378   int remainingCount = 0;
379   for (int i = 0; i < 10; i++)
380   {
381     EnemyWave *wave = &level->waves[i];
382     if (wave->wave != level->currentWave)
383     {
384       continue;
385     }
386     maxCount += wave->count;
387     remainingCount += wave->count - wave->spawned;
388   }
389   int aliveCount = EnemyCount();
390   remainingCount += aliveCount;
391 
392   const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount);
393   int textWidth = MeasureText(text, 20);
394   DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
395 }
396 
397 void DrawLevel(Level *level)
398 {
399   switch (level->state)
400   {
401     case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break;
402     case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break;
403     case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break;
404     case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break;
405     default: break;
406   }
407 
408   DrawLevelHud(level);
409 }
410 
411 void UpdateLevel(Level *level)
412 {
413   if (level->state == LEVEL_STATE_BATTLE)
414   {
415     int activeWaves = 0;
416     for (int i = 0; i < 10; i++)
417     {
418       EnemyWave *wave = &level->waves[i];
419       if (wave->spawned >= wave->count || wave->wave != level->currentWave)
420       {
421         continue;
422       }
423       activeWaves++;
424       wave->timeToSpawnNext -= gameTime.deltaTime;
425       if (wave->timeToSpawnNext <= 0.0f)
426       {
427         Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y);
428         if (enemy)
429         {
430           wave->timeToSpawnNext = wave->interval;
431           wave->spawned++;
432         }
433       }
434     }
435     if (GetTowerByType(TOWER_TYPE_BASE) == 0) {
436       level->waveEndTimer += gameTime.deltaTime;
437       if (level->waveEndTimer >= 2.0f)
438       {
439         level->nextState = LEVEL_STATE_LOST_WAVE;
440       }
441     }
442     else if (activeWaves == 0 && EnemyCount() == 0)
443     {
444       level->waveEndTimer += gameTime.deltaTime;
445       if (level->waveEndTimer >= 2.0f)
446       {
447         level->nextState = LEVEL_STATE_WON_WAVE;
448       }
449     }
450   }
451 
452   PathFindingMapUpdate();
453   EnemyUpdate();
454   TowerUpdate();
455   ProjectileUpdate();
456   ParticleUpdate();
457 
458   if (level->nextState == LEVEL_STATE_RESET)
459   {
460     InitLevel(level);
461   }
462   
463   if (level->nextState == LEVEL_STATE_BATTLE)
464   {
465     InitBattleStateConditions(level);
466   }
467   
468   if (level->nextState == LEVEL_STATE_WON_WAVE)
469   {
470     level->currentWave++;
471     level->state = LEVEL_STATE_WON_WAVE;
472   }
473   
474   if (level->nextState == LEVEL_STATE_LOST_WAVE)
475   {
476     level->state = LEVEL_STATE_LOST_WAVE;
477   }
478 
479   if (level->nextState == LEVEL_STATE_BUILDING)
480   {
481     level->state = LEVEL_STATE_BUILDING;
482   }
483 
484   if (level->nextState == LEVEL_STATE_WON_LEVEL)
485   {
486     // make something of this later
487     InitLevel(level);
488   }
489 
490   level->nextState = LEVEL_STATE_NONE;
491 }
492 
493 float nextSpawnTime = 0.0f;
494 
495 void ResetGame()
496 {
497   InitLevel(currentLevel);
498 }
499 
500 void InitGame()
501 {
502   TowerInit();
503   EnemyInit();
504   ProjectileInit();
505   ParticleInit();
506   PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f);
507 
508   currentLevel = levels;
509   InitLevel(currentLevel);
510 }
511 
512 //# Immediate GUI functions
513 
514 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth)
515 {
516   const float healthBarHeight = 6.0f;
517   const float healthBarOffset = 15.0f;
518   const float inset = 2.0f;
519   const float innerWidth = healthBarWidth - inset * 2;
520   const float innerHeight = healthBarHeight - inset * 2;
521 
522   Vector2 screenPos = GetWorldToScreen(position, camera);
523   float centerX = screenPos.x - healthBarWidth * 0.5f;
524   float topY = screenPos.y - healthBarOffset;
525   DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK);
526   float healthWidth = innerWidth * healthRatio;
527   DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor);
528 }
529 
530 int Button(const char *text, int x, int y, int width, int height, ButtonState *state)
531 {
532   Rectangle bounds = {x, y, width, height};
533   int isPressed = 0;
534   int isSelected = state && state->isSelected;
535   int isDisabled = state && state->isDisabled;
536   if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !isDisabled)
537   {
538     Color color = isSelected ? DARKGRAY : GRAY;
539     DrawRectangle(x, y, width, height, color);
540     if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
541     {
542       isPressed = 1;
543     }
544     guiState.isBlocked = 1;
545   }
546   else
547   {
548     Color color = isSelected ? WHITE : LIGHTGRAY;
549     DrawRectangle(x, y, width, height, color);
550   }
551   Font font = GetFontDefault();
552   Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1);
553   Color textColor = isDisabled ? GRAY : BLACK;
554   DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, textColor);
555   return isPressed;
556 }
557 
558 //# Main game loop
559 
560 void GameUpdate()
561 {
562   float dt = GetFrameTime();
563   // cap maximum delta time to 0.1 seconds to prevent large time steps
564   if (dt > 0.1f) dt = 0.1f;
565   gameTime.time += dt;
566   gameTime.deltaTime = dt;
567 
568   UpdateLevel(currentLevel);
569 }
570 
571 int main(void)
572 {
573   int screenWidth, screenHeight;
574   GetPreferredSize(&screenWidth, &screenHeight);
575   InitWindow(screenWidth, screenHeight, "Tower defense");
576   SetTargetFPS(30);
577 
578   LoadAssets();
579   InitGame();
580 
581   while (!WindowShouldClose())
582   {
583     if (IsPaused()) {
584       // canvas is not visible in browser - do nothing
585       continue;
586     }
587 
588     BeginDrawing();
589     ClearBackground((Color){0x4E, 0x63, 0x26, 0xFF});
590 
591     GameUpdate();
592     DrawLevel(currentLevel);
593 
594     EndDrawing();
595   }
596 
597   CloseWindow();
598 
599   return 0;
600 }
  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         .damage = 3.0f,
 11         .range = 3.0f,
 12         .cost = 6,
 13         .maxHealth = 10,
 14         .projectileSpeed = 4.0f,
 15         .projectileType = PROJECTILE_TYPE_ARROW,
 16     },
 17     [TOWER_TYPE_BALLISTA] = {
 18         .cooldown = 1.5f,
 19         .damage = 6.0f,
 20         .range = 6.0f,
 21         .cost = 9,
 22         .maxHealth = 10,
23 .projectileSpeed = 10.0f, 24 .projectileType = PROJECTILE_TYPE_BALLISTA,
25 }, 26 [TOWER_TYPE_CATAPULT] = { 27 .cooldown = 1.7f, 28 .damage = 2.0f, 29 .range = 5.0f, 30 .areaDamageRadius = 1.0f, 31 .cost = 10, 32 .maxHealth = 10, 33 .projectileSpeed = 3.0f,
34 .projectileType = PROJECTILE_TYPE_CATAPULT,
35 }, 36 [TOWER_TYPE_WALL] = { 37 .cost = 2, 38 .maxHealth = 10, 39 }, 40 }; 41 42 Tower towers[TOWER_MAX_COUNT]; 43 int towerCount = 0; 44 45 Model towerModels[TOWER_TYPE_COUNT]; 46 47 // definition of our archer unit 48 SpriteUnit archerUnit = { 49 .srcRect = {0, 0, 16, 16}, 50 .offset = {7, 1}, 51 .frameCount = 1, 52 .frameDuration = 0.0f, 53 .srcWeaponIdleRect = {16, 0, 6, 16}, 54 .srcWeaponIdleOffset = {8, 0}, 55 .srcWeaponCooldownRect = {22, 0, 11, 16}, 56 .srcWeaponCooldownOffset = {10, 0}, 57 }; 58 59 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase) 60 { 61 float xScale = flip ? -1.0f : 1.0f; 62 Camera3D camera = currentLevel->camera; 63 float size = 0.5f; 64 Vector2 offset = (Vector2){ unit.offset.x / 16.0f * size, unit.offset.y / 16.0f * size * xScale }; 65 Vector2 scale = (Vector2){ unit.srcRect.width / 16.0f * size, unit.srcRect.height / 16.0f * size }; 66 // we want the sprite to face the camera, so we need to calculate the up vector 67 Vector3 forward = Vector3Subtract(camera.target, camera.position); 68 Vector3 up = {0, 1, 0}; 69 Vector3 right = Vector3CrossProduct(forward, up); 70 up = Vector3Normalize(Vector3CrossProduct(right, forward)); 71 72 Rectangle srcRect = unit.srcRect; 73 if (unit.frameCount > 1) 74 { 75 srcRect.x += (int)(t / unit.frameDuration) % unit.frameCount * srcRect.width; 76 } 77 if (flip) 78 { 79 srcRect.x += srcRect.width; 80 srcRect.width = -srcRect.width; 81 } 82 DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE); 83 84 if (phase == SPRITE_UNIT_PHASE_WEAPON_COOLDOWN && unit.srcWeaponCooldownRect.width > 0) 85 { 86 offset = (Vector2){ unit.srcWeaponCooldownOffset.x / 16.0f * size, unit.srcWeaponCooldownOffset.y / 16.0f * size }; 87 scale = (Vector2){ unit.srcWeaponCooldownRect.width / 16.0f * size, unit.srcWeaponCooldownRect.height / 16.0f * size }; 88 srcRect = unit.srcWeaponCooldownRect; 89 if (flip) 90 { 91 // position.x = flip * scale.x * 0.5f; 92 srcRect.x += srcRect.width; 93 srcRect.width = -srcRect.width; 94 offset.x = scale.x - offset.x; 95 } 96 DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE); 97 } 98 else if (phase == SPRITE_UNIT_PHASE_WEAPON_IDLE && unit.srcWeaponIdleRect.width > 0) 99 { 100 offset = (Vector2){ unit.srcWeaponIdleOffset.x / 16.0f * size, unit.srcWeaponIdleOffset.y / 16.0f * size }; 101 scale = (Vector2){ unit.srcWeaponIdleRect.width / 16.0f * size, unit.srcWeaponIdleRect.height / 16.0f * size }; 102 srcRect = unit.srcWeaponIdleRect; 103 if (flip) 104 { 105 // position.x = flip * scale.x * 0.5f; 106 srcRect.x += srcRect.width; 107 srcRect.width = -srcRect.width; 108 offset.x = scale.x - offset.x; 109 } 110 DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE); 111 } 112 } 113 114 void TowerInit() 115 { 116 for (int i = 0; i < TOWER_MAX_COUNT; i++) 117 { 118 towers[i] = (Tower){0}; 119 } 120 towerCount = 0; 121 122 towerModels[TOWER_TYPE_BASE] = LoadModel("data/keep.glb"); 123 towerModels[TOWER_TYPE_WALL] = LoadModel("data/wall-0000.glb"); 124 125 for (int i = 0; i < TOWER_TYPE_COUNT; i++) 126 { 127 if (towerModels[i].materials) 128 { 129 // assign the palette texture to the material of the model (0 is not used afaik) 130 towerModels[i].materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette; 131 } 132 } 133 } 134 135 static void TowerGunUpdate(Tower *tower) 136 { 137 TowerTypeConfig config = towerTypeConfigs[tower->towerType]; 138 if (tower->cooldown <= 0.0f) 139 { 140 Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, config.range); 141 if (enemy) 142 { 143 tower->cooldown = config.cooldown; 144 // shoot the enemy; determine future position of the enemy 145 float bulletSpeed = config.projectileSpeed; 146 float bulletDamage = config.damage; 147 Vector2 velocity = enemy->simVelocity; 148 Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0); 149 Vector2 towerPosition = {tower->x, tower->y}; 150 float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed; 151 for (int i = 0; i < 8; i++) { 152 velocity = enemy->simVelocity; 153 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0); 154 float distance = Vector2Distance(towerPosition, futurePosition); 155 float eta2 = distance / bulletSpeed; 156 if (fabs(eta - eta2) < 0.01f) { 157 break; 158 } 159 eta = (eta2 + eta) * 0.5f; 160 }
161 162 ProjectileTryAdd(config.projectileType, enemy,
163 (Vector3){towerPosition.x, 1.33f, towerPosition.y}, 164 (Vector3){futurePosition.x, 0.25f, futurePosition.y}, 165 bulletSpeed, bulletDamage); 166 enemy->futureDamage += bulletDamage; 167 tower->lastTargetPosition = futurePosition; 168 } 169 } 170 else 171 { 172 tower->cooldown -= gameTime.deltaTime; 173 } 174 } 175 176 Tower *TowerGetAt(int16_t x, int16_t y) 177 { 178 for (int i = 0; i < towerCount; i++) 179 { 180 if (towers[i].x == x && towers[i].y == y && towers[i].towerType != TOWER_TYPE_NONE) 181 { 182 return &towers[i]; 183 } 184 } 185 return 0; 186 } 187 188 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y) 189 { 190 if (towerCount >= TOWER_MAX_COUNT) 191 { 192 return 0; 193 } 194 195 Tower *tower = TowerGetAt(x, y); 196 if (tower) 197 { 198 return 0; 199 } 200 201 tower = &towers[towerCount++]; 202 tower->x = x; 203 tower->y = y; 204 tower->towerType = towerType; 205 tower->cooldown = 0.0f; 206 tower->damage = 0.0f; 207 return tower; 208 } 209 210 Tower *GetTowerByType(uint8_t towerType) 211 { 212 for (int i = 0; i < towerCount; i++) 213 { 214 if (towers[i].towerType == towerType) 215 { 216 return &towers[i]; 217 } 218 } 219 return 0; 220 } 221 222 int GetTowerCosts(uint8_t towerType) 223 { 224 return towerTypeConfigs[towerType].cost; 225 } 226 227 float TowerGetMaxHealth(Tower *tower) 228 { 229 return towerTypeConfigs[tower->towerType].maxHealth; 230 } 231 232 void TowerDraw() 233 { 234 for (int i = 0; i < towerCount; i++) 235 { 236 Tower tower = towers[i]; 237 if (tower.towerType == TOWER_TYPE_NONE) 238 { 239 continue; 240 } 241 242 switch (tower.towerType) 243 { 244 case TOWER_TYPE_ARCHER: 245 { 246 Vector2 screenPosTower = GetWorldToScreen((Vector3){tower.x, 0.0f, tower.y}, currentLevel->camera); 247 Vector2 screenPosTarget = GetWorldToScreen((Vector3){tower.lastTargetPosition.x, 0.0f, tower.lastTargetPosition.y}, currentLevel->camera); 248 DrawModel(towerModels[TOWER_TYPE_WALL], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE); 249 DrawSpriteUnit(archerUnit, (Vector3){tower.x, 1.0f, tower.y}, 0, screenPosTarget.x > screenPosTower.x, 250 tower.cooldown > 0.2f ? SPRITE_UNIT_PHASE_WEAPON_COOLDOWN : SPRITE_UNIT_PHASE_WEAPON_IDLE); 251 } 252 break; 253 case TOWER_TYPE_BALLISTA: 254 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, BROWN); 255 break; 256 case TOWER_TYPE_CATAPULT: 257 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, DARKGRAY); 258 break; 259 default: 260 if (towerModels[tower.towerType].materials) 261 { 262 DrawModel(towerModels[tower.towerType], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE); 263 } else { 264 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY); 265 } 266 break; 267 } 268 } 269 } 270 271 void TowerUpdate() 272 { 273 for (int i = 0; i < towerCount; i++) 274 { 275 Tower *tower = &towers[i]; 276 switch (tower->towerType) 277 { 278 case TOWER_TYPE_CATAPULT: 279 case TOWER_TYPE_BALLISTA: 280 case TOWER_TYPE_ARCHER: 281 TowerGunUpdate(tower); 282 break; 283 } 284 } 285 } 286 287 void TowerDrawHealthBars(Camera3D camera) 288 { 289 for (int i = 0; i < towerCount; i++) 290 { 291 Tower *tower = &towers[i]; 292 if (tower->towerType == TOWER_TYPE_NONE || tower->damage <= 0.0f) 293 { 294 continue; 295 } 296 297 Vector3 position = (Vector3){tower->x, 0.5f, tower->y}; 298 float maxHealth = TowerGetMaxHealth(tower); 299 float health = maxHealth - tower->damage; 300 float healthRatio = health / maxHealth; 301 302 DrawHealthBar(camera, position, healthRatio, GREEN, 35.0f); 303 } 304 }
  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 } Particle;
 28 
 29 #define TOWER_MAX_COUNT 400
 30 enum TowerType
 31 {
 32   TOWER_TYPE_NONE,
 33   TOWER_TYPE_BASE,
 34   TOWER_TYPE_ARCHER,
 35   TOWER_TYPE_BALLISTA,
 36   TOWER_TYPE_CATAPULT,
 37   TOWER_TYPE_WALL,
 38   TOWER_TYPE_COUNT
 39 };
 40 
 41 typedef struct TowerTypeConfig
 42 {
 43   float cooldown;
 44   float damage;
 45   float range;
 46   float areaDamageRadius;
 47   float projectileSpeed;
 48   uint8_t cost;
 49   uint8_t projectileType;
 50   uint16_t maxHealth;
 51 } TowerTypeConfig;
 52 
 53 typedef struct Tower
 54 {
 55   int16_t x, y;
 56   uint8_t towerType;
 57   Vector2 lastTargetPosition;
 58   float cooldown;
 59   float damage;
 60 } Tower;
 61 
 62 typedef struct GameTime
 63 {
 64   float time;
 65   float deltaTime;
 66 } GameTime;
 67 
 68 typedef struct ButtonState {
 69   char isSelected;
 70   char isDisabled;
 71 } ButtonState;
 72 
 73 typedef struct GUIState {
 74   int isBlocked;
 75 } GUIState;
 76 
 77 typedef enum LevelState
 78 {
 79   LEVEL_STATE_NONE,
 80   LEVEL_STATE_BUILDING,
 81   LEVEL_STATE_BATTLE,
 82   LEVEL_STATE_WON_WAVE,
 83   LEVEL_STATE_LOST_WAVE,
 84   LEVEL_STATE_WON_LEVEL,
 85   LEVEL_STATE_RESET,
 86 } LevelState;
 87 
 88 typedef struct EnemyWave {
 89   uint8_t enemyType;
 90   uint8_t wave;
 91   uint16_t count;
 92   float interval;
 93   float delay;
 94   Vector2 spawnPosition;
 95 
 96   uint16_t spawned;
 97   float timeToSpawnNext;
 98 } EnemyWave;
 99 
100 typedef struct Level
101 {
102   int seed;
103   LevelState state;
104   LevelState nextState;
105   Camera3D camera;
106   int placementMode;
107 
108   int initialGold;
109   int playerGold;
110 
111   EnemyWave waves[10];
112   int currentWave;
113   float waveEndTimer;
114 } Level;
115 
116 typedef struct DeltaSrc
117 {
118   char x, y;
119 } DeltaSrc;
120 
121 typedef struct PathfindingMap
122 {
123   int width, height;
124   float scale;
125   float *distances;
126   long *towerIndex; 
127   DeltaSrc *deltaSrc;
128   float maxDistance;
129   Matrix toMapSpace;
130   Matrix toWorldSpace;
131 } PathfindingMap;
132 
133 // when we execute the pathfinding algorithm, we need to store the active nodes
134 // in a queue. Each node has a position, a distance from the start, and the
135 // position of the node that we came from.
136 typedef struct PathfindingNode
137 {
138   int16_t x, y, fromX, fromY;
139   float distance;
140 } PathfindingNode;
141 
142 typedef struct EnemyId
143 {
144   uint16_t index;
145   uint16_t generation;
146 } EnemyId;
147 
148 typedef struct EnemyClassConfig
149 {
150   float speed;
151   float health;
152   float radius;
153   float maxAcceleration;
154   float requiredContactTime;
155   float explosionDamage;
156   float explosionRange;
157   float explosionPushbackPower;
158   int goldValue;
159 } EnemyClassConfig;
160 
161 typedef struct Enemy
162 {
163   int16_t currentX, currentY;
164   int16_t nextX, nextY;
165   Vector2 simPosition;
166   Vector2 simVelocity;
167   uint16_t generation;
168   float walkedDistance;
169   float startMovingTime;
170   float damage, futureDamage;
171   float contactTime;
172   uint8_t enemyType;
173   uint8_t movePathCount;
174   Vector2 movePath[ENEMY_MAX_PATH_COUNT];
175 } Enemy;
176 
177 // a unit that uses sprites to be drawn
178 #define SPRITE_UNIT_PHASE_WEAPON_IDLE 0
179 #define SPRITE_UNIT_PHASE_WEAPON_COOLDOWN 1
180 typedef struct SpriteUnit
181 {
182   Rectangle srcRect;
183   Vector2 offset;
184   int frameCount;
185   float frameDuration;
186   Rectangle srcWeaponIdleRect;
187   Vector2 srcWeaponIdleOffset;
188   Rectangle srcWeaponCooldownRect;
189   Vector2 srcWeaponCooldownOffset;
190 } SpriteUnit;
191 
192 #define PROJECTILE_MAX_COUNT 1200
193 #define PROJECTILE_TYPE_NONE 0
194 #define PROJECTILE_TYPE_ARROW 1
195 #define PROJECTILE_TYPE_CATAPULT 2 196 #define PROJECTILE_TYPE_BALLISTA 3
197 198 typedef struct Projectile 199 { 200 uint8_t projectileType; 201 float shootTime; 202 float arrivalTime; 203 float distance; 204 float damage; 205 Vector3 position; 206 Vector3 target; 207 Vector3 directionNormal; 208 EnemyId targetEnemy; 209 } Projectile; 210 211 //# Function declarations 212 float TowerGetMaxHealth(Tower *tower); 213 int Button(const char *text, int x, int y, int width, int height, ButtonState *state); 214 int EnemyAddDamage(Enemy *enemy, float damage); 215 216 //# Enemy functions 217 void EnemyInit(); 218 void EnemyDraw(); 219 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource); 220 void EnemyUpdate(); 221 float EnemyGetCurrentMaxSpeed(Enemy *enemy); 222 float EnemyGetMaxHealth(Enemy *enemy); 223 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY); 224 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount); 225 EnemyId EnemyGetId(Enemy *enemy); 226 Enemy *EnemyTryResolve(EnemyId enemyId); 227 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY); 228 int EnemyAddDamage(Enemy *enemy, float damage); 229 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range); 230 int EnemyCount(); 231 void EnemyDrawHealthbars(Camera3D camera); 232 233 //# Tower functions 234 void TowerInit(); 235 Tower *TowerGetAt(int16_t x, int16_t y); 236 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y); 237 Tower *GetTowerByType(uint8_t towerType); 238 int GetTowerCosts(uint8_t towerType); 239 float TowerGetMaxHealth(Tower *tower); 240 void TowerDraw(); 241 void TowerUpdate(); 242 void TowerDrawHealthBars(Camera3D camera); 243 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase); 244 245 //# Particles 246 void ParticleInit(); 247 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, float lifetime); 248 void ParticleUpdate(); 249 void ParticleDraw(); 250 251 //# Projectiles 252 void ProjectileInit(); 253 void ProjectileDraw(); 254 void ProjectileUpdate(); 255 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, float damage); 256 257 //# Pathfinding map 258 void PathfindingMapInit(int width, int height, Vector3 translate, float scale); 259 float PathFindingGetDistance(int mapX, int mapY); 260 Vector2 PathFindingGetGradient(Vector3 world); 261 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY); 262 void PathFindingMapUpdate(); 263 void PathFindingMapDraw(); 264 265 //# UI 266 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth); 267 268 //# Level 269 void DrawLevelGround(Level *level); 270 271 //# variables 272 extern Level *currentLevel; 273 extern Enemy enemies[ENEMY_MAX_COUNT]; 274 extern int enemyCount; 275 extern EnemyClassConfig enemyClassConfigs[]; 276 277 extern GUIState guiState; 278 extern GameTime gameTime; 279 extern Tower towers[TOWER_MAX_COUNT]; 280 extern int towerCount; 281 282 extern Texture2D palette, spriteSheet; 283 284 #endif
  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) 92 { 93 EnemyAddDamage(enemy, projectile->damage); 94 } 95 continue; 96 } 97 } 98 } 99 100 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, float damage) 101 { 102 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++) 103 { 104 Projectile *projectile = &projectiles[i]; 105 if (projectile->projectileType == PROJECTILE_TYPE_NONE) 106 { 107 projectile->projectileType = projectileType; 108 projectile->shootTime = gameTime.time; 109 float distance = Vector3Distance(position, target); 110 projectile->arrivalTime = gameTime.time + distance / speed; 111 projectile->damage = damage; 112 projectile->position = position; 113 projectile->target = target; 114 projectile->directionNormal = Vector3Scale(Vector3Subtract(target, position), 1.0f / distance); 115 projectile->distance = distance; 116 projectile->targetEnemy = EnemyGetId(enemy); 117 projectileCount = projectileCount <= i ? i + 1 : projectileCount; 118 return projectile; 119 } 120 } 121 return 0; 122 }
  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}, 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 EnemyAddDamage(Enemy *enemy, float damage)
444 {
445   enemy->damage += damage;
446   if (enemy->damage >= EnemyGetMaxHealth(enemy))
447   {
448     currentLevel->playerGold += enemyClassConfigs[enemy->enemyType].goldValue;
449     enemy->enemyType = ENEMY_TYPE_NONE;
450     return 1;
451   }
452 
453   return 0;
454 }
455 
456 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
457 {
458   int16_t castleX = 0;
459   int16_t castleY = 0;
460   Enemy* closest = 0;
461   int16_t closestDistance = 0;
462   float range2 = range * range;
463   for (int i = 0; i < enemyCount; i++)
464   {
465     Enemy* enemy = &enemies[i];
466     if (enemy->enemyType == ENEMY_TYPE_NONE)
467     {
468       continue;
469     }
470     float maxHealth = EnemyGetMaxHealth(enemy);
471     if (enemy->futureDamage >= maxHealth)
472     {
473       // ignore enemies that will die soon
474       continue;
475     }
476     int16_t dx = castleX - enemy->currentX;
477     int16_t dy = castleY - enemy->currentY;
478     int16_t distance = abs(dx) + abs(dy);
479     if (!closest || distance < closestDistance)
480     {
481       float tdx = towerX - enemy->currentX;
482       float tdy = towerY - enemy->currentY;
483       float tdistance2 = tdx * tdx + tdy * tdy;
484       if (tdistance2 <= range2)
485       {
486         closest = enemy;
487         closestDistance = distance;
488       }
489     }
490   }
491   return closest;
492 }
493 
494 int EnemyCount()
495 {
496   int count = 0;
497   for (int i = 0; i < enemyCount; i++)
498   {
499     if (enemies[i].enemyType != ENEMY_TYPE_NONE)
500     {
501       count++;
502     }
503   }
504   return count;
505 }
506 
507 void EnemyDrawHealthbars(Camera3D camera)
508 {
509   for (int i = 0; i < enemyCount; i++)
510   {
511     Enemy *enemy = &enemies[i];
512     if (enemy->enemyType == ENEMY_TYPE_NONE || enemy->damage == 0.0f)
513     {
514       continue;
515     }
516     Vector3 position = (Vector3){enemy->simPosition.x, 0.5f, enemy->simPosition.y};
517     float maxHealth = EnemyGetMaxHealth(enemy);
518     float health = maxHealth - enemy->damage;
519     float healthRatio = health / maxHealth;
520     
521     DrawHealthBar(camera, position, healthRatio, GREEN, 15.0f);
522   }
523 }
  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 Particle particles[PARTICLE_MAX_COUNT];
  5 static int particleCount = 0;
  6 
  7 void ParticleInit()
  8 {
  9   for (int i = 0; i < PARTICLE_MAX_COUNT; i++)
 10   {
 11     particles[i] = (Particle){0};
 12   }
 13   particleCount = 0;
 14 }
 15 
 16 static void DrawExplosionParticle(Particle *particle, float transition)
 17 {
 18   float size = 1.2f * (1.0f - transition);
 19   Color startColor = WHITE;
 20   Color endColor = RED;
 21   Color color = ColorLerp(startColor, endColor, transition);
 22   DrawCube(particle->position, size, size, size, color);
 23 }
 24 
 25 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, float lifetime)
 26 {
 27   if (particleCount >= PARTICLE_MAX_COUNT)
 28   {
 29     return;
 30   }
 31 
 32   int index = -1;
 33   for (int i = 0; i < particleCount; i++)
 34   {
 35     if (particles[i].particleType == PARTICLE_TYPE_NONE)
 36     {
 37       index = i;
 38       break;
 39     }
 40   }
 41 
 42   if (index == -1)
 43   {
 44     index = particleCount++;
 45   }
 46 
 47   Particle *particle = &particles[index];
 48   particle->particleType = particleType;
 49   particle->spawnTime = gameTime.time;
 50   particle->lifetime = lifetime;
 51   particle->position = position;
 52   particle->velocity = velocity;
 53 }
 54 
 55 void ParticleUpdate()
 56 {
 57   for (int i = 0; i < particleCount; i++)
 58   {
 59     Particle *particle = &particles[i];
 60     if (particle->particleType == PARTICLE_TYPE_NONE)
 61     {
 62       continue;
 63     }
 64 
 65     float age = gameTime.time - particle->spawnTime;
 66 
 67     if (particle->lifetime > age)
 68     {
 69       particle->position = Vector3Add(particle->position, Vector3Scale(particle->velocity, gameTime.deltaTime));
 70     }
 71     else {
 72       particle->particleType = PARTICLE_TYPE_NONE;
 73     }
 74   }
 75 }
 76 
 77 void ParticleDraw()
 78 {
 79   for (int i = 0; i < particleCount; i++)
 80   {
 81     Particle particle = particles[i];
 82     if (particle.particleType == PARTICLE_TYPE_NONE)
 83     {
 84       continue;
 85     }
 86 
 87     float age = gameTime.time - particle.spawnTime;
 88     float transition = age / particle.lifetime;
 89     switch (particle.particleType)
 90     {
 91     case PARTICLE_TYPE_EXPLOSION:
 92       DrawExplosionParticle(&particle, transition);
 93       break;
 94     default:
 95       DrawCube(particle.position, 0.3f, 0.5f, 0.3f, RED);
 96       break;
 97     }
 98   }
 99 }
  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 two new towers should now also add another effect to the enemies:

To achieve these effects, we need to trigger these effects in the projectile update function that is currently only handling the damage to the enemies.

When we set up the tower configuration, we already added an area damage range value to that structure in anticipation of this. However, at the moment the projectile that deals the damage does not know about the tower that shot it or its config values.

It does feel right to specify these parameters in the tower configuration. So how do we pass this information to the place where the hit happens?

I personally find the last point most convincing and will do that - but as always, there are many solutions and it is difficult to foresee which solution will be the best bet. With a hit effect configuration, we can create building level dependent variants when needed and pass the it as value so the projectile system has a copy of the configuration.

  • 💾
  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 DrawLevelBuildingState(Level *level)
287 {
288   BeginMode3D(level->camera);
289   DrawLevelGround(level);
290   TowerDraw();
291   EnemyDraw();
292   ProjectileDraw();
293   ParticleDraw();
294 
295   Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera);
296   float planeDistance = ray.position.y / -ray.direction.y;
297   float planeX = ray.direction.x * planeDistance + ray.position.x;
298   float planeY = ray.direction.z * planeDistance + ray.position.z;
299   int16_t mapX = (int16_t)floorf(planeX + 0.5f);
300   int16_t mapY = (int16_t)floorf(planeY + 0.5f);
301   if (level->placementMode && !guiState.isBlocked && mapX >= -5 && mapX <= 5 && mapY >= -5 && mapY <= 5)
302   {
303     DrawCubeWires((Vector3){mapX, 0.2f, mapY}, 1.0f, 0.4f, 1.0f, RED);
304     if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
305     {
306       if (TowerTryAdd(level->placementMode, mapX, mapY))
307       {
308         level->playerGold -= GetTowerCosts(level->placementMode);
309         level->placementMode = TOWER_TYPE_NONE;
310       }
311     }
312   }
313 
314   guiState.isBlocked = 0;
315 
316   EndMode3D();
317 
318   TowerDrawHealthBars(level->camera);
319 
320   static ButtonState buildWallButtonState = {0};
321   static ButtonState buildGunButtonState = {0};
322   buildWallButtonState.isSelected = level->placementMode == TOWER_TYPE_WALL;
323   buildGunButtonState.isSelected = level->placementMode == TOWER_TYPE_ARCHER;
324 
325   DrawBuildingBuildButton(level, 10, 10, 110, 30, TOWER_TYPE_WALL, "Wall");
326   DrawBuildingBuildButton(level, 10, 50, 110, 30, TOWER_TYPE_ARCHER, "Archer");
327   DrawBuildingBuildButton(level, 10, 90, 110, 30, TOWER_TYPE_BALLISTA, "Ballista");
328   DrawBuildingBuildButton(level, 10, 130, 110, 30, TOWER_TYPE_CATAPULT, "Catapult");
329 
330   if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
331   {
332     level->nextState = LEVEL_STATE_RESET;
333   }
334   
335   if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0))
336   {
337     level->nextState = LEVEL_STATE_BATTLE;
338   }
339 
340   const char *text = "Building phase";
341   int textWidth = MeasureText(text, 20);
342   DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
343 }
344 
345 void InitBattleStateConditions(Level *level)
346 {
347   level->state = LEVEL_STATE_BATTLE;
348   level->nextState = LEVEL_STATE_NONE;
349   level->waveEndTimer = 0.0f;
350   for (int i = 0; i < 10; i++)
351   {
352     EnemyWave *wave = &level->waves[i];
353     wave->spawned = 0;
354     wave->timeToSpawnNext = wave->delay;
355   }
356 }
357 
358 void DrawLevelBattleState(Level *level)
359 {
360   BeginMode3D(level->camera);
361   DrawLevelGround(level);
362   TowerDraw();
363   EnemyDraw();
364   ProjectileDraw();
365   ParticleDraw();
366   guiState.isBlocked = 0;
367   EndMode3D();
368 
369   EnemyDrawHealthbars(level->camera);
370   TowerDrawHealthBars(level->camera);
371 
372   if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
373   {
374     level->nextState = LEVEL_STATE_RESET;
375   }
376 
377   int maxCount = 0;
378   int remainingCount = 0;
379   for (int i = 0; i < 10; i++)
380   {
381     EnemyWave *wave = &level->waves[i];
382     if (wave->wave != level->currentWave)
383     {
384       continue;
385     }
386     maxCount += wave->count;
387     remainingCount += wave->count - wave->spawned;
388   }
389   int aliveCount = EnemyCount();
390   remainingCount += aliveCount;
391 
392   const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount);
393   int textWidth = MeasureText(text, 20);
394   DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
395 }
396 
397 void DrawLevel(Level *level)
398 {
399   switch (level->state)
400   {
401     case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break;
402     case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break;
403     case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break;
404     case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break;
405     default: break;
406   }
407 
408   DrawLevelHud(level);
409 }
410 
411 void UpdateLevel(Level *level)
412 {
413   if (level->state == LEVEL_STATE_BATTLE)
414   {
415     int activeWaves = 0;
416     for (int i = 0; i < 10; i++)
417     {
418       EnemyWave *wave = &level->waves[i];
419       if (wave->spawned >= wave->count || wave->wave != level->currentWave)
420       {
421         continue;
422       }
423       activeWaves++;
424       wave->timeToSpawnNext -= gameTime.deltaTime;
425       if (wave->timeToSpawnNext <= 0.0f)
426       {
427         Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y);
428         if (enemy)
429         {
430           wave->timeToSpawnNext = wave->interval;
431           wave->spawned++;
432         }
433       }
434     }
435     if (GetTowerByType(TOWER_TYPE_BASE) == 0) {
436       level->waveEndTimer += gameTime.deltaTime;
437       if (level->waveEndTimer >= 2.0f)
438       {
439         level->nextState = LEVEL_STATE_LOST_WAVE;
440       }
441     }
442     else if (activeWaves == 0 && EnemyCount() == 0)
443     {
444       level->waveEndTimer += gameTime.deltaTime;
445       if (level->waveEndTimer >= 2.0f)
446       {
447         level->nextState = LEVEL_STATE_WON_WAVE;
448       }
449     }
450   }
451 
452   PathFindingMapUpdate();
453   EnemyUpdate();
454   TowerUpdate();
455   ProjectileUpdate();
456   ParticleUpdate();
457 
458   if (level->nextState == LEVEL_STATE_RESET)
459   {
460     InitLevel(level);
461   }
462   
463   if (level->nextState == LEVEL_STATE_BATTLE)
464   {
465     InitBattleStateConditions(level);
466   }
467   
468   if (level->nextState == LEVEL_STATE_WON_WAVE)
469   {
470     level->currentWave++;
471     level->state = LEVEL_STATE_WON_WAVE;
472   }
473   
474   if (level->nextState == LEVEL_STATE_LOST_WAVE)
475   {
476     level->state = LEVEL_STATE_LOST_WAVE;
477   }
478 
479   if (level->nextState == LEVEL_STATE_BUILDING)
480   {
481     level->state = LEVEL_STATE_BUILDING;
482   }
483 
484   if (level->nextState == LEVEL_STATE_WON_LEVEL)
485   {
486     // make something of this later
487     InitLevel(level);
488   }
489 
490   level->nextState = LEVEL_STATE_NONE;
491 }
492 
493 float nextSpawnTime = 0.0f;
494 
495 void ResetGame()
496 {
497   InitLevel(currentLevel);
498 }
499 
500 void InitGame()
501 {
502   TowerInit();
503   EnemyInit();
504   ProjectileInit();
505   ParticleInit();
506   PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f);
507 
508   currentLevel = levels;
509   InitLevel(currentLevel);
510 }
511 
512 //# Immediate GUI functions
513 
514 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth)
515 {
516   const float healthBarHeight = 6.0f;
517   const float healthBarOffset = 15.0f;
518   const float inset = 2.0f;
519   const float innerWidth = healthBarWidth - inset * 2;
520   const float innerHeight = healthBarHeight - inset * 2;
521 
522   Vector2 screenPos = GetWorldToScreen(position, camera);
523   float centerX = screenPos.x - healthBarWidth * 0.5f;
524   float topY = screenPos.y - healthBarOffset;
525   DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK);
526   float healthWidth = innerWidth * healthRatio;
527   DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor);
528 }
529 
530 int Button(const char *text, int x, int y, int width, int height, ButtonState *state)
531 {
532   Rectangle bounds = {x, y, width, height};
533   int isPressed = 0;
534   int isSelected = state && state->isSelected;
535   int isDisabled = state && state->isDisabled;
536   if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !isDisabled)
537   {
538     Color color = isSelected ? DARKGRAY : GRAY;
539     DrawRectangle(x, y, width, height, color);
540     if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
541     {
542       isPressed = 1;
543     }
544     guiState.isBlocked = 1;
545   }
546   else
547   {
548     Color color = isSelected ? WHITE : LIGHTGRAY;
549     DrawRectangle(x, y, width, height, color);
550   }
551   Font font = GetFontDefault();
552   Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1);
553   Color textColor = isDisabled ? GRAY : BLACK;
554   DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, textColor);
555   return isPressed;
556 }
557 
558 //# Main game loop
559 
560 void GameUpdate()
561 {
562   float dt = GetFrameTime();
563   // cap maximum delta time to 0.1 seconds to prevent large time steps
564   if (dt > 0.1f) dt = 0.1f;
565   gameTime.time += dt;
566   gameTime.deltaTime = dt;
567 
568   UpdateLevel(currentLevel);
569 }
570 
571 int main(void)
572 {
573   int screenWidth, screenHeight;
574   GetPreferredSize(&screenWidth, &screenHeight);
575   InitWindow(screenWidth, screenHeight, "Tower defense");
576   SetTargetFPS(30);
577 
578   LoadAssets();
579   InitGame();
580 
581   while (!WindowShouldClose())
582   {
583     if (IsPaused()) {
584       // canvas is not visible in browser - do nothing
585       continue;
586     }
587 
588     BeginDrawing();
589     ClearBackground((Color){0x4E, 0x63, 0x26, 0xFF});
590 
591     GameUpdate();
592     DrawLevel(currentLevel);
593 
594     EndDrawing();
595   }
596 
597   CloseWindow();
598 
599   return 0;
600 }
  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 } Particle;
 28 
 29 #define TOWER_MAX_COUNT 400
 30 enum TowerType
 31 {
 32   TOWER_TYPE_NONE,
 33   TOWER_TYPE_BASE,
 34   TOWER_TYPE_ARCHER,
 35   TOWER_TYPE_BALLISTA,
 36   TOWER_TYPE_CATAPULT,
 37   TOWER_TYPE_WALL,
 38   TOWER_TYPE_COUNT
 39 };
 40 
41 typedef struct HitEffectConfig 42 { 43 float damage; 44 float areaDamageRadius; 45 float pushbackPowerDistance; 46 } HitEffectConfig; 47
48 typedef struct TowerTypeConfig 49 { 50 float cooldown;
51 float range; 52 float projectileSpeed;
53 54 uint8_t cost;
55 uint8_t projectileType; 56 uint16_t maxHealth; 57
58 HitEffectConfig hitEffect; 59 } TowerTypeConfig; 60 61 typedef struct Tower 62 { 63 int16_t x, y; 64 uint8_t towerType; 65 Vector2 lastTargetPosition; 66 float cooldown; 67 float damage; 68 } Tower; 69 70 typedef struct GameTime 71 { 72 float time; 73 float deltaTime; 74 } GameTime; 75 76 typedef struct ButtonState { 77 char isSelected; 78 char isDisabled; 79 } ButtonState; 80 81 typedef struct GUIState { 82 int isBlocked; 83 } GUIState; 84 85 typedef enum LevelState 86 { 87 LEVEL_STATE_NONE, 88 LEVEL_STATE_BUILDING, 89 LEVEL_STATE_BATTLE, 90 LEVEL_STATE_WON_WAVE, 91 LEVEL_STATE_LOST_WAVE, 92 LEVEL_STATE_WON_LEVEL, 93 LEVEL_STATE_RESET, 94 } LevelState; 95 96 typedef struct EnemyWave { 97 uint8_t enemyType; 98 uint8_t wave; 99 uint16_t count; 100 float interval; 101 float delay; 102 Vector2 spawnPosition; 103 104 uint16_t spawned; 105 float timeToSpawnNext; 106 } EnemyWave; 107 108 typedef struct Level 109 { 110 int seed; 111 LevelState state; 112 LevelState nextState; 113 Camera3D camera; 114 int placementMode; 115 116 int initialGold; 117 int playerGold; 118 119 EnemyWave waves[10]; 120 int currentWave; 121 float waveEndTimer; 122 } Level; 123 124 typedef struct DeltaSrc 125 { 126 char x, y; 127 } DeltaSrc; 128 129 typedef struct PathfindingMap 130 { 131 int width, height; 132 float scale; 133 float *distances; 134 long *towerIndex; 135 DeltaSrc *deltaSrc; 136 float maxDistance; 137 Matrix toMapSpace; 138 Matrix toWorldSpace; 139 } PathfindingMap; 140 141 // when we execute the pathfinding algorithm, we need to store the active nodes 142 // in a queue. Each node has a position, a distance from the start, and the 143 // position of the node that we came from. 144 typedef struct PathfindingNode 145 { 146 int16_t x, y, fromX, fromY; 147 float distance; 148 } PathfindingNode; 149 150 typedef struct EnemyId 151 { 152 uint16_t index; 153 uint16_t generation; 154 } EnemyId; 155 156 typedef struct EnemyClassConfig 157 { 158 float speed; 159 float health; 160 float radius; 161 float maxAcceleration; 162 float requiredContactTime; 163 float explosionDamage; 164 float explosionRange; 165 float explosionPushbackPower; 166 int goldValue; 167 } EnemyClassConfig; 168 169 typedef struct Enemy 170 { 171 int16_t currentX, currentY; 172 int16_t nextX, nextY; 173 Vector2 simPosition; 174 Vector2 simVelocity; 175 uint16_t generation; 176 float walkedDistance; 177 float startMovingTime; 178 float damage, futureDamage; 179 float contactTime; 180 uint8_t enemyType; 181 uint8_t movePathCount; 182 Vector2 movePath[ENEMY_MAX_PATH_COUNT]; 183 } Enemy; 184 185 // a unit that uses sprites to be drawn 186 #define SPRITE_UNIT_PHASE_WEAPON_IDLE 0 187 #define SPRITE_UNIT_PHASE_WEAPON_COOLDOWN 1 188 typedef struct SpriteUnit 189 { 190 Rectangle srcRect; 191 Vector2 offset; 192 int frameCount; 193 float frameDuration; 194 Rectangle srcWeaponIdleRect; 195 Vector2 srcWeaponIdleOffset; 196 Rectangle srcWeaponCooldownRect; 197 Vector2 srcWeaponCooldownOffset; 198 } SpriteUnit; 199 200 #define PROJECTILE_MAX_COUNT 1200 201 #define PROJECTILE_TYPE_NONE 0 202 #define PROJECTILE_TYPE_ARROW 1 203 #define PROJECTILE_TYPE_CATAPULT 2 204 #define PROJECTILE_TYPE_BALLISTA 3 205 206 typedef struct Projectile 207 { 208 uint8_t projectileType;
209 float shootTime; 210 float arrivalTime;
211 float distance; 212 Vector3 position;
213 Vector3 target; 214 Vector3 directionNormal;
215 EnemyId targetEnemy; 216 HitEffectConfig hitEffectConfig; 217 } Projectile; 218
219 //# Function declarations 220 float TowerGetMaxHealth(Tower *tower);
221 int Button(const char *text, int x, int y, int width, int height, ButtonState *state); 222 int EnemyAddDamageRange(Vector2 position, float range, float damage); 223 int EnemyAddDamage(Enemy *enemy, float damage); 224 225 //# Enemy functions 226 void EnemyInit(); 227 void EnemyDraw(); 228 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource); 229 void EnemyUpdate(); 230 float EnemyGetCurrentMaxSpeed(Enemy *enemy); 231 float EnemyGetMaxHealth(Enemy *enemy); 232 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY); 233 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount); 234 EnemyId EnemyGetId(Enemy *enemy); 235 Enemy *EnemyTryResolve(EnemyId enemyId); 236 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY); 237 int EnemyAddDamage(Enemy *enemy, float damage); 238 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range); 239 int EnemyCount(); 240 void EnemyDrawHealthbars(Camera3D camera); 241 242 //# Tower functions 243 void TowerInit(); 244 Tower *TowerGetAt(int16_t x, int16_t y); 245 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y); 246 Tower *GetTowerByType(uint8_t towerType); 247 int GetTowerCosts(uint8_t towerType); 248 float TowerGetMaxHealth(Tower *tower); 249 void TowerDraw(); 250 void TowerUpdate(); 251 void TowerDrawHealthBars(Camera3D camera); 252 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase); 253 254 //# Particles 255 void ParticleInit(); 256 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, float lifetime); 257 void ParticleUpdate(); 258 void ParticleDraw(); 259 260 //# Projectiles 261 void ProjectileInit();
262 void ProjectileDraw();
263 void ProjectileUpdate(); 264 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig); 265 266 //# Pathfinding map 267 void PathfindingMapInit(int width, int height, Vector3 translate, float scale); 268 float PathFindingGetDistance(int mapX, int mapY); 269 Vector2 PathFindingGetGradient(Vector3 world); 270 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY); 271 void PathFindingMapUpdate(); 272 void PathFindingMapDraw(); 273 274 //# UI 275 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth); 276 277 //# Level 278 void DrawLevelGround(Level *level); 279 280 //# variables 281 extern Level *currentLevel; 282 extern Enemy enemies[ENEMY_MAX_COUNT]; 283 extern int enemyCount; 284 extern EnemyClassConfig enemyClassConfigs[]; 285 286 extern GUIState guiState; 287 extern GameTime gameTime; 288 extern Tower towers[TOWER_MAX_COUNT]; 289 extern int towerCount; 290 291 extern Texture2D palette, spriteSheet; 292 293 #endif
  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 } 101 else if (projectile->hitEffectConfig.damage > 0.0f && enemy) 102 { 103 EnemyAddDamage(enemy, projectile->hitEffectConfig.damage);
104 } 105 continue; 106 } 107 } 108 } 109
110 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig)
111 { 112 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++) 113 { 114 Projectile *projectile = &projectiles[i]; 115 if (projectile->projectileType == PROJECTILE_TYPE_NONE) 116 { 117 projectile->projectileType = projectileType; 118 projectile->shootTime = gameTime.time;
119 float distance = Vector3Distance(position, target); 120 projectile->arrivalTime = gameTime.time + distance / speed;
121 projectile->position = position; 122 projectile->target = target; 123 projectile->directionNormal = Vector3Scale(Vector3Subtract(target, position), 1.0f / distance); 124 projectile->distance = distance;
125 projectile->targetEnemy = EnemyGetId(enemy); 126 projectileCount = projectileCount <= i ? i + 1 : projectileCount;
127 projectile->hitEffectConfig = hitEffectConfig; 128 return projectile; 129 } 130 } 131 return 0; 132 }
  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}, 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 Particle particles[PARTICLE_MAX_COUNT];
  5 static int particleCount = 0;
  6 
  7 void ParticleInit()
  8 {
  9   for (int i = 0; i < PARTICLE_MAX_COUNT; i++)
 10   {
 11     particles[i] = (Particle){0};
 12   }
 13   particleCount = 0;
 14 }
 15 
 16 static void DrawExplosionParticle(Particle *particle, float transition)
 17 {
 18   float size = 1.2f * (1.0f - transition);
 19   Color startColor = WHITE;
 20   Color endColor = RED;
 21   Color color = ColorLerp(startColor, endColor, transition);
 22   DrawCube(particle->position, size, size, size, color);
 23 }
 24 
 25 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, float lifetime)
 26 {
 27   if (particleCount >= PARTICLE_MAX_COUNT)
 28   {
 29     return;
 30   }
 31 
 32   int index = -1;
 33   for (int i = 0; i < particleCount; i++)
 34   {
 35     if (particles[i].particleType == PARTICLE_TYPE_NONE)
 36     {
 37       index = i;
 38       break;
 39     }
 40   }
 41 
 42   if (index == -1)
 43   {
 44     index = particleCount++;
 45   }
 46 
 47   Particle *particle = &particles[index];
 48   particle->particleType = particleType;
 49   particle->spawnTime = gameTime.time;
 50   particle->lifetime = lifetime;
 51   particle->position = position;
 52   particle->velocity = velocity;
 53 }
 54 
 55 void ParticleUpdate()
 56 {
 57   for (int i = 0; i < particleCount; i++)
 58   {
 59     Particle *particle = &particles[i];
 60     if (particle->particleType == PARTICLE_TYPE_NONE)
 61     {
 62       continue;
 63     }
 64 
 65     float age = gameTime.time - particle->spawnTime;
 66 
 67     if (particle->lifetime > age)
 68     {
 69       particle->position = Vector3Add(particle->position, Vector3Scale(particle->velocity, gameTime.deltaTime));
 70     }
 71     else {
 72       particle->particleType = PARTICLE_TYPE_NONE;
 73     }
 74   }
 75 }
 76 
 77 void ParticleDraw()
 78 {
 79   for (int i = 0; i < particleCount; i++)
 80   {
 81     Particle particle = particles[i];
 82     if (particle.particleType == PARTICLE_TYPE_NONE)
 83     {
 84       continue;
 85     }
 86 
 87     float age = gameTime.time - particle.spawnTime;
 88     float transition = age / particle.lifetime;
 89     switch (particle.particleType)
 90     {
 91     case PARTICLE_TYPE_EXPLOSION:
 92       DrawExplosionParticle(&particle, transition);
 93       break;
 94     default:
 95       DrawCube(particle.position, 0.3f, 0.5f, 0.3f, RED);
 96       break;
 97     }
 98   }
 99 }
  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 catapult tower now has an area damage effect and the ballista is pushing enemies back.

It would now be nice if we could see the area effect of the catapult tower. So let's add a particle effect for the area damage with the size of the area upon impact.

  • 💾
  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 DrawLevelBuildingState(Level *level)
287 {
288   BeginMode3D(level->camera);
289   DrawLevelGround(level);
290   TowerDraw();
291   EnemyDraw();
292   ProjectileDraw();
293   ParticleDraw();
294 
295   Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera);
296   float planeDistance = ray.position.y / -ray.direction.y;
297   float planeX = ray.direction.x * planeDistance + ray.position.x;
298   float planeY = ray.direction.z * planeDistance + ray.position.z;
299   int16_t mapX = (int16_t)floorf(planeX + 0.5f);
300   int16_t mapY = (int16_t)floorf(planeY + 0.5f);
301   if (level->placementMode && !guiState.isBlocked && mapX >= -5 && mapX <= 5 && mapY >= -5 && mapY <= 5)
302   {
303     DrawCubeWires((Vector3){mapX, 0.2f, mapY}, 1.0f, 0.4f, 1.0f, RED);
304     if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
305     {
306       if (TowerTryAdd(level->placementMode, mapX, mapY))
307       {
308         level->playerGold -= GetTowerCosts(level->placementMode);
309         level->placementMode = TOWER_TYPE_NONE;
310       }
311     }
312   }
313 
314   guiState.isBlocked = 0;
315 
316   EndMode3D();
317 
318   TowerDrawHealthBars(level->camera);
319 
320   static ButtonState buildWallButtonState = {0};
321   static ButtonState buildGunButtonState = {0};
322   buildWallButtonState.isSelected = level->placementMode == TOWER_TYPE_WALL;
323   buildGunButtonState.isSelected = level->placementMode == TOWER_TYPE_ARCHER;
324 
325   DrawBuildingBuildButton(level, 10, 10, 110, 30, TOWER_TYPE_WALL, "Wall");
326   DrawBuildingBuildButton(level, 10, 50, 110, 30, TOWER_TYPE_ARCHER, "Archer");
327   DrawBuildingBuildButton(level, 10, 90, 110, 30, TOWER_TYPE_BALLISTA, "Ballista");
328   DrawBuildingBuildButton(level, 10, 130, 110, 30, TOWER_TYPE_CATAPULT, "Catapult");
329 
330   if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
331   {
332     level->nextState = LEVEL_STATE_RESET;
333   }
334   
335   if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0))
336   {
337     level->nextState = LEVEL_STATE_BATTLE;
338   }
339 
340   const char *text = "Building phase";
341   int textWidth = MeasureText(text, 20);
342   DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
343 }
344 
345 void InitBattleStateConditions(Level *level)
346 {
347   level->state = LEVEL_STATE_BATTLE;
348   level->nextState = LEVEL_STATE_NONE;
349   level->waveEndTimer = 0.0f;
350   for (int i = 0; i < 10; i++)
351   {
352     EnemyWave *wave = &level->waves[i];
353     wave->spawned = 0;
354     wave->timeToSpawnNext = wave->delay;
355   }
356 }
357 
358 void DrawLevelBattleState(Level *level)
359 {
360   BeginMode3D(level->camera);
361   DrawLevelGround(level);
362   TowerDraw();
363   EnemyDraw();
364   ProjectileDraw();
365   ParticleDraw();
366   guiState.isBlocked = 0;
367   EndMode3D();
368 
369   EnemyDrawHealthbars(level->camera);
370   TowerDrawHealthBars(level->camera);
371 
372   if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
373   {
374     level->nextState = LEVEL_STATE_RESET;
375   }
376 
377   int maxCount = 0;
378   int remainingCount = 0;
379   for (int i = 0; i < 10; i++)
380   {
381     EnemyWave *wave = &level->waves[i];
382     if (wave->wave != level->currentWave)
383     {
384       continue;
385     }
386     maxCount += wave->count;
387     remainingCount += wave->count - wave->spawned;
388   }
389   int aliveCount = EnemyCount();
390   remainingCount += aliveCount;
391 
392   const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount);
393   int textWidth = MeasureText(text, 20);
394   DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
395 }
396 
397 void DrawLevel(Level *level)
398 {
399   switch (level->state)
400   {
401     case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break;
402     case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break;
403     case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break;
404     case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break;
405     default: break;
406   }
407 
408   DrawLevelHud(level);
409 }
410 
411 void UpdateLevel(Level *level)
412 {
413   if (level->state == LEVEL_STATE_BATTLE)
414   {
415     int activeWaves = 0;
416     for (int i = 0; i < 10; i++)
417     {
418       EnemyWave *wave = &level->waves[i];
419       if (wave->spawned >= wave->count || wave->wave != level->currentWave)
420       {
421         continue;
422       }
423       activeWaves++;
424       wave->timeToSpawnNext -= gameTime.deltaTime;
425       if (wave->timeToSpawnNext <= 0.0f)
426       {
427         Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y);
428         if (enemy)
429         {
430           wave->timeToSpawnNext = wave->interval;
431           wave->spawned++;
432         }
433       }
434     }
435     if (GetTowerByType(TOWER_TYPE_BASE) == 0) {
436       level->waveEndTimer += gameTime.deltaTime;
437       if (level->waveEndTimer >= 2.0f)
438       {
439         level->nextState = LEVEL_STATE_LOST_WAVE;
440       }
441     }
442     else if (activeWaves == 0 && EnemyCount() == 0)
443     {
444       level->waveEndTimer += gameTime.deltaTime;
445       if (level->waveEndTimer >= 2.0f)
446       {
447         level->nextState = LEVEL_STATE_WON_WAVE;
448       }
449     }
450   }
451 
452   PathFindingMapUpdate();
453   EnemyUpdate();
454   TowerUpdate();
455   ProjectileUpdate();
456   ParticleUpdate();
457 
458   if (level->nextState == LEVEL_STATE_RESET)
459   {
460     InitLevel(level);
461   }
462   
463   if (level->nextState == LEVEL_STATE_BATTLE)
464   {
465     InitBattleStateConditions(level);
466   }
467   
468   if (level->nextState == LEVEL_STATE_WON_WAVE)
469   {
470     level->currentWave++;
471     level->state = LEVEL_STATE_WON_WAVE;
472   }
473   
474   if (level->nextState == LEVEL_STATE_LOST_WAVE)
475   {
476     level->state = LEVEL_STATE_LOST_WAVE;
477   }
478 
479   if (level->nextState == LEVEL_STATE_BUILDING)
480   {
481     level->state = LEVEL_STATE_BUILDING;
482   }
483 
484   if (level->nextState == LEVEL_STATE_WON_LEVEL)
485   {
486     // make something of this later
487     InitLevel(level);
488   }
489 
490   level->nextState = LEVEL_STATE_NONE;
491 }
492 
493 float nextSpawnTime = 0.0f;
494 
495 void ResetGame()
496 {
497   InitLevel(currentLevel);
498 }
499 
500 void InitGame()
501 {
502   TowerInit();
503   EnemyInit();
504   ProjectileInit();
505   ParticleInit();
506   PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f);
507 
508   currentLevel = levels;
509   InitLevel(currentLevel);
510 }
511 
512 //# Immediate GUI functions
513 
514 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth)
515 {
516   const float healthBarHeight = 6.0f;
517   const float healthBarOffset = 15.0f;
518   const float inset = 2.0f;
519   const float innerWidth = healthBarWidth - inset * 2;
520   const float innerHeight = healthBarHeight - inset * 2;
521 
522   Vector2 screenPos = GetWorldToScreen(position, camera);
523   float centerX = screenPos.x - healthBarWidth * 0.5f;
524   float topY = screenPos.y - healthBarOffset;
525   DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK);
526   float healthWidth = innerWidth * healthRatio;
527   DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor);
528 }
529 
530 int Button(const char *text, int x, int y, int width, int height, ButtonState *state)
531 {
532   Rectangle bounds = {x, y, width, height};
533   int isPressed = 0;
534   int isSelected = state && state->isSelected;
535   int isDisabled = state && state->isDisabled;
536   if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !isDisabled)
537   {
538     Color color = isSelected ? DARKGRAY : GRAY;
539     DrawRectangle(x, y, width, height, color);
540     if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
541     {
542       isPressed = 1;
543     }
544     guiState.isBlocked = 1;
545   }
546   else
547   {
548     Color color = isSelected ? WHITE : LIGHTGRAY;
549     DrawRectangle(x, y, width, height, color);
550   }
551   Font font = GetFontDefault();
552   Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1);
553   Color textColor = isDisabled ? GRAY : BLACK;
554   DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, textColor);
555   return isPressed;
556 }
557 
558 //# Main game loop
559 
560 void GameUpdate()
561 {
562   float dt = GetFrameTime();
563   // cap maximum delta time to 0.1 seconds to prevent large time steps
564   if (dt > 0.1f) dt = 0.1f;
565   gameTime.time += dt;
566   gameTime.deltaTime = dt;
567 
568   UpdateLevel(currentLevel);
569 }
570 
571 int main(void)
572 {
573   int screenWidth, screenHeight;
574   GetPreferredSize(&screenWidth, &screenHeight);
575   InitWindow(screenWidth, screenHeight, "Tower defense");
576   SetTargetFPS(30);
577 
578   LoadAssets();
579   InitGame();
580 
581   while (!WindowShouldClose())
582   {
583     if (IsPaused()) {
584       // canvas is not visible in browser - do nothing
585       continue;
586     }
587 
588     BeginDrawing();
589     ClearBackground((Color){0x4E, 0x63, 0x26, 0xFF});
590 
591     GameUpdate();
592     DrawLevel(currentLevel);
593 
594     EndDrawing();
595   }
596 
597   CloseWindow();
598 
599   return 0;
600 }
  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 #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 typedef struct Level 110 { 111 int seed; 112 LevelState state; 113 LevelState nextState; 114 Camera3D camera; 115 int placementMode; 116 117 int initialGold; 118 int playerGold; 119 120 EnemyWave waves[10]; 121 int currentWave; 122 float waveEndTimer; 123 } Level; 124 125 typedef struct DeltaSrc 126 { 127 char x, y; 128 } DeltaSrc; 129 130 typedef struct PathfindingMap 131 { 132 int width, height; 133 float scale; 134 float *distances; 135 long *towerIndex; 136 DeltaSrc *deltaSrc; 137 float maxDistance; 138 Matrix toMapSpace; 139 Matrix toWorldSpace; 140 } PathfindingMap; 141 142 // when we execute the pathfinding algorithm, we need to store the active nodes 143 // in a queue. Each node has a position, a distance from the start, and the 144 // position of the node that we came from. 145 typedef struct PathfindingNode 146 { 147 int16_t x, y, fromX, fromY; 148 float distance; 149 } PathfindingNode; 150 151 typedef struct EnemyId 152 { 153 uint16_t index; 154 uint16_t generation; 155 } EnemyId; 156 157 typedef struct EnemyClassConfig 158 { 159 float speed; 160 float health; 161 float radius; 162 float maxAcceleration; 163 float requiredContactTime; 164 float explosionDamage; 165 float explosionRange; 166 float explosionPushbackPower; 167 int goldValue; 168 } EnemyClassConfig; 169 170 typedef struct Enemy 171 { 172 int16_t currentX, currentY; 173 int16_t nextX, nextY; 174 Vector2 simPosition; 175 Vector2 simVelocity; 176 uint16_t generation; 177 float walkedDistance; 178 float startMovingTime; 179 float damage, futureDamage; 180 float contactTime; 181 uint8_t enemyType; 182 uint8_t movePathCount; 183 Vector2 movePath[ENEMY_MAX_PATH_COUNT]; 184 } Enemy; 185 186 // a unit that uses sprites to be drawn 187 #define SPRITE_UNIT_PHASE_WEAPON_IDLE 0 188 #define SPRITE_UNIT_PHASE_WEAPON_COOLDOWN 1 189 typedef struct SpriteUnit 190 { 191 Rectangle srcRect; 192 Vector2 offset; 193 int frameCount; 194 float frameDuration; 195 Rectangle srcWeaponIdleRect; 196 Vector2 srcWeaponIdleOffset; 197 Rectangle srcWeaponCooldownRect; 198 Vector2 srcWeaponCooldownOffset; 199 } SpriteUnit; 200 201 #define PROJECTILE_MAX_COUNT 1200 202 #define PROJECTILE_TYPE_NONE 0 203 #define PROJECTILE_TYPE_ARROW 1 204 #define PROJECTILE_TYPE_CATAPULT 2 205 #define PROJECTILE_TYPE_BALLISTA 3 206 207 typedef struct Projectile 208 { 209 uint8_t projectileType; 210 float shootTime; 211 float arrivalTime; 212 float distance; 213 Vector3 position; 214 Vector3 target; 215 Vector3 directionNormal; 216 EnemyId targetEnemy; 217 HitEffectConfig hitEffectConfig; 218 } Projectile; 219 220 //# Function declarations 221 float TowerGetMaxHealth(Tower *tower); 222 int Button(const char *text, int x, int y, int width, int height, ButtonState *state); 223 int EnemyAddDamageRange(Vector2 position, float range, float damage); 224 int EnemyAddDamage(Enemy *enemy, float damage); 225 226 //# Enemy functions 227 void EnemyInit(); 228 void EnemyDraw(); 229 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource); 230 void EnemyUpdate(); 231 float EnemyGetCurrentMaxSpeed(Enemy *enemy); 232 float EnemyGetMaxHealth(Enemy *enemy); 233 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY); 234 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount); 235 EnemyId EnemyGetId(Enemy *enemy); 236 Enemy *EnemyTryResolve(EnemyId enemyId); 237 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY); 238 int EnemyAddDamage(Enemy *enemy, float damage); 239 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range); 240 int EnemyCount(); 241 void EnemyDrawHealthbars(Camera3D camera); 242 243 //# Tower functions 244 void TowerInit(); 245 Tower *TowerGetAt(int16_t x, int16_t y); 246 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y); 247 Tower *GetTowerByType(uint8_t towerType); 248 int GetTowerCosts(uint8_t towerType); 249 float TowerGetMaxHealth(Tower *tower); 250 void TowerDraw(); 251 void TowerUpdate(); 252 void TowerDrawHealthBars(Camera3D camera); 253 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase); 254 255 //# Particles 256 void ParticleInit();
257 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime);
258 void ParticleUpdate(); 259 void ParticleDraw(); 260 261 //# Projectiles 262 void ProjectileInit(); 263 void ProjectileDraw(); 264 void ProjectileUpdate(); 265 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig); 266 267 //# Pathfinding map 268 void PathfindingMapInit(int width, int height, Vector3 translate, float scale); 269 float PathFindingGetDistance(int mapX, int mapY); 270 Vector2 PathFindingGetGradient(Vector3 world); 271 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY); 272 void PathFindingMapUpdate(); 273 void PathFindingMapDraw(); 274 275 //# UI 276 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth); 277 278 //# Level 279 void DrawLevelGround(Level *level); 280 281 //# variables 282 extern Level *currentLevel; 283 extern Enemy enemies[ENEMY_MAX_COUNT]; 284 extern int enemyCount; 285 extern EnemyClassConfig enemyClassConfigs[]; 286 287 extern GUIState guiState; 288 extern GameTime gameTime; 289 extern Tower towers[TOWER_MAX_COUNT]; 290 extern int towerCount; 291 292 extern Texture2D palette, spriteSheet; 293 294 #endif
  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 <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 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 
  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 "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 impact of the catapult tower now has a particle effect - which shows that the area damage is actually pretty large.

Wrap up

There are now 2 additional tower types with different effects in the game. They will also need graphics and animations soon so the overall look is more balanced again. Speaking of balancing: The current levels are very easy to beat. While we still lack a lot of features before we need actual balancing, it would be nice to make it at least a little bit more challenging and interesting.

One feature that is needed before we can start with balancing is an overlay during the planning phase that shows the current path that the enemies will take. This is all stuff for the next parts.

A problem for me right now is also that my buffer of written parts has run out. I will try to catch up a bit and write a few more parts, but I may need to skip a week at some point.

🍪