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 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:
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:
- Vector3CrossProduct - calculates the cross product of two vectors. The resulting vector is orthogonal to the two input vectors. Its length is the area of the parallelogram defined by the two input vectors.
 - Vector3DotProduct - calculates the dot product of two vectors. The dot product is the cosine of the angle between the two vectors multiplied by the length of the two vectors.
 - Vector3Normalize - normalizes a vector, meaning it scales the vector so its length is 1.
 
That's all. But maybe a little interactive visualization helps to understand what these functions do:
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
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:
- When the build mode is entered, the spring could be initialized at height 2 so it jumps to 1, making the tower initially appear very thin
 - When the tower is placed, we could could lift the tower up and let it fall down to its final position
 
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:
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.