Simple tower defense tutorial, part 15: Cartoonish tower movement (2/2)

The next problem to solve is how to draw the tower so that its top points to the tip of the spring (the red dot) and its size gets squished and squashed according to the spring's tension.

What we need is a look-at function that rotates the tower model in a way that the up direction points to the tip of the spring.

A naive approach could be to calculate the x and z rotation angles based on the difference between the tower position and the spring tip position.

Let's see how this looks like, though I believe, that there is a better solution. To compare the alignment of the orientation in comparison to the spring tip, we will draw a wired cube that represents the tower model.

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

The solution works well when moving the on a single axis, but when moving the tower diagonally, the red wire cube is not aligned anymore - you can test this by slowing the game time scale down by pressing T and pause the game by pressing P.

The tower is not well aligned with the spring tip.
The wire cube is not well aligned with the spring tip when moving diagonally.

The problem is that the x and z rotation is not done correctly, but even if it was, it would always result in a slight twist of the tower model when moving diagonally.

What we really want to do is to do this using just a single rotation. This can be achieved by using an axis that is orthogonal to the plane that is defined by the up vector and the spring tip vector. If that sounds confusing (to me it does), I hope that the visualization below helps to understand this better:

Visualization of the vectors used for the rotation.
Visualization of the vectors used for the rotation.

We can then determine the angle of the rotation by the dot product of the two vectors.

What may sound complex is actually rather simple. What we need to know are the following functions provided in the raymath module and how they work:

That's all. But maybe a little interactive visualization helps to understand what these functions do:

Use the mouse to point the red arrow tip. The cube is rotated so its up vector points aligns with the red arrow.

Let's see how this looks looks in action:

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

Perfect! Now we only need to use the actual tower model and implement the squash and stretch behavior. To avoid glitches, we limit the squash and stretch to a certain range. The squash and stretch is based on the length of the spring, so the tower height should match the spring tip's red debug indicator.

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

Ok, WOW. This feels really good, very cartoony! It is a little bit too squishy and wobbly, but we can adjust this. Making it float a little like we had before should be reintroduced as well.

Another thing: When we place the tower, we should also play a very short animation at the start and beginning. I would try to utilize the spring simulation for this as well:

Let's see how to do that:

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

Here's the build and placement animation in a single GIF:

Starting and placing a building construction.

What is really nice now is that the spawn and placement animation utilizes the same spring simulation that we use for the tower movement. The simulation parameters are a little adjusted to make the initial jump and the fall look a little squishier than the regular tower movement, but apart from that, it is the same simulation. For the starting phase, the spring is initialized at height and the parameters are changed:

  1 float damping = 0.5f;
  2 float springStiffness = 300.0f;
  3 float springDecay = 95.0f;
  4 float minHeight = 0.35f;
  5 
  6 if (level->placementPhase == PLACEMENT_PHASE_STARTING)
  7 {
  8   damping = 1.0f;
  9   springDecay = 90.0f;
 10   springStiffness = 100.0f;
 11   minHeight = 0.70f;
 12 }

When placing the tower, the floating height of the tower is also adjusted to make it look as if the tower is smashed down:

  1 // let the tower float up and down. Consider this height in the spring simulation as well
  2 float towerFloatHeight = sinf(gameTime.time * 4.0f) * 0.2f + 0.3f;
  3 
  4 if (level->placementPhase == PLACEMENT_PHASE_PLACING)
  5 {
  6   // The bouncing spring needs a bit of outro time to look nice and complete. 
  7   // So we scale the time so that the first 2/3rd of the placing phase handles the motion
  8   // and the last 1/3rd is the outro physics (bouncing)
  9   float t = fminf(1.0f, level->placementTimer / placementDuration * 1.5f);
 10   // let towerFloatHeight describe parabola curve, starting at towerFloatHeight and ending at 0
 11   float linearBlendHeight = (1.0f - t) * towerFloatHeight;
 12   float parabola = (1.0f - ((t - 0.5f) * (t - 0.5f) * 4.0f)) * 2.0f;
 13   towerFloatHeight = linearBlendHeight + parabola;
 14 }

Moving and placing the tower feels now pretty satisfying. Though ... this could be juiced up even more! A little screenshake when placing the tower would be nice, as well as some dust particles? Let's do this - at some point in the future 😅.

Conclusion

In this part, we applied the deformation to the tower model using vector math. In the end it is nothing more than a combination of rotating and scaling the model.

Cross and dot products are the among the most basic yet highly useful functions in my experience when it comes to 3D math and I hope that the visualization helped to understand how these functions can be used in practice.

In the next part, we will add a few more different enemy types to the game.

🍪