Simple tower defense tutorial, part 18: Tower upgrading, 2/2

Since we have now the means to draw customizable context menus, we can focus on implementing the tower upgrading feature. There are two parts: Working out the UI and implementing the game logic. Let's start with the UI since we need it to test the game logic part.

UI

The plan:

The player can therefore upgrade a single category up to 10 times while the other two categories would have to remain at level 0.

There is no strong reason for this design - I have no idea if this is a good idea or not. But I believe there should be some kind of upgrade system and this is something quite simple to implement. A more sophisticated and potentially cooler system would be an upgrade tree, but that is much more complex to implement. So let's start simple and see where it takes us!

  • 💾
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 #include <rlgl.h>
  4 #include <stdlib.h>
  5 #include <math.h>
  6 #include <string.h>
  7 
  8 //# Variables
  9 Font gameFontNormal = {0};
 10 GUIState guiState = {0};
 11 GameTime gameTime = {
 12   .fixedDeltaTime = 1.0f / 60.0f,
 13 };
 14 
 15 Model floorTileAModel = {0};
 16 Model floorTileBModel = {0};
 17 Model treeModel[2] = {0};
 18 Model firTreeModel[2] = {0};
 19 Model rockModels[5] = {0};
 20 Model grassPatchModel[1] = {0};
 21 
 22 Model pathArrowModel = {0};
 23 Model greenArrowModel = {0};
 24 
 25 Texture2D palette, spriteSheet;
 26 
 27 NPatchInfo uiPanelPatch = {
 28   .layout = NPATCH_NINE_PATCH,
 29   .source = {145, 1, 46, 46},
 30   .top = 18, .bottom = 18,
 31   .left = 16, .right = 16
 32 };
 33 NPatchInfo uiButtonNormal = {
 34   .layout = NPATCH_NINE_PATCH,
 35   .source = {193, 1, 32, 20},
 36   .top = 7, .bottom = 7,
 37   .left = 10, .right = 10
 38 };
 39 NPatchInfo uiButtonDisabled = {
 40   .layout = NPATCH_NINE_PATCH,
 41   .source = {193, 22, 32, 20},
 42   .top = 7, .bottom = 7,
 43   .left = 10, .right = 10
 44 };
 45 NPatchInfo uiButtonHovered = {
 46   .layout = NPATCH_NINE_PATCH,
 47   .source = {193, 43, 32, 20},
 48   .top = 7, .bottom = 7,
 49   .left = 10, .right = 10
 50 };
 51 NPatchInfo uiButtonPressed = {
 52   .layout = NPATCH_NINE_PATCH,
 53   .source = {193, 64, 32, 20},
 54   .top = 7, .bottom = 7,
 55   .left = 10, .right = 10
 56 };
 57 Rectangle uiDiamondMarker = {145, 48, 15, 15};
 58 
 59 Level levels[] = {
 60   [0] = {
 61     .state = LEVEL_STATE_BUILDING,
 62     .initialGold = 20,
 63     .waves[0] = {
 64       .enemyType = ENEMY_TYPE_SHIELD,
 65       .wave = 0,
 66       .count = 1,
 67       .interval = 2.5f,
 68       .delay = 1.0f,
 69       .spawnPosition = {2, 6},
 70     },
 71     .waves[1] = {
 72       .enemyType = ENEMY_TYPE_RUNNER,
 73       .wave = 0,
 74       .count = 5,
 75       .interval = 0.5f,
 76       .delay = 1.0f,
 77       .spawnPosition = {-2, 6},
 78     },
 79     .waves[2] = {
 80       .enemyType = ENEMY_TYPE_SHIELD,
 81       .wave = 1,
 82       .count = 20,
 83       .interval = 1.5f,
 84       .delay = 1.0f,
 85       .spawnPosition = {0, 6},
 86     },
 87     .waves[3] = {
 88       .enemyType = ENEMY_TYPE_MINION,
 89       .wave = 2,
 90       .count = 30,
 91       .interval = 1.2f,
 92       .delay = 1.0f,
 93       .spawnPosition = {2, 6},
 94     },
 95     .waves[4] = {
 96       .enemyType = ENEMY_TYPE_BOSS,
 97       .wave = 2,
 98       .count = 2,
 99       .interval = 5.0f,
100       .delay = 2.0f,
101       .spawnPosition = {-2, 4},
102     }
103   },
104 };
105 
106 Level *currentLevel = levels;
107 
108 void DrawBoxedText(const char *text, int x, int y, int width, int height, float alignX, float alignY, Color textColor);
109 
110 void DrawTitleText(const char *text, int anchorX, float alignX, Color color)
111 {
112   int textWidth = MeasureTextEx(gameFontNormal, text, gameFontNormal.baseSize, 0).x;
113   int panelWidth = textWidth + 40;
114   int posX = anchorX - panelWidth * alignX;
115   int textOffset = 20;
116   DrawTextureNPatch(spriteSheet, uiPanelPatch, (Rectangle){ posX, -10, panelWidth, 40}, Vector2Zero(), 0, WHITE);
117   DrawTextEx(gameFontNormal, text, (Vector2){posX + textOffset + 1, 6 + 1}, gameFontNormal.baseSize, 0, BLACK);
118   DrawTextEx(gameFontNormal, text, (Vector2){posX + textOffset, 6}, gameFontNormal.baseSize, 0, color);
119 }
120 
121 void DrawTitle(const char *text)
122 {
123   DrawTitleText(text, GetScreenWidth() / 2, 0.5f, WHITE);
124 }
125 
126 //# Game
127 
128 static Model LoadGLBModel(char *filename)
129 {
130   Model model = LoadModel(TextFormat("data/%s.glb",filename));
131   for (int i = 0; i < model.materialCount; i++)
132   {
133     model.materials[i].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
134   }
135   return model;
136 }
137 
138 void LoadAssets()
139 {
140   // load a sprite sheet that contains all units
141   spriteSheet = LoadTexture("data/spritesheet.png");
142   SetTextureFilter(spriteSheet, TEXTURE_FILTER_BILINEAR);
143 
144   // we'll use a palette texture to colorize the all buildings and environment art
145   palette = LoadTexture("data/palette.png");
146   // The texture uses gradients on very small space, so we'll enable bilinear filtering
147   SetTextureFilter(palette, TEXTURE_FILTER_BILINEAR);
148 
149   gameFontNormal = LoadFont("data/alagard.png");
150 
151   floorTileAModel = LoadGLBModel("floor-tile-a");
152   floorTileBModel = LoadGLBModel("floor-tile-b");
153   treeModel[0] = LoadGLBModel("leaftree-large-1-a");
154   treeModel[1] = LoadGLBModel("leaftree-large-1-b");
155   firTreeModel[0] = LoadGLBModel("firtree-1-a");
156   firTreeModel[1] = LoadGLBModel("firtree-1-b");
157   rockModels[0] = LoadGLBModel("rock-1");
158   rockModels[1] = LoadGLBModel("rock-2");
159   rockModels[2] = LoadGLBModel("rock-3");
160   rockModels[3] = LoadGLBModel("rock-4");
161   rockModels[4] = LoadGLBModel("rock-5");
162   grassPatchModel[0] = LoadGLBModel("grass-patch-1");
163 
164   pathArrowModel = LoadGLBModel("direction-arrow-x");
165   greenArrowModel = LoadGLBModel("green-arrow");
166 }
167 
168 void InitLevel(Level *level)
169 {
170   level->seed = (int)(GetTime() * 100.0f);
171 
172   TowerInit();
173   EnemyInit();
174   ProjectileInit();
175   ParticleInit();
176   TowerTryAdd(TOWER_TYPE_BASE, 0, 0);
177 
178   level->placementMode = 0;
179   level->state = LEVEL_STATE_BUILDING;
180   level->nextState = LEVEL_STATE_NONE;
181   level->playerGold = level->initialGold;
182   level->currentWave = 0;
183   level->placementX = -1;
184   level->placementY = 0;
185 
186   Camera *camera = &level->camera;
187   camera->position = (Vector3){4.0f, 8.0f, 8.0f};
188   camera->target = (Vector3){0.0f, 0.0f, 0.0f};
189   camera->up = (Vector3){0.0f, 1.0f, 0.0f};
190   camera->fovy = 11.5f;
191   camera->projection = CAMERA_ORTHOGRAPHIC;
192 }
193 
194 void DrawLevelHud(Level *level)
195 {
196   const char *text = TextFormat("Gold: %d", level->playerGold);
197   DrawTitleText(text, GetScreenWidth() - 10, 1.0f, YELLOW);
198 }
199 
200 void DrawLevelReportLostWave(Level *level)
201 {
202   BeginMode3D(level->camera);
203   DrawLevelGround(level);
204   TowerDraw();
205   EnemyDraw();
206   ProjectileDraw();
207   ParticleDraw();
208   guiState.isBlocked = 0;
209   EndMode3D();
210 
211   TowerDrawHealthBars(level->camera);
212 
213   DrawTitle("Wave lost");
214   
215   if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
216   {
217     level->nextState = LEVEL_STATE_RESET;
218   }
219 }
220 
221 int HasLevelNextWave(Level *level)
222 {
223   for (int i = 0; i < 10; i++)
224   {
225     EnemyWave *wave = &level->waves[i];
226     if (wave->wave == level->currentWave)
227     {
228       return 1;
229     }
230   }
231   return 0;
232 }
233 
234 void DrawLevelReportWonWave(Level *level)
235 {
236   BeginMode3D(level->camera);
237   DrawLevelGround(level);
238   TowerDraw();
239   EnemyDraw();
240   ProjectileDraw();
241   ParticleDraw();
242   guiState.isBlocked = 0;
243   EndMode3D();
244 
245   TowerDrawHealthBars(level->camera);
246 
247   DrawTitle("Wave won");
248 
249 
250   if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
251   {
252     level->nextState = LEVEL_STATE_RESET;
253   }
254 
255   if (HasLevelNextWave(level))
256   {
257     if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
258     {
259       level->nextState = LEVEL_STATE_BUILDING;
260     }
261   }
262   else {
263     if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
264     {
265       level->nextState = LEVEL_STATE_WON_LEVEL;
266     }
267   }
268 }
269 
270 int DrawBuildingBuildButton(Level *level, int x, int y, int width, int height, uint8_t towerType, const char *name)
271 {
272   static ButtonState buttonStates[8] = {0};
273   int cost = GetTowerCosts(towerType);
274   const char *text = TextFormat("%s: %d", name, cost);
275   buttonStates[towerType].isSelected = level->placementMode == towerType;
276   buttonStates[towerType].isDisabled = level->playerGold < cost;
277   if (Button(text, x, y, width, height, &buttonStates[towerType]))
278   {
279     level->placementMode = buttonStates[towerType].isSelected ? 0 : towerType;
280     level->nextState = LEVEL_STATE_BUILDING_PLACEMENT;
281     return 1;
282   }
283   return 0;
284 }
285 
286 float GetRandomFloat(float min, float max)
287 {
288   int random = GetRandomValue(0, 0xfffffff);
289   return ((float)random / (float)0xfffffff) * (max - min) + min;
290 }
291 
292 void DrawLevelGround(Level *level)
293 {
294   // draw checkerboard ground pattern
295   for (int x = -5; x <= 5; x += 1)
296   {
297     for (int y = -5; y <= 5; y += 1)
298     {
299       Model *model = (x + y) % 2 == 0 ? &floorTileAModel : &floorTileBModel;
300       DrawModel(*model, (Vector3){x, 0.0f, y}, 1.0f, WHITE);
301     }
302   }
303 
304   int oldSeed = GetRandomValue(0, 0xfffffff);
305   SetRandomSeed(level->seed);
306   // increase probability for trees via duplicated entries
307   Model borderModels[64];
308   int maxRockCount = GetRandomValue(2, 6);
309   int maxTreeCount = GetRandomValue(10, 20);
310   int maxFirTreeCount = GetRandomValue(5, 10);
311   int maxLeafTreeCount = maxTreeCount - maxFirTreeCount;
312   int grassPatchCount = GetRandomValue(5, 30);
313 
314   int modelCount = 0;
315   for (int i = 0; i < maxRockCount && modelCount < 63; i++)
316   {
317     borderModels[modelCount++] = rockModels[GetRandomValue(0, 5)];
318   }
319   for (int i = 0; i < maxLeafTreeCount && modelCount < 63; i++)
320   {
321     borderModels[modelCount++] = treeModel[GetRandomValue(0, 1)];
322   }
323   for (int i = 0; i < maxFirTreeCount && modelCount < 63; i++)
324   {
325     borderModels[modelCount++] = firTreeModel[GetRandomValue(0, 1)];
326   }
327   for (int i = 0; i < grassPatchCount && modelCount < 63; i++)
328   {
329     borderModels[modelCount++] = grassPatchModel[0];
330   }
331 
332   // draw some objects around the border of the map
333   Vector3 up = {0, 1, 0};
334   // a pseudo random number generator to get the same result every time
335   const float wiggle = 0.75f;
336   const int layerCount = 3;
337   for (int layer = 0; layer <= layerCount; layer++)
338   {
339     int layerPos = 6 + layer;
340     Model *selectedModels = borderModels;
341     int selectedModelCount = modelCount;
342     if (layer == 0)
343     {
344       selectedModels = grassPatchModel;
345       selectedModelCount = 1;
346     }
347     for (int x = -6 - layer; x <= 6 + layer; x += 1)
348     {
349       DrawModelEx(selectedModels[GetRandomValue(0, selectedModelCount - 1)], 
350         (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, -layerPos + GetRandomFloat(0.0f, wiggle)}, 
351         up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
352       DrawModelEx(selectedModels[GetRandomValue(0, selectedModelCount - 1)], 
353         (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, layerPos + GetRandomFloat(0.0f, wiggle)}, 
354         up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
355     }
356 
357     for (int z = -5 - layer; z <= 5 + layer; z += 1)
358     {
359       DrawModelEx(selectedModels[GetRandomValue(0, selectedModelCount - 1)], 
360         (Vector3){-layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)}, 
361         up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
362       DrawModelEx(selectedModels[GetRandomValue(0, selectedModelCount - 1)], 
363         (Vector3){layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)}, 
364         up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
365     }
366   }
367 
368   SetRandomSeed(oldSeed);
369 }
370 
371 void DrawEnemyPath(Level *level, Color arrowColor)
372 {
373   const int castleX = 0, castleY = 0;
374   const int maxWaypointCount = 200;
375   const float timeStep = 1.0f;
376   Vector3 arrowScale = {0.75f, 0.75f, 0.75f};
377 
378   // we start with a time offset to simulate the path, 
379   // this way the arrows are animated in a forward moving direction
380   // The time is wrapped around the time step to get a smooth animation
381   float timeOffset = fmodf(GetTime(), timeStep);
382 
383   for (int i = 0; i < ENEMY_MAX_WAVE_COUNT; i++)
384   {
385     EnemyWave *wave = &level->waves[i];
386     if (wave->wave != level->currentWave)
387     {
388       continue;
389     }
390 
391     // use this dummy enemy to simulate the path
392     Enemy dummy = {
393       .enemyType = ENEMY_TYPE_MINION,
394       .simPosition = (Vector2){wave->spawnPosition.x, wave->spawnPosition.y},
395       .nextX = wave->spawnPosition.x,
396       .nextY = wave->spawnPosition.y,
397       .currentX = wave->spawnPosition.x,
398       .currentY = wave->spawnPosition.y,
399     };
400 
401     float deltaTime = timeOffset;
402     for (int j = 0; j < maxWaypointCount; j++)
403     {
404       int waypointPassedCount = 0;
405       Vector2 pos = EnemyGetPosition(&dummy, deltaTime, &dummy.simVelocity, &waypointPassedCount);
406       // after the initial variable starting offset, we use a fixed time step
407       deltaTime = timeStep;
408       dummy.simPosition = pos;
409 
410       // Update the dummy's position just like we do in the regular enemy update loop
411       for (int k = 0; k < waypointPassedCount; k++)
412       {
413         dummy.currentX = dummy.nextX;
414         dummy.currentY = dummy.nextY;
415         if (EnemyGetNextPosition(dummy.currentX, dummy.currentY, &dummy.nextX, &dummy.nextY) &&
416           Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
417         {
418           break;
419         }
420       }
421       if (Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
422       {
423         break;
424       }
425       
426       // get the angle we need to rotate the arrow model. The velocity is just fine for this.
427       float angle = atan2f(dummy.simVelocity.x, dummy.simVelocity.y) * RAD2DEG - 90.0f;
428       DrawModelEx(pathArrowModel, (Vector3){pos.x, 0.15f, pos.y}, (Vector3){0, 1, 0}, angle, arrowScale, arrowColor);
429     }
430   }
431 }
432 
433 void DrawEnemyPaths(Level *level)
434 {
435   // disable depth testing for the path arrows
436   // flush the 3D batch to draw the arrows on top of everything
437   rlDrawRenderBatchActive();
438   rlDisableDepthTest();
439   DrawEnemyPath(level, (Color){64, 64, 64, 160});
440 
441   rlDrawRenderBatchActive();
442   rlEnableDepthTest();
443   DrawEnemyPath(level, WHITE);
444 }
445 
446 static void SimulateTowerPlacementBehavior(Level *level, float towerFloatHeight, float mapX, float mapY)
447 {
448   float dt = gameTime.fixedDeltaTime;
449   // smooth transition for the placement position using exponential decay
450   const float lambda = 15.0f;
451   float factor = 1.0f - expf(-lambda * dt);
452 
453   float damping = 0.5f;
454   float springStiffness = 300.0f;
455   float springDecay = 95.0f;
456   float minHeight = 0.35f;
457 
458   if (level->placementPhase == PLACEMENT_PHASE_STARTING)
459   {
460     damping = 1.0f;
461     springDecay = 90.0f;
462     springStiffness = 100.0f;
463     minHeight = 0.70f;
464   }
465 
466   for (int i = 0; i < gameTime.fixedStepCount; i++)
467   {
468     level->placementTransitionPosition = 
469       Vector2Lerp(
470         level->placementTransitionPosition, 
471         (Vector2){mapX, mapY}, factor);
472 
473     // draw the spring position for debugging the spring simulation
474     // first step: stiff spring, no simulation
475     Vector3 worldPlacementPosition = (Vector3){
476       level->placementTransitionPosition.x, towerFloatHeight, level->placementTransitionPosition.y};
477     Vector3 springTargetPosition = (Vector3){
478       worldPlacementPosition.x, 1.0f + towerFloatHeight, worldPlacementPosition.z};
479     // consider the current velocity to predict the future position in order to dampen
480     // the spring simulation. Longer prediction times will result in more damping
481     Vector3 predictedSpringPosition = Vector3Add(level->placementTowerSpring.position, 
482       Vector3Scale(level->placementTowerSpring.velocity, dt * damping));
483     Vector3 springPointDelta = Vector3Subtract(springTargetPosition, predictedSpringPosition);
484     Vector3 velocityChange = Vector3Scale(springPointDelta, dt * springStiffness);
485     // decay velocity of the upright forcing spring
486     // This force acts like a 2nd spring that pulls the tip upright into the air above the
487     // base position
488     level->placementTowerSpring.velocity = Vector3Scale(level->placementTowerSpring.velocity, 1.0f - expf(-springDecay * dt));
489     level->placementTowerSpring.velocity = Vector3Add(level->placementTowerSpring.velocity, velocityChange);
490 
491     // calculate length of the spring to calculate the force to apply to the placementTowerSpring position
492     // we use a simple spring model with a rest length of 1.0f
493     Vector3 springDelta = Vector3Subtract(worldPlacementPosition, level->placementTowerSpring.position);
494     float springLength = Vector3Length(springDelta);
495     float springForce = (springLength - 1.0f) * springStiffness;
496     Vector3 springForceVector = Vector3Normalize(springDelta);
497     springForceVector = Vector3Scale(springForceVector, springForce);
498     level->placementTowerSpring.velocity = Vector3Add(level->placementTowerSpring.velocity, 
499       Vector3Scale(springForceVector, dt));
500 
501     level->placementTowerSpring.position = Vector3Add(level->placementTowerSpring.position, 
502       Vector3Scale(level->placementTowerSpring.velocity, dt));
503     if (level->placementTowerSpring.position.y < minHeight + towerFloatHeight)
504     {
505       level->placementTowerSpring.velocity.y *= -1.0f;
506       level->placementTowerSpring.position.y = fmaxf(level->placementTowerSpring.position.y, minHeight + towerFloatHeight);
507     }
508   }
509 }
510 
511 void DrawLevelBuildingPlacementState(Level *level)
512 {
513   const float placementDuration = 0.5f;
514 
515   level->placementTimer += gameTime.deltaTime;
516   if (level->placementTimer > 1.0f && level->placementPhase == PLACEMENT_PHASE_STARTING)
517   {
518     level->placementPhase = PLACEMENT_PHASE_MOVING;
519     level->placementTimer = 0.0f;
520   }
521 
522   BeginMode3D(level->camera);
523   DrawLevelGround(level);
524 
525   int blockedCellCount = 0;
526   Vector2 blockedCells[1];
527   Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera);
528   float planeDistance = ray.position.y / -ray.direction.y;
529   float planeX = ray.direction.x * planeDistance + ray.position.x;
530   float planeY = ray.direction.z * planeDistance + ray.position.z;
531   int16_t mapX = (int16_t)floorf(planeX + 0.5f);
532   int16_t mapY = (int16_t)floorf(planeY + 0.5f);
533   if (level->placementPhase == PLACEMENT_PHASE_MOVING && 
534     level->placementMode && !guiState.isBlocked && 
535     mapX >= -5 && mapX <= 5 && mapY >= -5 && mapY <= 5 && IsMouseButtonDown(MOUSE_LEFT_BUTTON))
536   {
537     level->placementX = mapX;
538     level->placementY = mapY;
539   }
540   else
541   {
542     mapX = level->placementX;
543     mapY = level->placementY;
544   }
545   blockedCells[blockedCellCount++] = (Vector2){mapX, mapY};
546   PathFindingMapUpdate(blockedCellCount, blockedCells);
547 
548   TowerDraw();
549   EnemyDraw();
550   ProjectileDraw();
551   ParticleDraw();
552   DrawEnemyPaths(level);
553 
554   // let the tower float up and down. Consider this height in the spring simulation as well
555   float towerFloatHeight = sinf(gameTime.time * 4.0f) * 0.2f + 0.3f;
556 
557   if (level->placementPhase == PLACEMENT_PHASE_PLACING)
558   {
559     // The bouncing spring needs a bit of outro time to look nice and complete. 
560     // So we scale the time so that the first 2/3rd of the placing phase handles the motion
561     // and the last 1/3rd is the outro physics (bouncing)
562     float t = fminf(1.0f, level->placementTimer / placementDuration * 1.5f);
563     // let towerFloatHeight describe parabola curve, starting at towerFloatHeight and ending at 0
564     float linearBlendHeight = (1.0f - t) * towerFloatHeight;
565     float parabola = (1.0f - ((t - 0.5f) * (t - 0.5f) * 4.0f)) * 2.0f;
566     towerFloatHeight = linearBlendHeight + parabola;
567   }
568 
569   SimulateTowerPlacementBehavior(level, towerFloatHeight, mapX, mapY);
570   
571   rlPushMatrix();
572   rlTranslatef(level->placementTransitionPosition.x, 0, level->placementTransitionPosition.y);
573 
574   rlPushMatrix();
575   rlTranslatef(0.0f, towerFloatHeight, 0.0f);
576   // calculate x and z rotation to align the model with the spring
577   Vector3 position = {level->placementTransitionPosition.x, towerFloatHeight, level->placementTransitionPosition.y};
578   Vector3 towerUp = Vector3Subtract(level->placementTowerSpring.position, position);
579   Vector3 rotationAxis = Vector3CrossProduct(towerUp, (Vector3){0, 1, 0});
580   float angle = acosf(Vector3DotProduct(towerUp, (Vector3){0, 1, 0}) / Vector3Length(towerUp)) * RAD2DEG;
581   float springLength = Vector3Length(towerUp);
582   float towerStretch = fminf(fmaxf(springLength, 0.5f), 4.5f);
583   float towerSquash = 1.0f / towerStretch;
584   rlRotatef(-angle, rotationAxis.x, rotationAxis.y, rotationAxis.z);
585   rlScalef(towerSquash, towerStretch, towerSquash);
586   Tower dummy = {
587     .towerType = level->placementMode,
588   };
589   TowerDrawSingle(dummy);
590   rlPopMatrix();
591 
592   // draw a shadow for the tower
593   float umbrasize = 0.8 + sqrtf(towerFloatHeight);
594   DrawCube((Vector3){0.0f, 0.05f, 0.0f}, umbrasize, 0.0f, umbrasize, (Color){0, 0, 0, 32});
595   DrawCube((Vector3){0.0f, 0.075f, 0.0f}, 0.85f, 0.0f, 0.85f, (Color){0, 0, 0, 64});
596 
597 
598   float bounce = sinf(gameTime.time * 8.0f) * 0.5f + 0.5f;
599   float offset = fmaxf(0.0f, bounce - 0.4f) * 0.35f + 0.7f;
600   float squeeze = -fminf(0.0f, bounce - 0.4f) * 0.7f + 0.7f;
601   float stretch = fmaxf(0.0f, bounce - 0.3f) * 0.5f + 0.8f;
602   
603   DrawModelEx(greenArrowModel, (Vector3){ offset, 0.0f,  0.0f}, (Vector3){0, 1, 0}, 90, (Vector3){squeeze, 1.0f, stretch}, WHITE);
604   DrawModelEx(greenArrowModel, (Vector3){-offset, 0.0f,  0.0f}, (Vector3){0, 1, 0}, 270, (Vector3){squeeze, 1.0f, stretch}, WHITE);
605   DrawModelEx(greenArrowModel, (Vector3){ 0.0f, 0.0f,  offset}, (Vector3){0, 1, 0}, 0, (Vector3){squeeze, 1.0f, stretch}, WHITE);
606   DrawModelEx(greenArrowModel, (Vector3){ 0.0f, 0.0f, -offset}, (Vector3){0, 1, 0}, 180, (Vector3){squeeze, 1.0f, stretch}, WHITE);
607   rlPopMatrix();
608 
609   guiState.isBlocked = 0;
610 
611   EndMode3D();
612 
613   TowerDrawHealthBars(level->camera);
614 
615   if (level->placementPhase == PLACEMENT_PHASE_PLACING)
616   {
617     if (level->placementTimer > placementDuration)
618     {
619         Tower *tower = TowerTryAdd(level->placementMode, mapX, mapY);
620         // testing repairing
621         tower->damage = 2.5f;
622         level->playerGold -= GetTowerCosts(level->placementMode);
623         level->nextState = LEVEL_STATE_BUILDING;
624         level->placementMode = TOWER_TYPE_NONE;
625     }
626   }
627   else
628   {   
629     if (Button("Cancel", 20, GetScreenHeight() - 40, 160, 30, 0))
630     {
631       level->nextState = LEVEL_STATE_BUILDING;
632       level->placementMode = TOWER_TYPE_NONE;
633       TraceLog(LOG_INFO, "Cancel building");
634     }
635     
636     if (TowerGetAt(mapX, mapY) == 0 &&  Button("Build", GetScreenWidth() - 180, GetScreenHeight() - 40, 160, 30, 0))
637     {
638       level->placementPhase = PLACEMENT_PHASE_PLACING;
639       level->placementTimer = 0.0f;
640     }
641   }
642 }
643 
644 enum ContextMenuType
645 {
646   CONTEXT_MENU_TYPE_MAIN,
647   CONTEXT_MENU_TYPE_SELL_CONFIRM,
648 CONTEXT_MENU_TYPE_UPGRADE, 649 }; 650 651 enum UpgradeType 652 { 653 UPGRADE_TYPE_SPEED, 654 UPGRADE_TYPE_DAMAGE, 655 UPGRADE_TYPE_RANGE,
656 }; 657 658 typedef struct ContextMenuArgs 659 { 660 void *data; 661 uint8_t uint8; 662 int32_t int32; 663 Tower *tower; 664 } ContextMenuArgs; 665 666 int OnContextMenuBuild(Level *level, ContextMenuArgs *data) 667 { 668 uint8_t towerType = data->uint8; 669 level->placementMode = towerType; 670 level->nextState = LEVEL_STATE_BUILDING_PLACEMENT; 671 return 1; 672 } 673 674 int OnContextMenuSellConfirm(Level *level, ContextMenuArgs *data) 675 { 676 Tower *tower = data->tower; 677 int gold = data->int32; 678 level->playerGold += gold; 679 tower->towerType = TOWER_TYPE_NONE; 680 return 1; 681 } 682 683 int OnContextMenuSellCancel(Level *level, ContextMenuArgs *data) 684 { 685 return 1; 686 } 687 688 int OnContextMenuSell(Level *level, ContextMenuArgs *data) 689 {
690 level->placementContextMenuType = CONTEXT_MENU_TYPE_SELL_CONFIRM; 691 return 0; 692 } 693 694 int OnContextMenuDoUpgrade(Level *level, ContextMenuArgs *data) 695 { 696 Tower *tower = data->tower; 697 switch (data->uint8) 698 { 699 case UPGRADE_TYPE_SPEED: tower->upgradeState.speed++; break; 700 case UPGRADE_TYPE_DAMAGE: tower->upgradeState.damage++; break; 701 case UPGRADE_TYPE_RANGE: tower->upgradeState.range++; break; 702 } 703 level->playerGold -= data->int32; 704 return 0; 705 } 706 707 int OnContextMenuUpgrade(Level *level, ContextMenuArgs *data) 708 { 709 level->placementContextMenuType = CONTEXT_MENU_TYPE_UPGRADE;
710 return 0; 711 } 712 713 int OnContextMenuRepair(Level *level, ContextMenuArgs *data) 714 { 715 Tower *tower = data->tower; 716 if (level->playerGold >= 1) 717 { 718 level->playerGold -= 1; 719 tower->damage = fmaxf(0.0f, tower->damage - 1.0f); 720 } 721 return tower->damage == 0.0f; 722 } 723 724 typedef struct ContextMenuItem 725 { 726 uint8_t index; 727 char text[24]; 728 float alignX; 729 int (*action)(Level*, ContextMenuArgs*); 730 void *data; 731 ContextMenuArgs args; 732 ButtonState buttonState; 733 } ContextMenuItem; 734 735 ContextMenuItem ContextMenuItemText(uint8_t index, const char *text, float alignX) 736 { 737 ContextMenuItem item = {.index = index, .alignX = alignX}; 738 strncpy(item.text, text, 24); 739 return item; 740 } 741 742 ContextMenuItem ContextMenuItemButton(uint8_t index, const char *text, int (*action)(Level*, ContextMenuArgs*), ContextMenuArgs args) 743 { 744 ContextMenuItem item = {.index = index, .action = action, .args = args}; 745 strncpy(item.text, text, 24); 746 return item; 747 } 748 749 int DrawContextMenu(Level *level, Vector2 anchorLow, Vector2 anchorHigh, int width, ContextMenuItem *menus) 750 { 751 const int itemHeight = 28; 752 const int itemSpacing = 1; 753 const int padding = 8; 754 int itemCount = 0; 755 for (int i = 0; menus[i].text[0]; i++) 756 { 757 itemCount = itemCount > menus[i].index ? itemCount : menus[i].index; 758 } 759 760 Rectangle contextMenu = {0, 0, width, 761 (itemHeight + itemSpacing) * (itemCount + 1) + itemSpacing + padding * 2}; 762 763 Vector2 anchor = anchorHigh.y - 30 > contextMenu.height ? anchorHigh : anchorLow; 764 float anchorPivotY = anchorHigh.y - 30 > contextMenu.height ? 1.0f : 0.0f; 765 766 contextMenu.x = anchor.x - contextMenu.width * 0.5f;
767 contextMenu.y = anchor.y - contextMenu.height * anchorPivotY; 768 contextMenu.x = fmaxf(0, fminf(GetScreenWidth() - contextMenu.width, contextMenu.x)); 769 contextMenu.y = fmaxf(0, fminf(GetScreenHeight() - contextMenu.height, contextMenu.y)); 770
771 DrawTextureNPatch(spriteSheet, uiPanelPatch, contextMenu, Vector2Zero(), 0, WHITE); 772 DrawTextureRec(spriteSheet, uiDiamondMarker, (Vector2){anchor.x - uiDiamondMarker.width / 2, anchor.y - uiDiamondMarker.height / 2}, WHITE); 773 const int itemX = contextMenu.x + itemSpacing; 774 const int itemWidth = contextMenu.width - itemSpacing * 2; 775 #define ITEM_Y(idx) (contextMenu.y + itemSpacing + (itemHeight + itemSpacing) * idx + padding) 776 #define ITEM_RECT(idx, marginLR) itemX + (marginLR) + padding, ITEM_Y(idx), itemWidth - (marginLR) * 2 - padding * 2, itemHeight 777 int status = 0; 778 for (int i = 0; menus[i].text[0]; i++) 779 { 780 if (menus[i].action) 781 { 782 if (Button(menus[i].text, ITEM_RECT(menus[i].index, 0), &menus[i].buttonState)) 783 { 784 status = menus[i].action(level, &menus[i].args); 785 } 786 } 787 else 788 { 789 DrawBoxedText(menus[i].text, ITEM_RECT(menus[i].index, 0), menus[i].alignX, 0.5f, WHITE); 790 } 791 } 792 793 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && !CheckCollisionPointRec(GetMousePosition(), contextMenu)) 794 { 795 return 1; 796 } 797
798 return status; 799 } 800 801 void DrawLevelBuildingStateMainContextMenu(Level *level, Tower *tower, int32_t sellValue, float hp, float maxHitpoints, Vector2 anchorLow, Vector2 anchorHigh) 802 { 803 ContextMenuItem menu[12] = {0}; 804 int menuCount = 0; 805 int menuIndex = 0; 806 if (tower) 807 { 808 809 if (tower) { 810 menu[menuCount++] = ContextMenuItemText(menuIndex++, GetTowerName(tower->towerType), 0.5f); 811 } 812 813 // two texts, same line 814 menu[menuCount++] = ContextMenuItemText(menuIndex, "HP:", 0.0f); 815 menu[menuCount++] = ContextMenuItemText(menuIndex++, TextFormat("%.1f / %.1f", hp, maxHitpoints), 1.0f); 816 817 if (tower->towerType != TOWER_TYPE_BASE) 818 { 819 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "Upgrade", OnContextMenuUpgrade, 820 (ContextMenuArgs){.tower = tower}); 821 } 822 823 if (tower->towerType != TOWER_TYPE_BASE) 824 { 825 826 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "Sell", OnContextMenuSell, 827 (ContextMenuArgs){.tower = tower, .int32 = sellValue}); 828 } 829 if (tower->towerType != TOWER_TYPE_BASE && tower->damage > 0 && level->playerGold >= 1) 830 { 831 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "Repair 1 (1G)", OnContextMenuRepair, 832 (ContextMenuArgs){.tower = tower}); 833 } 834 } 835 else 836 { 837 menu[menuCount] = ContextMenuItemButton(menuIndex++, 838 TextFormat("Wall: %dG", GetTowerCosts(TOWER_TYPE_WALL)), 839 OnContextMenuBuild, (ContextMenuArgs){ .uint8 = (uint8_t) TOWER_TYPE_WALL}); 840 menu[menuCount++].buttonState.isDisabled = level->playerGold < GetTowerCosts(TOWER_TYPE_WALL); 841 842 menu[menuCount] = ContextMenuItemButton(menuIndex++, 843 TextFormat("Archer: %dG", GetTowerCosts(TOWER_TYPE_ARCHER)), 844 OnContextMenuBuild, (ContextMenuArgs){ .uint8 = (uint8_t) TOWER_TYPE_ARCHER}); 845 menu[menuCount++].buttonState.isDisabled = level->playerGold < GetTowerCosts(TOWER_TYPE_ARCHER); 846 847 menu[menuCount] = ContextMenuItemButton(menuIndex++, 848 TextFormat("Ballista: %dG", GetTowerCosts(TOWER_TYPE_BALLISTA)), 849 OnContextMenuBuild, (ContextMenuArgs){ .uint8 = (uint8_t) TOWER_TYPE_BALLISTA}); 850 menu[menuCount++].buttonState.isDisabled = level->playerGold < GetTowerCosts(TOWER_TYPE_BALLISTA); 851 852 menu[menuCount] = ContextMenuItemButton(menuIndex++, 853 TextFormat("Catapult: %dG", GetTowerCosts(TOWER_TYPE_CATAPULT)), 854 OnContextMenuBuild, (ContextMenuArgs){ .uint8 = (uint8_t) TOWER_TYPE_CATAPULT}); 855 menu[menuCount++].buttonState.isDisabled = level->playerGold < GetTowerCosts(TOWER_TYPE_CATAPULT); 856 } 857 858 if (DrawContextMenu(level, anchorLow, anchorHigh, 150, menu)) 859 { 860 level->placementContextMenuStatus = -1; 861 }
862 } 863 864 void DrawLevelBuildingState(Level *level) 865 { 866 BeginMode3D(level->camera); 867 DrawLevelGround(level); 868 869 PathFindingMapUpdate(0, 0); 870 TowerDraw(); 871 EnemyDraw(); 872 ProjectileDraw(); 873 ParticleDraw(); 874 DrawEnemyPaths(level); 875 876 guiState.isBlocked = 0; 877 878 // when the context menu is not active, we update the placement position 879 if (level->placementContextMenuStatus == 0) 880 { 881 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera); 882 float hitDistance = ray.position.y / -ray.direction.y; 883 float hitX = ray.direction.x * hitDistance + ray.position.x; 884 float hitY = ray.direction.z * hitDistance + ray.position.z; 885 level->placementX = (int)floorf(hitX + 0.5f); 886 level->placementY = (int)floorf(hitY + 0.5f); 887 } 888 889 // Hover rectangle, when the mouse is over the map 890 int isHovering = level->placementX >= -5 && level->placementX <= 5 && level->placementY >= -5 && level->placementY <= 5; 891 if (isHovering) 892 {
893 DrawCubeWires((Vector3){level->placementX, 0.75f, level->placementY}, 1.0f, 1.5f, 1.0f, RED);
894 } 895 896 EndMode3D(); 897 898 TowerDrawHealthBars(level->camera); 899 900 DrawTitle("Building phase"); 901 902 // Draw the context menu when the context menu is active 903 if (level->placementContextMenuStatus >= 1) 904 { 905 Tower *tower = TowerGetAt(level->placementX, level->placementY); 906 float maxHitpoints = 0.0f; 907 float hp = 0.0f; 908 float damageFactor = 0.0f; 909 int32_t sellValue = 0; 910 911 if (tower) 912 { 913 maxHitpoints = TowerGetMaxHealth(tower); 914 hp = maxHitpoints - tower->damage; 915 damageFactor = 1.0f - tower->damage / maxHitpoints; 916 sellValue = (int32_t) ceilf(GetTowerCosts(tower->towerType) * 0.5f * damageFactor); 917 } 918 919 ContextMenuItem menu[12] = {0}; 920 int menuCount = 0; 921 int menuIndex = 0;
922 Vector2 anchorLow = GetWorldToScreen((Vector3){level->placementX, -0.25f, level->placementY}, level->camera); 923 Vector2 anchorHigh = GetWorldToScreen((Vector3){level->placementX, 2.0f, level->placementY}, level->camera); 924 925 if (level->placementContextMenuType == CONTEXT_MENU_TYPE_MAIN) 926 { 927 DrawLevelBuildingStateMainContextMenu(level, tower, sellValue, hp, maxHitpoints, anchorLow, anchorHigh); 928 } 929 else if (level->placementContextMenuType == CONTEXT_MENU_TYPE_UPGRADE) 930 { 931 int totalLevel = tower->upgradeState.damage + tower->upgradeState.speed + tower->upgradeState.range; 932 int costs = totalLevel * 4; 933 int isMaxLevel = totalLevel >= TOWER_MAX_STAGE; 934 menu[menuCount++] = ContextMenuItemText(menuIndex++, TextFormat("%s tower, stage %d%s", 935 GetTowerName(tower->towerType), totalLevel, isMaxLevel ? " (max)" : ""), 0.5f);
936 int buttonMenuIndex = menuIndex; 937 menu[menuCount++] = ContextMenuItemButton(menuIndex++, TextFormat("Speed: %d (%dG)", tower->upgradeState.speed, costs), 938 OnContextMenuDoUpgrade, (ContextMenuArgs){.tower = tower, .uint8 = UPGRADE_TYPE_SPEED, .int32 = costs}); 939 menu[menuCount++] = ContextMenuItemButton(menuIndex++, TextFormat("Damage: %d (%dG)", tower->upgradeState.damage, costs), 940 OnContextMenuDoUpgrade, (ContextMenuArgs){.tower = tower, .uint8 = UPGRADE_TYPE_DAMAGE, .int32 = costs}); 941 menu[menuCount++] = ContextMenuItemButton(menuIndex++, TextFormat("Range: %d (%dG)", tower->upgradeState.range, costs), 942 OnContextMenuDoUpgrade, (ContextMenuArgs){.tower = tower, .uint8 = UPGRADE_TYPE_RANGE, .int32 = costs}); 943 944 // check if buttons should be disabled 945 if (isMaxLevel || level->playerGold < costs) 946 { 947 for (int i = buttonMenuIndex; i < menuCount; i++) 948 { 949 menu[i].buttonState.isDisabled = 1; 950 } 951 } 952 953 if (DrawContextMenu(level, anchorLow, anchorHigh, 220, menu)) 954 { 955 level->placementContextMenuType = CONTEXT_MENU_TYPE_MAIN; 956 } 957 } 958 else if (level->placementContextMenuType == CONTEXT_MENU_TYPE_SELL_CONFIRM) 959 { 960 menu[menuCount++] = ContextMenuItemText(menuIndex++, TextFormat("Sell %s?", GetTowerName(tower->towerType)), 0.5f); 961 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "Yes", OnContextMenuSellConfirm, 962 (ContextMenuArgs){.tower = tower, .int32 = sellValue}); 963 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "No", OnContextMenuSellCancel, (ContextMenuArgs){0}); 964 Vector2 anchorLow = {GetScreenWidth() * 0.5f, GetScreenHeight() * 0.5f}; 965 if (DrawContextMenu(level, anchorLow, anchorLow, 150, menu)) 966 { 967 level->placementContextMenuStatus = -1; 968 level->placementContextMenuType = CONTEXT_MENU_TYPE_MAIN; 969 } 970 } 971 } 972 973 // Activate the context menu when the mouse is clicked and the context menu is not active 974 else if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON) && isHovering && level->placementContextMenuStatus <= 0) 975 { 976 level->placementContextMenuStatus += 1; 977 } 978 979 if (level->placementContextMenuStatus == 0) 980 { 981 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 982 { 983 level->nextState = LEVEL_STATE_RESET; 984 } 985 986 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0)) 987 { 988 level->nextState = LEVEL_STATE_BATTLE; 989 } 990 991 } 992 } 993 994 void InitBattleStateConditions(Level *level) 995 { 996 level->state = LEVEL_STATE_BATTLE; 997 level->nextState = LEVEL_STATE_NONE; 998 level->waveEndTimer = 0.0f; 999 for (int i = 0; i < 10; i++) 1000 { 1001 EnemyWave *wave = &level->waves[i]; 1002 wave->spawned = 0; 1003 wave->timeToSpawnNext = wave->delay; 1004 } 1005 } 1006 1007 void DrawLevelBattleState(Level *level) 1008 { 1009 BeginMode3D(level->camera); 1010 DrawLevelGround(level); 1011 TowerDraw(); 1012 EnemyDraw(); 1013 ProjectileDraw(); 1014 ParticleDraw(); 1015 guiState.isBlocked = 0; 1016 EndMode3D(); 1017 1018 EnemyDrawHealthbars(level->camera); 1019 TowerDrawHealthBars(level->camera); 1020 1021 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 1022 { 1023 level->nextState = LEVEL_STATE_RESET; 1024 } 1025 1026 int maxCount = 0; 1027 int remainingCount = 0; 1028 for (int i = 0; i < 10; i++) 1029 { 1030 EnemyWave *wave = &level->waves[i]; 1031 if (wave->wave != level->currentWave) 1032 { 1033 continue; 1034 } 1035 maxCount += wave->count; 1036 remainingCount += wave->count - wave->spawned; 1037 } 1038 int aliveCount = EnemyCount(); 1039 remainingCount += aliveCount; 1040 1041 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount); 1042 DrawTitle(text); 1043 } 1044 1045 void DrawLevel(Level *level) 1046 { 1047 switch (level->state) 1048 { 1049 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break; 1050 case LEVEL_STATE_BUILDING_PLACEMENT: DrawLevelBuildingPlacementState(level); break; 1051 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break; 1052 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break; 1053 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break; 1054 default: break; 1055 } 1056 1057 DrawLevelHud(level); 1058 } 1059 1060 void UpdateLevel(Level *level) 1061 { 1062 if (level->state == LEVEL_STATE_BATTLE) 1063 { 1064 int activeWaves = 0; 1065 for (int i = 0; i < 10; i++) 1066 { 1067 EnemyWave *wave = &level->waves[i]; 1068 if (wave->spawned >= wave->count || wave->wave != level->currentWave) 1069 { 1070 continue; 1071 } 1072 activeWaves++; 1073 wave->timeToSpawnNext -= gameTime.deltaTime; 1074 if (wave->timeToSpawnNext <= 0.0f) 1075 { 1076 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y); 1077 if (enemy) 1078 { 1079 wave->timeToSpawnNext = wave->interval; 1080 wave->spawned++; 1081 } 1082 } 1083 } 1084 if (GetTowerByType(TOWER_TYPE_BASE) == 0) { 1085 level->waveEndTimer += gameTime.deltaTime; 1086 if (level->waveEndTimer >= 2.0f) 1087 { 1088 level->nextState = LEVEL_STATE_LOST_WAVE; 1089 } 1090 } 1091 else if (activeWaves == 0 && EnemyCount() == 0) 1092 { 1093 level->waveEndTimer += gameTime.deltaTime; 1094 if (level->waveEndTimer >= 2.0f) 1095 { 1096 level->nextState = LEVEL_STATE_WON_WAVE; 1097 } 1098 } 1099 } 1100 1101 PathFindingMapUpdate(0, 0); 1102 EnemyUpdate(); 1103 TowerUpdate(); 1104 ProjectileUpdate(); 1105 ParticleUpdate(); 1106 1107 if (level->nextState == LEVEL_STATE_RESET) 1108 { 1109 InitLevel(level); 1110 } 1111 1112 if (level->nextState == LEVEL_STATE_BATTLE) 1113 { 1114 InitBattleStateConditions(level); 1115 } 1116 1117 if (level->nextState == LEVEL_STATE_WON_WAVE) 1118 { 1119 level->currentWave++; 1120 level->state = LEVEL_STATE_WON_WAVE; 1121 } 1122 1123 if (level->nextState == LEVEL_STATE_LOST_WAVE) 1124 { 1125 level->state = LEVEL_STATE_LOST_WAVE; 1126 } 1127 1128 if (level->nextState == LEVEL_STATE_BUILDING) 1129 { 1130 level->state = LEVEL_STATE_BUILDING; 1131 level->placementContextMenuStatus = 0; 1132 } 1133 1134 if (level->nextState == LEVEL_STATE_BUILDING_PLACEMENT) 1135 { 1136 level->state = LEVEL_STATE_BUILDING_PLACEMENT; 1137 level->placementTransitionPosition = (Vector2){ 1138 level->placementX, level->placementY}; 1139 // initialize the spring to the current position 1140 level->placementTowerSpring = (PhysicsPoint){ 1141 .position = (Vector3){level->placementX, 8.0f, level->placementY}, 1142 .velocity = (Vector3){0.0f, 0.0f, 0.0f}, 1143 }; 1144 level->placementPhase = PLACEMENT_PHASE_STARTING; 1145 level->placementTimer = 0.0f; 1146 } 1147 1148 if (level->nextState == LEVEL_STATE_WON_LEVEL) 1149 { 1150 // make something of this later 1151 InitLevel(level); 1152 } 1153 1154 level->nextState = LEVEL_STATE_NONE; 1155 } 1156 1157 float nextSpawnTime = 0.0f; 1158 1159 void ResetGame() 1160 { 1161 InitLevel(currentLevel); 1162 } 1163 1164 void InitGame() 1165 { 1166 TowerInit(); 1167 EnemyInit(); 1168 ProjectileInit(); 1169 ParticleInit(); 1170 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f); 1171 1172 currentLevel = levels; 1173 InitLevel(currentLevel); 1174 } 1175 1176 //# Immediate GUI functions 1177 1178 void DrawHealthBar(Camera3D camera, Vector3 position, Vector2 screenOffset, float healthRatio, Color barColor, float healthBarWidth) 1179 { 1180 const float healthBarHeight = 6.0f; 1181 const float healthBarOffset = 15.0f; 1182 const float inset = 2.0f; 1183 const float innerWidth = healthBarWidth - inset * 2; 1184 const float innerHeight = healthBarHeight - inset * 2; 1185 1186 Vector2 screenPos = GetWorldToScreen(position, camera); 1187 screenPos = Vector2Add(screenPos, screenOffset); 1188 float centerX = screenPos.x - healthBarWidth * 0.5f; 1189 float topY = screenPos.y - healthBarOffset; 1190 DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK); 1191 float healthWidth = innerWidth * healthRatio; 1192 DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor); 1193 } 1194 1195 void DrawBoxedText(const char *text, int x, int y, int width, int height, float alignX, float alignY, Color textColor) 1196 { 1197 Vector2 textSize = MeasureTextEx(gameFontNormal, text, gameFontNormal.baseSize, 1); 1198 1199 DrawTextEx(gameFontNormal, text, (Vector2){ 1200 x + (width - textSize.x) * alignX, 1201 y + (height - textSize.y) * alignY 1202 }, gameFontNormal.baseSize, 1, textColor); 1203 } 1204 1205 int Button(const char *text, int x, int y, int width, int height, ButtonState *state) 1206 { 1207 Rectangle bounds = {x, y, width, height}; 1208 int isPressed = 0; 1209 int isSelected = state && state->isSelected; 1210 int isDisabled = state && state->isDisabled; 1211 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !isDisabled) 1212 { 1213 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) 1214 { 1215 isPressed = 1; 1216 } 1217 guiState.isBlocked = 1; 1218 DrawTextureNPatch(spriteSheet, isPressed ? uiButtonPressed : uiButtonHovered, 1219 bounds, Vector2Zero(), 0, WHITE); 1220 } 1221 else 1222 { 1223 DrawTextureNPatch(spriteSheet, isSelected ? uiButtonHovered : (isDisabled ? uiButtonDisabled : uiButtonNormal), 1224 bounds, Vector2Zero(), 0, WHITE); 1225 } 1226 Vector2 textSize = MeasureTextEx(gameFontNormal, text, gameFontNormal.baseSize, 1); 1227 Color textColor = isDisabled ? LIGHTGRAY : BLACK; 1228 DrawTextEx(gameFontNormal, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, gameFontNormal.baseSize, 1, textColor); 1229 return isPressed; 1230 } 1231 1232 //# Main game loop 1233 1234 void GameUpdate() 1235 { 1236 UpdateLevel(currentLevel); 1237 } 1238 1239 int main(void) 1240 { 1241 int screenWidth, screenHeight; 1242 GetPreferredSize(&screenWidth, &screenHeight); 1243 InitWindow(screenWidth, screenHeight, "Tower defense"); 1244 float gamespeed = 1.0f; 1245 SetTargetFPS(30); 1246 1247 LoadAssets(); 1248 InitGame(); 1249 1250 float pause = 1.0f; 1251 1252 while (!WindowShouldClose()) 1253 { 1254 if (IsPaused()) { 1255 // canvas is not visible in browser - do nothing 1256 continue; 1257 } 1258 1259 if (IsKeyPressed(KEY_T)) 1260 { 1261 gamespeed += 0.1f; 1262 if (gamespeed > 1.05f) gamespeed = 0.1f; 1263 } 1264 1265 if (IsKeyPressed(KEY_P)) 1266 { 1267 pause = pause > 0.5f ? 0.0f : 1.0f; 1268 } 1269 1270 float dt = GetFrameTime() * gamespeed * pause; 1271 // cap maximum delta time to 0.1 seconds to prevent large time steps 1272 if (dt > 0.1f) dt = 0.1f; 1273 gameTime.time += dt; 1274 gameTime.deltaTime = dt; 1275 gameTime.frameCount += 1; 1276 1277 float fixedTimeDiff = gameTime.time - gameTime.fixedTimeStart; 1278 gameTime.fixedStepCount = (uint8_t)(fixedTimeDiff / gameTime.fixedDeltaTime); 1279 1280 BeginDrawing(); 1281 ClearBackground((Color){0x4E, 0x63, 0x26, 0xFF}); 1282 1283 GameUpdate(); 1284 DrawLevel(currentLevel); 1285 1286 if (gamespeed != 1.0f) 1287 DrawText(TextFormat("Speed: %.1f", gamespeed), GetScreenWidth() - 180, 60, 20, WHITE); 1288 EndDrawing(); 1289 1290 gameTime.fixedTimeStart += gameTime.fixedStepCount * gameTime.fixedDeltaTime; 1291 } 1292 1293 CloseWindow(); 1294 1295 return 0; 1296 }
  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 
 21 #define ENEMY_TYPE_MINION 1
 22 #define ENEMY_TYPE_RUNNER 2
 23 #define ENEMY_TYPE_SHIELD 3
 24 #define ENEMY_TYPE_BOSS 4
 25 
 26 #define PARTICLE_MAX_COUNT 400
 27 #define PARTICLE_TYPE_NONE 0
 28 #define PARTICLE_TYPE_EXPLOSION 1
 29 
 30 typedef struct Particle
 31 {
 32   uint8_t particleType;
 33   float spawnTime;
 34   float lifetime;
 35   Vector3 position;
 36   Vector3 velocity;
 37   Vector3 scale;
 38 } Particle;
 39 
 40 #define TOWER_MAX_COUNT 400
 41 enum TowerType
 42 {
 43   TOWER_TYPE_NONE,
 44   TOWER_TYPE_BASE,
 45   TOWER_TYPE_ARCHER,
 46   TOWER_TYPE_BALLISTA,
 47   TOWER_TYPE_CATAPULT,
 48   TOWER_TYPE_WALL,
 49   TOWER_TYPE_COUNT
 50 };
 51 
 52 typedef struct HitEffectConfig
 53 {
 54   float damage;
 55   float areaDamageRadius;
 56   float pushbackPowerDistance;
 57 } HitEffectConfig;
 58 
 59 typedef struct TowerTypeConfig
 60 {
 61   const char *name;
 62   float cooldown;
 63   float range;
 64   float projectileSpeed;
 65   
 66   uint8_t cost;
 67   uint8_t projectileType;
 68   uint16_t maxHealth;
 69 
 70   HitEffectConfig hitEffect;
 71 } TowerTypeConfig;
 72 
73 #define TOWER_MAX_STAGE 10 74
75 typedef struct TowerUpgradeState 76 { 77 uint8_t range; 78 uint8_t damage; 79 uint8_t speed; 80 } TowerUpgradeState; 81 82 typedef struct Tower 83 { 84 int16_t x, y; 85 uint8_t towerType; 86 TowerUpgradeState upgradeState; 87 Vector2 lastTargetPosition; 88 float cooldown; 89 float damage; 90 } Tower; 91 92 typedef struct GameTime 93 { 94 float time; 95 float deltaTime; 96 uint32_t frameCount; 97 98 float fixedDeltaTime; 99 // leaving the fixed time stepping to the update functions, 100 // we need to know the fixed time at the start of the frame 101 float fixedTimeStart; 102 // and the number of fixed steps that we have to make this frame 103 // The fixedTime is fixedTimeStart + n * fixedStepCount 104 uint8_t fixedStepCount; 105 } GameTime; 106 107 typedef struct ButtonState { 108 char isSelected; 109 char isDisabled; 110 } ButtonState; 111 112 typedef struct GUIState { 113 int isBlocked; 114 } GUIState; 115 116 typedef enum LevelState 117 { 118 LEVEL_STATE_NONE, 119 LEVEL_STATE_BUILDING, 120 LEVEL_STATE_BUILDING_PLACEMENT, 121 LEVEL_STATE_BATTLE, 122 LEVEL_STATE_WON_WAVE, 123 LEVEL_STATE_LOST_WAVE, 124 LEVEL_STATE_WON_LEVEL, 125 LEVEL_STATE_RESET, 126 } LevelState; 127 128 typedef struct EnemyWave { 129 uint8_t enemyType; 130 uint8_t wave; 131 uint16_t count; 132 float interval; 133 float delay; 134 Vector2 spawnPosition; 135 136 uint16_t spawned; 137 float timeToSpawnNext; 138 } EnemyWave; 139 140 #define ENEMY_MAX_WAVE_COUNT 10 141 142 typedef enum PlacementPhase 143 { 144 PLACEMENT_PHASE_STARTING, 145 PLACEMENT_PHASE_MOVING, 146 PLACEMENT_PHASE_PLACING, 147 } PlacementPhase; 148 149 typedef struct Level 150 { 151 int seed; 152 LevelState state; 153 LevelState nextState; 154 Camera3D camera; 155 int placementMode; 156 PlacementPhase placementPhase; 157 float placementTimer; 158 159 int16_t placementX; 160 int16_t placementY; 161 int8_t placementContextMenuStatus; 162 int8_t placementContextMenuType; 163 164 Vector2 placementTransitionPosition; 165 PhysicsPoint placementTowerSpring; 166 167 int initialGold; 168 int playerGold; 169 170 EnemyWave waves[ENEMY_MAX_WAVE_COUNT]; 171 int currentWave; 172 float waveEndTimer; 173 } Level; 174 175 typedef struct DeltaSrc 176 { 177 char x, y; 178 } DeltaSrc; 179 180 typedef struct PathfindingMap 181 { 182 int width, height; 183 float scale; 184 float *distances; 185 long *towerIndex; 186 DeltaSrc *deltaSrc; 187 float maxDistance; 188 Matrix toMapSpace; 189 Matrix toWorldSpace; 190 } PathfindingMap; 191 192 // when we execute the pathfinding algorithm, we need to store the active nodes 193 // in a queue. Each node has a position, a distance from the start, and the 194 // position of the node that we came from. 195 typedef struct PathfindingNode 196 { 197 int16_t x, y, fromX, fromY; 198 float distance; 199 } PathfindingNode; 200 201 typedef struct EnemyId 202 { 203 uint16_t index; 204 uint16_t generation; 205 } EnemyId; 206 207 typedef struct EnemyClassConfig 208 { 209 float speed; 210 float health; 211 float shieldHealth; 212 float shieldDamageAbsorption; 213 float radius; 214 float maxAcceleration; 215 float requiredContactTime; 216 float explosionDamage; 217 float explosionRange; 218 float explosionPushbackPower; 219 int goldValue; 220 } EnemyClassConfig; 221 222 typedef struct Enemy 223 { 224 int16_t currentX, currentY; 225 int16_t nextX, nextY; 226 Vector2 simPosition; 227 Vector2 simVelocity; 228 uint16_t generation; 229 float walkedDistance; 230 float startMovingTime; 231 float damage, futureDamage; 232 float shieldDamage; 233 float contactTime; 234 uint8_t enemyType; 235 uint8_t movePathCount; 236 Vector2 movePath[ENEMY_MAX_PATH_COUNT]; 237 } Enemy; 238 239 // a unit that uses sprites to be drawn 240 #define SPRITE_UNIT_ANIMATION_COUNT 6 241 #define SPRITE_UNIT_PHASE_WEAPON_IDLE 1 242 #define SPRITE_UNIT_PHASE_WEAPON_COOLDOWN 2 243 #define SPRITE_UNIT_PHASE_WEAPON_SHIELD 3 244 245 typedef struct SpriteAnimation 246 { 247 Rectangle srcRect; 248 Vector2 offset; 249 uint8_t animationId; 250 uint8_t frameCount; 251 uint8_t frameWidth; 252 float frameDuration; 253 } SpriteAnimation; 254 255 typedef struct SpriteUnit 256 { 257 float scale; 258 SpriteAnimation animations[SPRITE_UNIT_ANIMATION_COUNT]; 259 } SpriteUnit; 260 261 #define PROJECTILE_MAX_COUNT 1200 262 #define PROJECTILE_TYPE_NONE 0 263 #define PROJECTILE_TYPE_ARROW 1 264 #define PROJECTILE_TYPE_CATAPULT 2 265 #define PROJECTILE_TYPE_BALLISTA 3 266 267 typedef struct Projectile 268 { 269 uint8_t projectileType; 270 float shootTime; 271 float arrivalTime; 272 float distance; 273 Vector3 position; 274 Vector3 target; 275 Vector3 directionNormal; 276 EnemyId targetEnemy; 277 HitEffectConfig hitEffectConfig; 278 } Projectile; 279 280 //# Function declarations 281 float TowerGetMaxHealth(Tower *tower); 282 int Button(const char *text, int x, int y, int width, int height, ButtonState *state); 283 int EnemyAddDamageRange(Vector2 position, float range, float damage); 284 int EnemyAddDamage(Enemy *enemy, float damage); 285 286 //# Enemy functions 287 void EnemyInit(); 288 void EnemyDraw(); 289 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource); 290 void EnemyUpdate(); 291 float EnemyGetCurrentMaxSpeed(Enemy *enemy); 292 float EnemyGetMaxHealth(Enemy *enemy); 293 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY); 294 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount); 295 EnemyId EnemyGetId(Enemy *enemy); 296 Enemy *EnemyTryResolve(EnemyId enemyId); 297 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY); 298 int EnemyAddDamage(Enemy *enemy, float damage); 299 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range); 300 int EnemyCount(); 301 void EnemyDrawHealthbars(Camera3D camera); 302 303 //# Tower functions 304 void TowerInit(); 305 Tower *TowerGetAt(int16_t x, int16_t y); 306 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y); 307 Tower *GetTowerByType(uint8_t towerType); 308 int GetTowerCosts(uint8_t towerType); 309 const char *GetTowerName(uint8_t towerType); 310 float TowerGetMaxHealth(Tower *tower); 311 void TowerDraw(); 312 void TowerDrawSingle(Tower tower); 313 void TowerUpdate(); 314 void TowerDrawHealthBars(Camera3D camera); 315 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase); 316 317 //# Particles 318 void ParticleInit(); 319 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime); 320 void ParticleUpdate(); 321 void ParticleDraw(); 322 323 //# Projectiles 324 void ProjectileInit(); 325 void ProjectileDraw(); 326 void ProjectileUpdate(); 327 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig); 328 329 //# Pathfinding map 330 void PathfindingMapInit(int width, int height, Vector3 translate, float scale); 331 float PathFindingGetDistance(int mapX, int mapY); 332 Vector2 PathFindingGetGradient(Vector3 world); 333 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY); 334 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells); 335 void PathFindingMapDraw(); 336 337 //# UI 338 void DrawHealthBar(Camera3D camera, Vector3 position, Vector2 screenOffset, float healthRatio, Color barColor, float healthBarWidth); 339 340 //# Level 341 void DrawLevelGround(Level *level); 342 void DrawEnemyPath(Level *level, Color arrowColor); 343 344 //# variables 345 extern Level *currentLevel; 346 extern Enemy enemies[ENEMY_MAX_COUNT]; 347 extern int enemyCount; 348 extern EnemyClassConfig enemyClassConfigs[]; 349 350 extern GUIState guiState; 351 extern GameTime gameTime; 352 extern Tower towers[TOWER_MAX_COUNT]; 353 extern int towerCount; 354 355 extern Texture2D palette, spriteSheet; 356 357 #endif
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 
  4 static TowerTypeConfig towerTypeConfigs[TOWER_TYPE_COUNT] = {
  5     [TOWER_TYPE_BASE] = {
  6         .name = "Castle",
  7         .maxHealth = 10,
  8     },
  9     [TOWER_TYPE_ARCHER] = {
 10         .name = "Archer",
 11         .cooldown = 0.5f,
 12         .range = 3.0f,
 13         .cost = 6,
 14         .maxHealth = 10,
 15         .projectileSpeed = 4.0f,
 16         .projectileType = PROJECTILE_TYPE_ARROW,
 17         .hitEffect = {
 18           .damage = 3.0f,
 19         }
 20     },
 21     [TOWER_TYPE_BALLISTA] = {
 22         .name = "Ballista",
 23         .cooldown = 1.5f,
 24         .range = 6.0f,
 25         .cost = 9,
 26         .maxHealth = 10,
 27         .projectileSpeed = 10.0f,
 28         .projectileType = PROJECTILE_TYPE_BALLISTA,
 29         .hitEffect = {
 30           .damage = 8.0f,
 31           .pushbackPowerDistance = 0.25f,
 32         }
 33     },
 34     [TOWER_TYPE_CATAPULT] = {
 35         .name = "Catapult",
 36         .cooldown = 1.7f,
 37         .range = 5.0f,
 38         .cost = 10,
 39         .maxHealth = 10,
 40         .projectileSpeed = 3.0f,
 41         .projectileType = PROJECTILE_TYPE_CATAPULT,
 42         .hitEffect = {
 43           .damage = 2.0f,
 44           .areaDamageRadius = 1.75f,
 45         }
 46     },
 47     [TOWER_TYPE_WALL] = {
 48         .name = "Wall",
 49         .cost = 2,
 50         .maxHealth = 10,
 51     },
 52 };
 53 
 54 Tower towers[TOWER_MAX_COUNT];
 55 int towerCount = 0;
 56 
 57 Model towerModels[TOWER_TYPE_COUNT];
 58 
 59 // definition of our archer unit
 60 SpriteUnit archerUnit = {
 61   .animations[0] = {
 62     .srcRect = {0, 0, 16, 16},
 63     .offset = {7, 1},
 64     .frameCount = 1,
 65     .frameDuration = 0.0f,
 66   },
 67   .animations[1] = {
 68     .animationId = SPRITE_UNIT_PHASE_WEAPON_COOLDOWN,
 69     .srcRect = {16, 0, 6, 16},
 70     .offset = {8, 0},
 71   },
 72   .animations[2] = {
 73     .animationId = SPRITE_UNIT_PHASE_WEAPON_IDLE,
 74     .srcRect = {22, 0, 11, 16},
 75     .offset = {10, 0},
 76   },
 77 };
 78 
 79 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase)
 80 {
 81   float unitScale = unit.scale == 0 ? 1.0f : unit.scale;
 82   float xScale = flip ? -1.0f : 1.0f;
 83   Camera3D camera = currentLevel->camera;
 84   float size = 0.5f * unitScale;
 85   // we want the sprite to face the camera, so we need to calculate the up vector
 86   Vector3 forward = Vector3Subtract(camera.target, camera.position);
 87   Vector3 up = {0, 1, 0};
 88   Vector3 right = Vector3CrossProduct(forward, up);
 89   up = Vector3Normalize(Vector3CrossProduct(right, forward));
 90   
 91   for (int i=0;i<SPRITE_UNIT_ANIMATION_COUNT;i++)
 92   {
 93     SpriteAnimation anim = unit.animations[i];
 94     if (anim.animationId != phase && anim.animationId != 0)
 95     {
 96       continue;
 97     }
 98     Rectangle srcRect = anim.srcRect;
 99     if (anim.frameCount > 1)
100     {
101       int w = anim.frameWidth > 0 ? anim.frameWidth : srcRect.width;
102       srcRect.x += (int)(t / anim.frameDuration) % anim.frameCount * w;
103     }
104     Vector2 offset = { anim.offset.x / 16.0f * size, anim.offset.y / 16.0f * size * xScale };
105     Vector2 scale = { srcRect.width / 16.0f * size, srcRect.height / 16.0f * size };
106     
107     if (flip)
108     {
109       srcRect.x += srcRect.width;
110       srcRect.width = -srcRect.width;
111       offset.x = scale.x - offset.x;
112     }
113     DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
114     // move the sprite slightly towards the camera to avoid z-fighting
115     position = Vector3Add(position, Vector3Scale(Vector3Normalize(forward), -0.01f));  
116   }
117 }
118 
119 void TowerInit()
120 {
121   for (int i = 0; i < TOWER_MAX_COUNT; i++)
122   {
123     towers[i] = (Tower){0};
124   }
125   towerCount = 0;
126 
127   towerModels[TOWER_TYPE_BASE] = LoadModel("data/keep.glb");
128   towerModels[TOWER_TYPE_WALL] = LoadModel("data/wall-0000.glb");
129 
130   for (int i = 0; i < TOWER_TYPE_COUNT; i++)
131   {
132     if (towerModels[i].materials)
133     {
134       // assign the palette texture to the material of the model (0 is not used afaik)
135       towerModels[i].materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
136     }
137   }
138 }
139 
140 static void TowerGunUpdate(Tower *tower)
141 {
142   TowerTypeConfig config = towerTypeConfigs[tower->towerType];
143   if (tower->cooldown <= 0.0f)
144   {
145     Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, config.range);
146     if (enemy)
147     {
148       tower->cooldown = config.cooldown;
149       // shoot the enemy; determine future position of the enemy
150       float bulletSpeed = config.projectileSpeed;
151       Vector2 velocity = enemy->simVelocity;
152       Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0);
153       Vector2 towerPosition = {tower->x, tower->y};
154       float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed;
155       for (int i = 0; i < 8; i++) {
156         velocity = enemy->simVelocity;
157         futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0);
158         float distance = Vector2Distance(towerPosition, futurePosition);
159         float eta2 = distance / bulletSpeed;
160         if (fabs(eta - eta2) < 0.01f) {
161           break;
162         }
163         eta = (eta2 + eta) * 0.5f;
164       }
165 
166       ProjectileTryAdd(config.projectileType, enemy, 
167         (Vector3){towerPosition.x, 1.33f, towerPosition.y}, 
168         (Vector3){futurePosition.x, 0.25f, futurePosition.y},
169         bulletSpeed, config.hitEffect);
170       enemy->futureDamage += config.hitEffect.damage;
171       tower->lastTargetPosition = futurePosition;
172     }
173   }
174   else
175   {
176     tower->cooldown -= gameTime.deltaTime;
177   }
178 }
179 
180 Tower *TowerGetAt(int16_t x, int16_t y)
181 {
182   for (int i = 0; i < towerCount; i++)
183   {
184     if (towers[i].x == x && towers[i].y == y && towers[i].towerType != TOWER_TYPE_NONE)
185     {
186       return &towers[i];
187     }
188   }
189   return 0;
190 }
191 
192 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y)
193 {
194   if (towerCount >= TOWER_MAX_COUNT)
195   {
196     return 0;
197   }
198 
199   Tower *tower = TowerGetAt(x, y);
200   if (tower)
201   {
202     return 0;
203   }
204 
205   tower = &towers[towerCount++];
206 *tower = (Tower){ 207 .x = x, 208 .y = y, 209 .towerType = towerType, 210 .cooldown = 0.0f, 211 .damage = 0.0f, 212 };
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 const char *GetTowerName(uint8_t towerType) 229 { 230 return towerTypeConfigs[towerType].name; 231 } 232 233 int GetTowerCosts(uint8_t towerType) 234 { 235 return towerTypeConfigs[towerType].cost; 236 } 237 238 float TowerGetMaxHealth(Tower *tower) 239 { 240 return towerTypeConfigs[tower->towerType].maxHealth; 241 } 242 243 void TowerDrawSingle(Tower tower) 244 { 245 if (tower.towerType == TOWER_TYPE_NONE) 246 { 247 return; 248 } 249 250 switch (tower.towerType) 251 { 252 case TOWER_TYPE_ARCHER: 253 { 254 Vector2 screenPosTower = GetWorldToScreen((Vector3){tower.x, 0.0f, tower.y}, currentLevel->camera); 255 Vector2 screenPosTarget = GetWorldToScreen((Vector3){tower.lastTargetPosition.x, 0.0f, tower.lastTargetPosition.y}, currentLevel->camera); 256 DrawModel(towerModels[TOWER_TYPE_WALL], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE); 257 DrawSpriteUnit(archerUnit, (Vector3){tower.x, 1.0f, tower.y}, 0, screenPosTarget.x > screenPosTower.x, 258 tower.cooldown > 0.2f ? SPRITE_UNIT_PHASE_WEAPON_COOLDOWN : SPRITE_UNIT_PHASE_WEAPON_IDLE); 259 } 260 break; 261 case TOWER_TYPE_BALLISTA: 262 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, BROWN); 263 break; 264 case TOWER_TYPE_CATAPULT: 265 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, DARKGRAY); 266 break; 267 default: 268 if (towerModels[tower.towerType].materials) 269 { 270 DrawModel(towerModels[tower.towerType], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE); 271 } else { 272 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY); 273 } 274 break; 275 } 276 } 277 278 void TowerDraw() 279 { 280 for (int i = 0; i < towerCount; i++) 281 { 282 TowerDrawSingle(towers[i]); 283 } 284 } 285 286 void TowerUpdate() 287 { 288 for (int i = 0; i < towerCount; i++) 289 { 290 Tower *tower = &towers[i]; 291 switch (tower->towerType) 292 { 293 case TOWER_TYPE_CATAPULT: 294 case TOWER_TYPE_BALLISTA: 295 case TOWER_TYPE_ARCHER: 296 TowerGunUpdate(tower); 297 break; 298 } 299 } 300 } 301 302 void TowerDrawHealthBars(Camera3D camera) 303 { 304 for (int i = 0; i < towerCount; i++) 305 { 306 Tower *tower = &towers[i]; 307 if (tower->towerType == TOWER_TYPE_NONE || tower->damage <= 0.0f) 308 { 309 continue; 310 } 311 312 Vector3 position = (Vector3){tower->x, 0.5f, tower->y}; 313 float maxHealth = TowerGetMaxHealth(tower); 314 float health = maxHealth - tower->damage; 315 float healthRatio = health / maxHealth; 316 317 DrawHealthBar(camera, position, Vector2Zero(), healthRatio, GREEN, 35.0f); 318 } 319 }
  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 #include <stdlib.h>
  4 #include <math.h>
  5 #include <rlgl.h>
  6 
  7 EnemyClassConfig enemyClassConfigs[] = {
  8     [ENEMY_TYPE_MINION] = {
  9       .health = 10.0f, 
 10       .speed = 0.6f, 
 11       .radius = 0.25f, 
 12       .maxAcceleration = 1.0f,
 13       .explosionDamage = 1.0f,
 14       .requiredContactTime = 0.5f,
 15       .explosionRange = 1.0f,
 16       .explosionPushbackPower = 0.25f,
 17       .goldValue = 1,
 18     },
 19     [ENEMY_TYPE_RUNNER] = {
 20       .health = 5.0f, 
 21       .speed = 1.0f, 
 22       .radius = 0.25f, 
 23       .maxAcceleration = 2.0f,
 24       .explosionDamage = 1.0f,
 25       .requiredContactTime = 0.5f,
 26       .explosionRange = 1.0f,
 27       .explosionPushbackPower = 0.25f,
 28       .goldValue = 2,
 29     },
 30     [ENEMY_TYPE_SHIELD] = {
 31       .health = 8.0f, 
 32       .speed = 0.5f, 
 33       .radius = 0.25f, 
 34       .maxAcceleration = 1.0f,
 35       .explosionDamage = 2.0f,
 36       .requiredContactTime = 0.5f,
 37       .explosionRange = 1.0f,
 38       .explosionPushbackPower = 0.25f,
 39       .goldValue = 3,
 40       .shieldDamageAbsorption = 4.0f,
 41       .shieldHealth = 25.0f,
 42     },
 43     [ENEMY_TYPE_BOSS] = {
 44       .health = 50.0f, 
 45       .speed = 0.4f, 
 46       .radius = 0.25f, 
 47       .maxAcceleration = 1.0f,
 48       .explosionDamage = 5.0f,
 49       .requiredContactTime = 0.5f,
 50       .explosionRange = 1.0f,
 51       .explosionPushbackPower = 0.25f,
 52       .goldValue = 10,
 53     },
 54 };
 55 
 56 Enemy enemies[ENEMY_MAX_COUNT];
 57 int enemyCount = 0;
 58 
 59 SpriteUnit enemySprites[] = {
 60     [ENEMY_TYPE_MINION] = {
 61       .animations[0] = {
 62         .srcRect = {0, 17, 16, 15},
 63         .offset = {8.0f, 0.0f},
 64         .frameCount = 6,
 65         .frameDuration = 0.1f,
 66       },
 67       .animations[1] = {
 68         .srcRect = {1, 33, 15, 14},
 69         .offset = {7.0f, 0.0f},
 70         .frameCount = 6,
 71         .frameWidth = 16,
 72         .frameDuration = 0.1f,
 73       },
 74     },
 75     [ENEMY_TYPE_RUNNER] = {
 76       .scale = 0.75f,
 77       .animations[0] = {
 78         .srcRect = {0, 17, 16, 15},
 79         .offset = {8.0f, 0.0f},
 80         .frameCount = 6,
 81         .frameDuration = 0.1f,
 82       },
 83     },
 84     [ENEMY_TYPE_SHIELD] = {
 85       .animations[0] = {
 86         .srcRect = {0, 17, 16, 15},
 87         .offset = {8.0f, 0.0f},
 88         .frameCount = 6,
 89         .frameDuration = 0.1f,
 90       },
 91       .animations[1] = {
 92         .animationId = SPRITE_UNIT_PHASE_WEAPON_SHIELD,
 93         .srcRect = {99, 17, 10, 11},
 94         .offset = {7.0f, 0.0f},
 95       },
 96     },
 97     [ENEMY_TYPE_BOSS] = {
 98       .scale = 1.5f,
 99       .animations[0] = {
100         .srcRect = {0, 17, 16, 15},
101         .offset = {8.0f, 0.0f},
102         .frameCount = 6,
103         .frameDuration = 0.1f,
104       },
105       .animations[1] = {
106         .srcRect = {97, 29, 14, 7},
107         .offset = {7.0f, -9.0f},
108       },
109     },
110 };
111 
112 void EnemyInit()
113 {
114   for (int i = 0; i < ENEMY_MAX_COUNT; i++)
115   {
116     enemies[i] = (Enemy){0};
117   }
118   enemyCount = 0;
119 }
120 
121 float EnemyGetCurrentMaxSpeed(Enemy *enemy)
122 {
123   return enemyClassConfigs[enemy->enemyType].speed;
124 }
125 
126 float EnemyGetMaxHealth(Enemy *enemy)
127 {
128   return enemyClassConfigs[enemy->enemyType].health;
129 }
130 
131 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY)
132 {
133   int16_t castleX = 0;
134   int16_t castleY = 0;
135   int16_t dx = castleX - currentX;
136   int16_t dy = castleY - currentY;
137   if (dx == 0 && dy == 0)
138   {
139     *nextX = currentX;
140     *nextY = currentY;
141     return 1;
142   }
143   Vector2 gradient = PathFindingGetGradient((Vector3){currentX, 0, currentY});
144 
145   if (gradient.x == 0 && gradient.y == 0)
146   {
147     *nextX = currentX;
148     *nextY = currentY;
149     return 1;
150   }
151 
152   if (fabsf(gradient.x) > fabsf(gradient.y))
153   {
154     *nextX = currentX + (int16_t)(gradient.x > 0.0f ? 1 : -1);
155     *nextY = currentY;
156     return 0;
157   }
158   *nextX = currentX;
159   *nextY = currentY + (int16_t)(gradient.y > 0.0f ? 1 : -1);
160   return 0;
161 }
162 
163 
164 // this function predicts the movement of the unit for the next deltaT seconds
165 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount)
166 {
167   const float pointReachedDistance = 0.25f;
168   const float pointReachedDistance2 = pointReachedDistance * pointReachedDistance;
169   const float maxSimStepTime = 0.015625f;
170   
171   float maxAcceleration = enemyClassConfigs[enemy->enemyType].maxAcceleration;
172   float maxSpeed = EnemyGetCurrentMaxSpeed(enemy);
173   int16_t nextX = enemy->nextX;
174   int16_t nextY = enemy->nextY;
175   Vector2 position = enemy->simPosition;
176   int passedCount = 0;
177   for (float t = 0.0f; t < deltaT; t += maxSimStepTime)
178   {
179     float stepTime = fminf(deltaT - t, maxSimStepTime);
180     Vector2 target = (Vector2){nextX, nextY};
181     float speed = Vector2Length(*velocity);
182     // draw the target position for debugging
183     //DrawCubeWires((Vector3){target.x, 0.2f, target.y}, 0.1f, 0.4f, 0.1f, RED);
184     Vector2 lookForwardPos = Vector2Add(position, Vector2Scale(*velocity, speed));
185     if (Vector2DistanceSqr(target, lookForwardPos) <= pointReachedDistance2)
186     {
187       // we reached the target position, let's move to the next waypoint
188       EnemyGetNextPosition(nextX, nextY, &nextX, &nextY);
189       target = (Vector2){nextX, nextY};
190       // track how many waypoints we passed
191       passedCount++;
192     }
193     
194     // acceleration towards the target
195     Vector2 unitDirection = Vector2Normalize(Vector2Subtract(target, lookForwardPos));
196     Vector2 acceleration = Vector2Scale(unitDirection, maxAcceleration * stepTime);
197     *velocity = Vector2Add(*velocity, acceleration);
198 
199     // limit the speed to the maximum speed
200     if (speed > maxSpeed)
201     {
202       *velocity = Vector2Scale(*velocity, maxSpeed / speed);
203     }
204 
205     // move the enemy
206     position = Vector2Add(position, Vector2Scale(*velocity, stepTime));
207   }
208 
209   if (waypointPassedCount)
210   {
211     (*waypointPassedCount) = passedCount;
212   }
213 
214   return position;
215 }
216 
217 void EnemyDraw()
218 {
219   rlDrawRenderBatchActive();
220   rlDisableDepthMask();
221   for (int i = 0; i < enemyCount; i++)
222   {
223     Enemy enemy = enemies[i];
224     if (enemy.enemyType == ENEMY_TYPE_NONE)
225     {
226       continue;
227     }
228 
229     Vector2 position = EnemyGetPosition(&enemy, gameTime.time - enemy.startMovingTime, &enemy.simVelocity, 0);
230     
231     // don't draw any trails for now; might replace this with footprints later
232     // if (enemy.movePathCount > 0)
233     // {
234     //   Vector3 p = {enemy.movePath[0].x, 0.2f, enemy.movePath[0].y};
235     //   DrawLine3D(p, (Vector3){position.x, 0.2f, position.y}, GREEN);
236     // }
237     // for (int j = 1; j < enemy.movePathCount; j++)
238     // {
239     //   Vector3 p = {enemy.movePath[j - 1].x, 0.2f, enemy.movePath[j - 1].y};
240     //   Vector3 q = {enemy.movePath[j].x, 0.2f, enemy.movePath[j].y};
241     //   DrawLine3D(p, q, GREEN);
242     // }
243 
244     float shieldHealth = enemyClassConfigs[enemy.enemyType].shieldHealth;
245     int phase = 0;
246     if (shieldHealth > 0 && enemy.shieldDamage < shieldHealth)
247     {
248       phase = SPRITE_UNIT_PHASE_WEAPON_SHIELD;
249     }
250 
251     switch (enemy.enemyType)
252     {
253     case ENEMY_TYPE_MINION:
254     case ENEMY_TYPE_RUNNER:
255     case ENEMY_TYPE_SHIELD:
256     case ENEMY_TYPE_BOSS:
257       DrawSpriteUnit(enemySprites[enemy.enemyType], (Vector3){position.x, 0.0f, position.y}, 
258         enemy.walkedDistance, 0, phase);
259       break;
260     }
261   }
262   rlDrawRenderBatchActive();
263   rlEnableDepthMask();
264 }
265 
266 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource)
267 {
268   // damage the tower
269   float explosionDamge = enemyClassConfigs[enemy->enemyType].explosionDamage;
270   float explosionRange = enemyClassConfigs[enemy->enemyType].explosionRange;
271   float explosionPushbackPower = enemyClassConfigs[enemy->enemyType].explosionPushbackPower;
272   float explosionRange2 = explosionRange * explosionRange;
273   tower->damage += enemyClassConfigs[enemy->enemyType].explosionDamage;
274   // explode the enemy
275   if (tower->damage >= TowerGetMaxHealth(tower))
276   {
277     tower->towerType = TOWER_TYPE_NONE;
278   }
279 
280   ParticleAdd(PARTICLE_TYPE_EXPLOSION, 
281     explosionSource, 
282     (Vector3){0, 0.1f, 0}, (Vector3){1.0f, 1.0f, 1.0f}, 1.0f);
283 
284   enemy->enemyType = ENEMY_TYPE_NONE;
285 
286   // push back enemies & dealing damage
287   for (int i = 0; i < enemyCount; i++)
288   {
289     Enemy *other = &enemies[i];
290     if (other->enemyType == ENEMY_TYPE_NONE)
291     {
292       continue;
293     }
294     float distanceSqr = Vector2DistanceSqr(enemy->simPosition, other->simPosition);
295     if (distanceSqr > 0 && distanceSqr < explosionRange2)
296     {
297       Vector2 direction = Vector2Normalize(Vector2Subtract(other->simPosition, enemy->simPosition));
298       other->simPosition = Vector2Add(other->simPosition, Vector2Scale(direction, explosionPushbackPower));
299       EnemyAddDamage(other, explosionDamge);
300     }
301   }
302 }
303 
304 void EnemyUpdate()
305 {
306   const float castleX = 0;
307   const float castleY = 0;
308   const float maxPathDistance2 = 0.25f * 0.25f;
309   
310   for (int i = 0; i < enemyCount; i++)
311   {
312     Enemy *enemy = &enemies[i];
313     if (enemy->enemyType == ENEMY_TYPE_NONE)
314     {
315       continue;
316     }
317 
318     int waypointPassedCount = 0;
319     Vector2 prevPosition = enemy->simPosition;
320     enemy->simPosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &enemy->simVelocity, &waypointPassedCount);
321     enemy->startMovingTime = gameTime.time;
322     enemy->walkedDistance += Vector2Distance(prevPosition, enemy->simPosition);
323     // track path of unit
324     if (enemy->movePathCount == 0 || Vector2DistanceSqr(enemy->simPosition, enemy->movePath[0]) > maxPathDistance2)
325     {
326       for (int j = ENEMY_MAX_PATH_COUNT - 1; j > 0; j--)
327       {
328         enemy->movePath[j] = enemy->movePath[j - 1];
329       }
330       enemy->movePath[0] = enemy->simPosition;
331       if (++enemy->movePathCount > ENEMY_MAX_PATH_COUNT)
332       {
333         enemy->movePathCount = ENEMY_MAX_PATH_COUNT;
334       }
335     }
336 
337     if (waypointPassedCount > 0)
338     {
339       enemy->currentX = enemy->nextX;
340       enemy->currentY = enemy->nextY;
341       if (EnemyGetNextPosition(enemy->currentX, enemy->currentY, &enemy->nextX, &enemy->nextY) &&
342         Vector2DistanceSqr(enemy->simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
343       {
344         // enemy reached the castle; remove it
345         enemy->enemyType = ENEMY_TYPE_NONE;
346         continue;
347       }
348     }
349   }
350 
351   // handle collisions between enemies
352   for (int i = 0; i < enemyCount - 1; i++)
353   {
354     Enemy *enemyA = &enemies[i];
355     if (enemyA->enemyType == ENEMY_TYPE_NONE)
356     {
357       continue;
358     }
359     for (int j = i + 1; j < enemyCount; j++)
360     {
361       Enemy *enemyB = &enemies[j];
362       if (enemyB->enemyType == ENEMY_TYPE_NONE)
363       {
364         continue;
365       }
366       float distanceSqr = Vector2DistanceSqr(enemyA->simPosition, enemyB->simPosition);
367       float radiusA = enemyClassConfigs[enemyA->enemyType].radius;
368       float radiusB = enemyClassConfigs[enemyB->enemyType].radius;
369       float radiusSum = radiusA + radiusB;
370       if (distanceSqr < radiusSum * radiusSum && distanceSqr > 0.001f)
371       {
372         // collision
373         float distance = sqrtf(distanceSqr);
374         float overlap = radiusSum - distance;
375         // move the enemies apart, but softly; if we have a clog of enemies,
376         // moving them perfectly apart can cause them to jitter
377         float positionCorrection = overlap / 5.0f;
378         Vector2 direction = (Vector2){
379             (enemyB->simPosition.x - enemyA->simPosition.x) / distance * positionCorrection,
380             (enemyB->simPosition.y - enemyA->simPosition.y) / distance * positionCorrection};
381         enemyA->simPosition = Vector2Subtract(enemyA->simPosition, direction);
382         enemyB->simPosition = Vector2Add(enemyB->simPosition, direction);
383       }
384     }
385   }
386 
387   // handle collisions between enemies and towers
388   for (int i = 0; i < enemyCount; i++)
389   {
390     Enemy *enemy = &enemies[i];
391     if (enemy->enemyType == ENEMY_TYPE_NONE)
392     {
393       continue;
394     }
395     enemy->contactTime -= gameTime.deltaTime;
396     if (enemy->contactTime < 0.0f)
397     {
398       enemy->contactTime = 0.0f;
399     }
400 
401     float enemyRadius = enemyClassConfigs[enemy->enemyType].radius;
402     // linear search over towers; could be optimized by using path finding tower map,
403     // but for now, we keep it simple
404     for (int j = 0; j < towerCount; j++)
405     {
406       Tower *tower = &towers[j];
407       if (tower->towerType == TOWER_TYPE_NONE)
408       {
409         continue;
410       }
411       float distanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){tower->x, tower->y});
412       float combinedRadius = enemyRadius + 0.708; // sqrt(0.5^2 + 0.5^2), corner-center distance of square with side length 1
413       if (distanceSqr > combinedRadius * combinedRadius)
414       {
415         continue;
416       }
417       // potential collision; square / circle intersection
418       float dx = tower->x - enemy->simPosition.x;
419       float dy = tower->y - enemy->simPosition.y;
420       float absDx = fabsf(dx);
421       float absDy = fabsf(dy);
422       Vector3 contactPoint = {0};
423       if (absDx <= 0.5f && absDx <= absDy) {
424         // vertical collision; push the enemy out horizontally
425         float overlap = enemyRadius + 0.5f - absDy;
426         if (overlap < 0.0f)
427         {
428           continue;
429         }
430         float direction = dy > 0.0f ? -1.0f : 1.0f;
431         enemy->simPosition.y += direction * overlap;
432         contactPoint = (Vector3){enemy->simPosition.x, 0.2f, tower->y + direction * 0.5f};
433       }
434       else if (absDy <= 0.5f && absDy <= absDx)
435       {
436         // horizontal collision; push the enemy out vertically
437         float overlap = enemyRadius + 0.5f - absDx;
438         if (overlap < 0.0f)
439         {
440           continue;
441         }
442         float direction = dx > 0.0f ? -1.0f : 1.0f;
443         enemy->simPosition.x += direction * overlap;
444         contactPoint = (Vector3){tower->x + direction * 0.5f, 0.2f, enemy->simPosition.y};
445       }
446       else
447       {
448         // possible collision with a corner
449         float cornerDX = dx > 0.0f ? -0.5f : 0.5f;
450         float cornerDY = dy > 0.0f ? -0.5f : 0.5f;
451         float cornerX = tower->x + cornerDX;
452         float cornerY = tower->y + cornerDY;
453         float cornerDistanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){cornerX, cornerY});
454         if (cornerDistanceSqr > enemyRadius * enemyRadius)
455         {
456           continue;
457         }
458         // push the enemy out along the diagonal
459         float cornerDistance = sqrtf(cornerDistanceSqr);
460         float overlap = enemyRadius - cornerDistance;
461         float directionX = cornerDistance > 0.0f ? (cornerX - enemy->simPosition.x) / cornerDistance : -cornerDX;
462         float directionY = cornerDistance > 0.0f ? (cornerY - enemy->simPosition.y) / cornerDistance : -cornerDY;
463         enemy->simPosition.x -= directionX * overlap;
464         enemy->simPosition.y -= directionY * overlap;
465         contactPoint = (Vector3){cornerX, 0.2f, cornerY};
466       }
467 
468       if (enemyClassConfigs[enemy->enemyType].explosionDamage > 0.0f)
469       {
470         enemy->contactTime += gameTime.deltaTime * 2.0f; // * 2 to undo the subtraction above
471         if (enemy->contactTime >= enemyClassConfigs[enemy->enemyType].requiredContactTime)
472         {
473           EnemyTriggerExplode(enemy, tower, contactPoint);
474         }
475       }
476     }
477   }
478 }
479 
480 EnemyId EnemyGetId(Enemy *enemy)
481 {
482   return (EnemyId){enemy - enemies, enemy->generation};
483 }
484 
485 Enemy *EnemyTryResolve(EnemyId enemyId)
486 {
487   if (enemyId.index >= ENEMY_MAX_COUNT)
488   {
489     return 0;
490   }
491   Enemy *enemy = &enemies[enemyId.index];
492   if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE)
493   {
494     return 0;
495   }
496   return enemy;
497 }
498 
499 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY)
500 {
501   Enemy *spawn = 0;
502   for (int i = 0; i < enemyCount; i++)
503   {
504     Enemy *enemy = &enemies[i];
505     if (enemy->enemyType == ENEMY_TYPE_NONE)
506     {
507       spawn = enemy;
508       break;
509     }
510   }
511 
512   if (enemyCount < ENEMY_MAX_COUNT && !spawn)
513   {
514     spawn = &enemies[enemyCount++];
515   }
516 
517   if (spawn)
518   {
519     *spawn = (Enemy){
520       .currentX = currentX,
521       .currentY = currentY,
522       .nextX = currentX,
523       .nextY = currentY,
524       .simPosition = (Vector2){currentX, currentY},
525       .simVelocity = (Vector2){0, 0},
526       .enemyType = enemyType,
527       .startMovingTime = gameTime.time,
528       .movePathCount = 0,
529       .walkedDistance = 0.0f,
530       .shieldDamage = 0.0f,
531       .damage = 0.0f,
532       .futureDamage = 0.0f,
533       .contactTime = 0.0f,
534       .generation = spawn->generation + 1,
535     };
536   }
537 
538   return spawn;
539 }
540 
541 int EnemyAddDamageRange(Vector2 position, float range, float damage)
542 {
543   int count = 0;
544   float range2 = range * range;
545   for (int i = 0; i < enemyCount; i++)
546   {
547     Enemy *enemy = &enemies[i];
548     if (enemy->enemyType == ENEMY_TYPE_NONE)
549     {
550       continue;
551     }
552     float distance2 = Vector2DistanceSqr(position, enemy->simPosition);
553     if (distance2 <= range2)
554     {
555       EnemyAddDamage(enemy, damage);
556       count++;
557     }
558   }
559   return count;
560 }
561 
562 int EnemyAddDamage(Enemy *enemy, float damage)
563 {
564   float shieldHealth = enemyClassConfigs[enemy->enemyType].shieldHealth;
565   if (shieldHealth > 0.0f && enemy->shieldDamage < shieldHealth)
566   {
567     float shieldDamageAbsorption = enemyClassConfigs[enemy->enemyType].shieldDamageAbsorption;
568     float shieldDamage = fminf(fminf(shieldDamageAbsorption, damage), shieldHealth - enemy->shieldDamage);
569     enemy->shieldDamage += shieldDamage;
570     damage -= shieldDamage;
571   }
572   enemy->damage += damage;
573   if (enemy->damage >= EnemyGetMaxHealth(enemy))
574   {
575     currentLevel->playerGold += enemyClassConfigs[enemy->enemyType].goldValue;
576     enemy->enemyType = ENEMY_TYPE_NONE;
577     return 1;
578   }
579 
580   return 0;
581 }
582 
583 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
584 {
585   int16_t castleX = 0;
586   int16_t castleY = 0;
587   Enemy* closest = 0;
588   int16_t closestDistance = 0;
589   float range2 = range * range;
590   for (int i = 0; i < enemyCount; i++)
591   {
592     Enemy* enemy = &enemies[i];
593     if (enemy->enemyType == ENEMY_TYPE_NONE)
594     {
595       continue;
596     }
597     float maxHealth = EnemyGetMaxHealth(enemy) + enemyClassConfigs[enemy->enemyType].shieldHealth;
598     if (enemy->futureDamage >= maxHealth)
599     {
600       // ignore enemies that will die soon
601       continue;
602     }
603     int16_t dx = castleX - enemy->currentX;
604     int16_t dy = castleY - enemy->currentY;
605     int16_t distance = abs(dx) + abs(dy);
606     if (!closest || distance < closestDistance)
607     {
608       float tdx = towerX - enemy->currentX;
609       float tdy = towerY - enemy->currentY;
610       float tdistance2 = tdx * tdx + tdy * tdy;
611       if (tdistance2 <= range2)
612       {
613         closest = enemy;
614         closestDistance = distance;
615       }
616     }
617   }
618   return closest;
619 }
620 
621 int EnemyCount()
622 {
623   int count = 0;
624   for (int i = 0; i < enemyCount; i++)
625   {
626     if (enemies[i].enemyType != ENEMY_TYPE_NONE)
627     {
628       count++;
629     }
630   }
631   return count;
632 }
633 
634 void EnemyDrawHealthbars(Camera3D camera)
635 {
636   for (int i = 0; i < enemyCount; i++)
637   {
638     Enemy *enemy = &enemies[i];
639     
640     float maxShieldHealth = enemyClassConfigs[enemy->enemyType].shieldHealth;
641     if (maxShieldHealth > 0.0f && enemy->shieldDamage < maxShieldHealth && enemy->shieldDamage > 0.0f)
642     {
643       float shieldHealth = maxShieldHealth - enemy->shieldDamage;
644       float shieldHealthRatio = shieldHealth / maxShieldHealth;
645       Vector3 position = (Vector3){enemy->simPosition.x, 0.5f, enemy->simPosition.y};
646       DrawHealthBar(camera, position, (Vector2) {.y = -4}, shieldHealthRatio, BLUE, 20.0f);
647     }
648 
649     if (enemy->enemyType == ENEMY_TYPE_NONE || enemy->damage == 0.0f)
650     {
651       continue;
652     }
653     Vector3 position = (Vector3){enemy->simPosition.x, 0.5f, enemy->simPosition.y};
654     float maxHealth = EnemyGetMaxHealth(enemy);
655     float health = maxHealth - enemy->damage;
656     float healthRatio = health / maxHealth;
657     
658     DrawHealthBar(camera, position, Vector2Zero(), healthRatio, GREEN, 15.0f);
659   }
660 }
  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 upgrade status is now stored in the TowerUpgradeState struct (which I added in the previous part already before realizing that I first need some more UI bits).

When placing the tower and when opening the tower's context menu, it would be nice to see the range of the tower. This way we can also verify that the range upgrade works as expected.

  • 💾
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 #include <rlgl.h>
  4 #include <stdlib.h>
  5 #include <math.h>
  6 #include <string.h>
  7 
  8 //# Variables
  9 Font gameFontNormal = {0};
 10 GUIState guiState = {0};
 11 GameTime gameTime = {
 12   .fixedDeltaTime = 1.0f / 60.0f,
 13 };
 14 
 15 Model floorTileAModel = {0};
 16 Model floorTileBModel = {0};
 17 Model treeModel[2] = {0};
 18 Model firTreeModel[2] = {0};
 19 Model rockModels[5] = {0};
 20 Model grassPatchModel[1] = {0};
 21 
 22 Model pathArrowModel = {0};
 23 Model greenArrowModel = {0};
 24 
 25 Texture2D palette, spriteSheet;
 26 
 27 NPatchInfo uiPanelPatch = {
 28   .layout = NPATCH_NINE_PATCH,
 29   .source = {145, 1, 46, 46},
 30   .top = 18, .bottom = 18,
 31   .left = 16, .right = 16
 32 };
 33 NPatchInfo uiButtonNormal = {
 34   .layout = NPATCH_NINE_PATCH,
 35   .source = {193, 1, 32, 20},
 36   .top = 7, .bottom = 7,
 37   .left = 10, .right = 10
 38 };
 39 NPatchInfo uiButtonDisabled = {
 40   .layout = NPATCH_NINE_PATCH,
 41   .source = {193, 22, 32, 20},
 42   .top = 7, .bottom = 7,
 43   .left = 10, .right = 10
 44 };
 45 NPatchInfo uiButtonHovered = {
 46   .layout = NPATCH_NINE_PATCH,
 47   .source = {193, 43, 32, 20},
 48   .top = 7, .bottom = 7,
 49   .left = 10, .right = 10
 50 };
 51 NPatchInfo uiButtonPressed = {
 52   .layout = NPATCH_NINE_PATCH,
 53   .source = {193, 64, 32, 20},
 54   .top = 7, .bottom = 7,
 55   .left = 10, .right = 10
 56 };
 57 Rectangle uiDiamondMarker = {145, 48, 15, 15};
 58 
 59 Level levels[] = {
 60   [0] = {
 61     .state = LEVEL_STATE_BUILDING,
 62     .initialGold = 20,
 63     .waves[0] = {
 64       .enemyType = ENEMY_TYPE_SHIELD,
 65       .wave = 0,
 66       .count = 1,
 67       .interval = 2.5f,
 68       .delay = 1.0f,
 69       .spawnPosition = {2, 6},
 70     },
 71     .waves[1] = {
 72       .enemyType = ENEMY_TYPE_RUNNER,
 73       .wave = 0,
 74       .count = 5,
 75       .interval = 0.5f,
 76       .delay = 1.0f,
 77       .spawnPosition = {-2, 6},
 78     },
 79     .waves[2] = {
 80       .enemyType = ENEMY_TYPE_SHIELD,
 81       .wave = 1,
 82       .count = 20,
 83       .interval = 1.5f,
 84       .delay = 1.0f,
 85       .spawnPosition = {0, 6},
 86     },
 87     .waves[3] = {
 88       .enemyType = ENEMY_TYPE_MINION,
 89       .wave = 2,
 90       .count = 30,
 91       .interval = 1.2f,
 92       .delay = 1.0f,
 93       .spawnPosition = {2, 6},
 94     },
 95     .waves[4] = {
 96       .enemyType = ENEMY_TYPE_BOSS,
 97       .wave = 2,
 98       .count = 2,
 99       .interval = 5.0f,
100       .delay = 2.0f,
101       .spawnPosition = {-2, 4},
102     }
103   },
104 };
105 
106 Level *currentLevel = levels;
107 
108 void DrawBoxedText(const char *text, int x, int y, int width, int height, float alignX, float alignY, Color textColor);
109 
110 void DrawTitleText(const char *text, int anchorX, float alignX, Color color)
111 {
112   int textWidth = MeasureTextEx(gameFontNormal, text, gameFontNormal.baseSize, 0).x;
113   int panelWidth = textWidth + 40;
114   int posX = anchorX - panelWidth * alignX;
115   int textOffset = 20;
116   DrawTextureNPatch(spriteSheet, uiPanelPatch, (Rectangle){ posX, -10, panelWidth, 40}, Vector2Zero(), 0, WHITE);
117   DrawTextEx(gameFontNormal, text, (Vector2){posX + textOffset + 1, 6 + 1}, gameFontNormal.baseSize, 0, BLACK);
118   DrawTextEx(gameFontNormal, text, (Vector2){posX + textOffset, 6}, gameFontNormal.baseSize, 0, color);
119 }
120 
121 void DrawTitle(const char *text)
122 {
123   DrawTitleText(text, GetScreenWidth() / 2, 0.5f, WHITE);
124 }
125 
126 //# Game
127 
128 static Model LoadGLBModel(char *filename)
129 {
130   Model model = LoadModel(TextFormat("data/%s.glb",filename));
131   for (int i = 0; i < model.materialCount; i++)
132   {
133     model.materials[i].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
134   }
135   return model;
136 }
137 
138 void LoadAssets()
139 {
140   // load a sprite sheet that contains all units
141   spriteSheet = LoadTexture("data/spritesheet.png");
142   SetTextureFilter(spriteSheet, TEXTURE_FILTER_BILINEAR);
143 
144   // we'll use a palette texture to colorize the all buildings and environment art
145   palette = LoadTexture("data/palette.png");
146   // The texture uses gradients on very small space, so we'll enable bilinear filtering
147   SetTextureFilter(palette, TEXTURE_FILTER_BILINEAR);
148 
149   gameFontNormal = LoadFont("data/alagard.png");
150 
151   floorTileAModel = LoadGLBModel("floor-tile-a");
152   floorTileBModel = LoadGLBModel("floor-tile-b");
153   treeModel[0] = LoadGLBModel("leaftree-large-1-a");
154   treeModel[1] = LoadGLBModel("leaftree-large-1-b");
155   firTreeModel[0] = LoadGLBModel("firtree-1-a");
156   firTreeModel[1] = LoadGLBModel("firtree-1-b");
157   rockModels[0] = LoadGLBModel("rock-1");
158   rockModels[1] = LoadGLBModel("rock-2");
159   rockModels[2] = LoadGLBModel("rock-3");
160   rockModels[3] = LoadGLBModel("rock-4");
161   rockModels[4] = LoadGLBModel("rock-5");
162   grassPatchModel[0] = LoadGLBModel("grass-patch-1");
163 
164   pathArrowModel = LoadGLBModel("direction-arrow-x");
165   greenArrowModel = LoadGLBModel("green-arrow");
166 }
167 
168 void InitLevel(Level *level)
169 {
170   level->seed = (int)(GetTime() * 100.0f);
171 
172   TowerInit();
173   EnemyInit();
174   ProjectileInit();
175   ParticleInit();
176   TowerTryAdd(TOWER_TYPE_BASE, 0, 0);
177 
178   level->placementMode = 0;
179   level->state = LEVEL_STATE_BUILDING;
180   level->nextState = LEVEL_STATE_NONE;
181   level->playerGold = level->initialGold;
182   level->currentWave = 0;
183   level->placementX = -1;
184   level->placementY = 0;
185 
186   Camera *camera = &level->camera;
187   camera->position = (Vector3){4.0f, 8.0f, 8.0f};
188   camera->target = (Vector3){0.0f, 0.0f, 0.0f};
189   camera->up = (Vector3){0.0f, 1.0f, 0.0f};
190   camera->fovy = 11.5f;
191   camera->projection = CAMERA_ORTHOGRAPHIC;
192 }
193 
194 void DrawLevelHud(Level *level)
195 {
196   const char *text = TextFormat("Gold: %d", level->playerGold);
197   DrawTitleText(text, GetScreenWidth() - 10, 1.0f, YELLOW);
198 }
199 
200 void DrawLevelReportLostWave(Level *level)
201 {
202   BeginMode3D(level->camera);
203   DrawLevelGround(level);
204   TowerDraw();
205   EnemyDraw();
206   ProjectileDraw();
207   ParticleDraw();
208   guiState.isBlocked = 0;
209   EndMode3D();
210 
211   TowerDrawHealthBars(level->camera);
212 
213   DrawTitle("Wave lost");
214   
215   if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
216   {
217     level->nextState = LEVEL_STATE_RESET;
218   }
219 }
220 
221 int HasLevelNextWave(Level *level)
222 {
223   for (int i = 0; i < 10; i++)
224   {
225     EnemyWave *wave = &level->waves[i];
226     if (wave->wave == level->currentWave)
227     {
228       return 1;
229     }
230   }
231   return 0;
232 }
233 
234 void DrawLevelReportWonWave(Level *level)
235 {
236   BeginMode3D(level->camera);
237   DrawLevelGround(level);
238   TowerDraw();
239   EnemyDraw();
240   ProjectileDraw();
241   ParticleDraw();
242   guiState.isBlocked = 0;
243   EndMode3D();
244 
245   TowerDrawHealthBars(level->camera);
246 
247   DrawTitle("Wave won");
248 
249 
250   if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
251   {
252     level->nextState = LEVEL_STATE_RESET;
253   }
254 
255   if (HasLevelNextWave(level))
256   {
257     if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
258     {
259       level->nextState = LEVEL_STATE_BUILDING;
260     }
261   }
262   else {
263     if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
264     {
265       level->nextState = LEVEL_STATE_WON_LEVEL;
266     }
267   }
268 }
269 
270 int DrawBuildingBuildButton(Level *level, int x, int y, int width, int height, uint8_t towerType, const char *name)
271 {
272   static ButtonState buttonStates[8] = {0};
273   int cost = GetTowerCosts(towerType);
274   const char *text = TextFormat("%s: %d", name, cost);
275   buttonStates[towerType].isSelected = level->placementMode == towerType;
276   buttonStates[towerType].isDisabled = level->playerGold < cost;
277   if (Button(text, x, y, width, height, &buttonStates[towerType]))
278   {
279     level->placementMode = buttonStates[towerType].isSelected ? 0 : towerType;
280     level->nextState = LEVEL_STATE_BUILDING_PLACEMENT;
281     return 1;
282   }
283   return 0;
284 }
285 
286 float GetRandomFloat(float min, float max)
287 {
288   int random = GetRandomValue(0, 0xfffffff);
289   return ((float)random / (float)0xfffffff) * (max - min) + min;
290 }
291 
292 void DrawLevelGround(Level *level)
293 {
294   // draw checkerboard ground pattern
295   for (int x = -5; x <= 5; x += 1)
296   {
297     for (int y = -5; y <= 5; y += 1)
298     {
299       Model *model = (x + y) % 2 == 0 ? &floorTileAModel : &floorTileBModel;
300       DrawModel(*model, (Vector3){x, 0.0f, y}, 1.0f, WHITE);
301     }
302   }
303 
304   int oldSeed = GetRandomValue(0, 0xfffffff);
305   SetRandomSeed(level->seed);
306   // increase probability for trees via duplicated entries
307   Model borderModels[64];
308   int maxRockCount = GetRandomValue(2, 6);
309   int maxTreeCount = GetRandomValue(10, 20);
310   int maxFirTreeCount = GetRandomValue(5, 10);
311   int maxLeafTreeCount = maxTreeCount - maxFirTreeCount;
312   int grassPatchCount = GetRandomValue(5, 30);
313 
314   int modelCount = 0;
315   for (int i = 0; i < maxRockCount && modelCount < 63; i++)
316   {
317     borderModels[modelCount++] = rockModels[GetRandomValue(0, 5)];
318   }
319   for (int i = 0; i < maxLeafTreeCount && modelCount < 63; i++)
320   {
321     borderModels[modelCount++] = treeModel[GetRandomValue(0, 1)];
322   }
323   for (int i = 0; i < maxFirTreeCount && modelCount < 63; i++)
324   {
325     borderModels[modelCount++] = firTreeModel[GetRandomValue(0, 1)];
326   }
327   for (int i = 0; i < grassPatchCount && modelCount < 63; i++)
328   {
329     borderModels[modelCount++] = grassPatchModel[0];
330   }
331 
332   // draw some objects around the border of the map
333   Vector3 up = {0, 1, 0};
334   // a pseudo random number generator to get the same result every time
335   const float wiggle = 0.75f;
336   const int layerCount = 3;
337   for (int layer = 0; layer <= layerCount; layer++)
338   {
339     int layerPos = 6 + layer;
340     Model *selectedModels = borderModels;
341     int selectedModelCount = modelCount;
342     if (layer == 0)
343     {
344       selectedModels = grassPatchModel;
345       selectedModelCount = 1;
346     }
347     for (int x = -6 - layer; x <= 6 + layer; x += 1)
348     {
349       DrawModelEx(selectedModels[GetRandomValue(0, selectedModelCount - 1)], 
350         (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, -layerPos + GetRandomFloat(0.0f, wiggle)}, 
351         up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
352       DrawModelEx(selectedModels[GetRandomValue(0, selectedModelCount - 1)], 
353         (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, layerPos + GetRandomFloat(0.0f, wiggle)}, 
354         up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
355     }
356 
357     for (int z = -5 - layer; z <= 5 + layer; z += 1)
358     {
359       DrawModelEx(selectedModels[GetRandomValue(0, selectedModelCount - 1)], 
360         (Vector3){-layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)}, 
361         up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
362       DrawModelEx(selectedModels[GetRandomValue(0, selectedModelCount - 1)], 
363         (Vector3){layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)}, 
364         up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
365     }
366   }
367 
368   SetRandomSeed(oldSeed);
369 }
370 
371 void DrawEnemyPath(Level *level, Color arrowColor)
372 {
373   const int castleX = 0, castleY = 0;
374   const int maxWaypointCount = 200;
375   const float timeStep = 1.0f;
376   Vector3 arrowScale = {0.75f, 0.75f, 0.75f};
377 
378   // we start with a time offset to simulate the path, 
379   // this way the arrows are animated in a forward moving direction
380   // The time is wrapped around the time step to get a smooth animation
381   float timeOffset = fmodf(GetTime(), timeStep);
382 
383   for (int i = 0; i < ENEMY_MAX_WAVE_COUNT; i++)
384   {
385     EnemyWave *wave = &level->waves[i];
386     if (wave->wave != level->currentWave)
387     {
388       continue;
389     }
390 
391     // use this dummy enemy to simulate the path
392     Enemy dummy = {
393       .enemyType = ENEMY_TYPE_MINION,
394       .simPosition = (Vector2){wave->spawnPosition.x, wave->spawnPosition.y},
395       .nextX = wave->spawnPosition.x,
396       .nextY = wave->spawnPosition.y,
397       .currentX = wave->spawnPosition.x,
398       .currentY = wave->spawnPosition.y,
399     };
400 
401     float deltaTime = timeOffset;
402     for (int j = 0; j < maxWaypointCount; j++)
403     {
404       int waypointPassedCount = 0;
405       Vector2 pos = EnemyGetPosition(&dummy, deltaTime, &dummy.simVelocity, &waypointPassedCount);
406       // after the initial variable starting offset, we use a fixed time step
407       deltaTime = timeStep;
408       dummy.simPosition = pos;
409 
410       // Update the dummy's position just like we do in the regular enemy update loop
411       for (int k = 0; k < waypointPassedCount; k++)
412       {
413         dummy.currentX = dummy.nextX;
414         dummy.currentY = dummy.nextY;
415         if (EnemyGetNextPosition(dummy.currentX, dummy.currentY, &dummy.nextX, &dummy.nextY) &&
416           Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
417         {
418           break;
419         }
420       }
421       if (Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
422       {
423         break;
424       }
425       
426       // get the angle we need to rotate the arrow model. The velocity is just fine for this.
427       float angle = atan2f(dummy.simVelocity.x, dummy.simVelocity.y) * RAD2DEG - 90.0f;
428       DrawModelEx(pathArrowModel, (Vector3){pos.x, 0.15f, pos.y}, (Vector3){0, 1, 0}, angle, arrowScale, arrowColor);
429     }
430   }
431 }
432 
433 void DrawEnemyPaths(Level *level)
434 {
435   // disable depth testing for the path arrows
436   // flush the 3D batch to draw the arrows on top of everything
437   rlDrawRenderBatchActive();
438   rlDisableDepthTest();
439   DrawEnemyPath(level, (Color){64, 64, 64, 160});
440 
441   rlDrawRenderBatchActive();
442   rlEnableDepthTest();
443   DrawEnemyPath(level, WHITE);
444 }
445 
446 static void SimulateTowerPlacementBehavior(Level *level, float towerFloatHeight, float mapX, float mapY)
447 {
448   float dt = gameTime.fixedDeltaTime;
449   // smooth transition for the placement position using exponential decay
450   const float lambda = 15.0f;
451   float factor = 1.0f - expf(-lambda * dt);
452 
453   float damping = 0.5f;
454   float springStiffness = 300.0f;
455   float springDecay = 95.0f;
456   float minHeight = 0.35f;
457 
458   if (level->placementPhase == PLACEMENT_PHASE_STARTING)
459   {
460     damping = 1.0f;
461     springDecay = 90.0f;
462     springStiffness = 100.0f;
463     minHeight = 0.70f;
464   }
465 
466   for (int i = 0; i < gameTime.fixedStepCount; i++)
467   {
468     level->placementTransitionPosition = 
469       Vector2Lerp(
470         level->placementTransitionPosition, 
471         (Vector2){mapX, mapY}, factor);
472 
473     // draw the spring position for debugging the spring simulation
474     // first step: stiff spring, no simulation
475     Vector3 worldPlacementPosition = (Vector3){
476       level->placementTransitionPosition.x, towerFloatHeight, level->placementTransitionPosition.y};
477     Vector3 springTargetPosition = (Vector3){
478       worldPlacementPosition.x, 1.0f + towerFloatHeight, worldPlacementPosition.z};
479     // consider the current velocity to predict the future position in order to dampen
480     // the spring simulation. Longer prediction times will result in more damping
481     Vector3 predictedSpringPosition = Vector3Add(level->placementTowerSpring.position, 
482       Vector3Scale(level->placementTowerSpring.velocity, dt * damping));
483     Vector3 springPointDelta = Vector3Subtract(springTargetPosition, predictedSpringPosition);
484     Vector3 velocityChange = Vector3Scale(springPointDelta, dt * springStiffness);
485     // decay velocity of the upright forcing spring
486     // This force acts like a 2nd spring that pulls the tip upright into the air above the
487     // base position
488     level->placementTowerSpring.velocity = Vector3Scale(level->placementTowerSpring.velocity, 1.0f - expf(-springDecay * dt));
489     level->placementTowerSpring.velocity = Vector3Add(level->placementTowerSpring.velocity, velocityChange);
490 
491     // calculate length of the spring to calculate the force to apply to the placementTowerSpring position
492     // we use a simple spring model with a rest length of 1.0f
493     Vector3 springDelta = Vector3Subtract(worldPlacementPosition, level->placementTowerSpring.position);
494     float springLength = Vector3Length(springDelta);
495     float springForce = (springLength - 1.0f) * springStiffness;
496     Vector3 springForceVector = Vector3Normalize(springDelta);
497     springForceVector = Vector3Scale(springForceVector, springForce);
498     level->placementTowerSpring.velocity = Vector3Add(level->placementTowerSpring.velocity, 
499       Vector3Scale(springForceVector, dt));
500 
501     level->placementTowerSpring.position = Vector3Add(level->placementTowerSpring.position, 
502       Vector3Scale(level->placementTowerSpring.velocity, dt));
503     if (level->placementTowerSpring.position.y < minHeight + towerFloatHeight)
504     {
505       level->placementTowerSpring.velocity.y *= -1.0f;
506       level->placementTowerSpring.position.y = fmaxf(level->placementTowerSpring.position.y, minHeight + towerFloatHeight);
507     }
508   }
509 }
510 
511 void DrawLevelBuildingPlacementState(Level *level)
512 {
513   const float placementDuration = 0.5f;
514 
515   level->placementTimer += gameTime.deltaTime;
516   if (level->placementTimer > 1.0f && level->placementPhase == PLACEMENT_PHASE_STARTING)
517   {
518     level->placementPhase = PLACEMENT_PHASE_MOVING;
519     level->placementTimer = 0.0f;
520   }
521 
522   BeginMode3D(level->camera);
523   DrawLevelGround(level);
524 
525   int blockedCellCount = 0;
526   Vector2 blockedCells[1];
527   Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera);
528   float planeDistance = ray.position.y / -ray.direction.y;
529   float planeX = ray.direction.x * planeDistance + ray.position.x;
530   float planeY = ray.direction.z * planeDistance + ray.position.z;
531   int16_t mapX = (int16_t)floorf(planeX + 0.5f);
532   int16_t mapY = (int16_t)floorf(planeY + 0.5f);
533   if (level->placementPhase == PLACEMENT_PHASE_MOVING && 
534     level->placementMode && !guiState.isBlocked && 
535     mapX >= -5 && mapX <= 5 && mapY >= -5 && mapY <= 5 && IsMouseButtonDown(MOUSE_LEFT_BUTTON))
536   {
537     level->placementX = mapX;
538     level->placementY = mapY;
539   }
540   else
541   {
542     mapX = level->placementX;
543     mapY = level->placementY;
544   }
545   blockedCells[blockedCellCount++] = (Vector2){mapX, mapY};
546   PathFindingMapUpdate(blockedCellCount, blockedCells);
547 
548   TowerDraw();
549   EnemyDraw();
550   ProjectileDraw();
551   ParticleDraw();
552   DrawEnemyPaths(level);
553 
554   // let the tower float up and down. Consider this height in the spring simulation as well
555   float towerFloatHeight = sinf(gameTime.time * 4.0f) * 0.2f + 0.3f;
556 
557   if (level->placementPhase == PLACEMENT_PHASE_PLACING)
558   {
559     // The bouncing spring needs a bit of outro time to look nice and complete. 
560     // So we scale the time so that the first 2/3rd of the placing phase handles the motion
561     // and the last 1/3rd is the outro physics (bouncing)
562     float t = fminf(1.0f, level->placementTimer / placementDuration * 1.5f);
563     // let towerFloatHeight describe parabola curve, starting at towerFloatHeight and ending at 0
564     float linearBlendHeight = (1.0f - t) * towerFloatHeight;
565     float parabola = (1.0f - ((t - 0.5f) * (t - 0.5f) * 4.0f)) * 2.0f;
566     towerFloatHeight = linearBlendHeight + parabola;
567   }
568 
569   SimulateTowerPlacementBehavior(level, towerFloatHeight, mapX, mapY);
570   
571   rlPushMatrix();
572   rlTranslatef(level->placementTransitionPosition.x, 0, level->placementTransitionPosition.y);
573 574 // calculate x and z rotation to align the model with the spring
575 Vector3 position = {level->placementTransitionPosition.x, towerFloatHeight, level->placementTransitionPosition.y}; 576 Vector3 towerUp = Vector3Subtract(level->placementTowerSpring.position, position); 577 Vector3 rotationAxis = Vector3CrossProduct(towerUp, (Vector3){0, 1, 0}); 578 float angle = acosf(Vector3DotProduct(towerUp, (Vector3){0, 1, 0}) / Vector3Length(towerUp)) * RAD2DEG;
579 float springLength = Vector3Length(towerUp); 580 float towerStretch = fminf(fmaxf(springLength, 0.5f), 4.5f);
581 float towerSquash = 1.0f / towerStretch;
582 583 Tower dummy = { 584 .towerType = level->placementMode, 585 }; 586 587 float rangeAlpha = fminf(1.0f, level->placementTimer / placementDuration); 588 if (level->placementPhase == PLACEMENT_PHASE_PLACING) 589 { 590 rangeAlpha = 1.0f - rangeAlpha; 591 } 592 else if (level->placementPhase == PLACEMENT_PHASE_MOVING) 593 { 594 rangeAlpha = 1.0f; 595 } 596 597 TowerDrawRange(dummy, rangeAlpha); 598 599 rlPushMatrix(); 600 rlTranslatef(0.0f, towerFloatHeight, 0.0f);
601
602 rlRotatef(-angle, rotationAxis.x, rotationAxis.y, rotationAxis.z); 603 rlScalef(towerSquash, towerStretch, towerSquash);
604 TowerDrawSingle(dummy); 605 rlPopMatrix(); 606 607 608 // draw a shadow for the tower 609 float umbrasize = 0.8 + sqrtf(towerFloatHeight); 610 DrawCube((Vector3){0.0f, 0.05f, 0.0f}, umbrasize, 0.0f, umbrasize, (Color){0, 0, 0, 32}); 611 DrawCube((Vector3){0.0f, 0.075f, 0.0f}, 0.85f, 0.0f, 0.85f, (Color){0, 0, 0, 64}); 612 613 614 float bounce = sinf(gameTime.time * 8.0f) * 0.5f + 0.5f; 615 float offset = fmaxf(0.0f, bounce - 0.4f) * 0.35f + 0.7f; 616 float squeeze = -fminf(0.0f, bounce - 0.4f) * 0.7f + 0.7f; 617 float stretch = fmaxf(0.0f, bounce - 0.3f) * 0.5f + 0.8f; 618 619 DrawModelEx(greenArrowModel, (Vector3){ offset, 0.0f, 0.0f}, (Vector3){0, 1, 0}, 90, (Vector3){squeeze, 1.0f, stretch}, WHITE); 620 DrawModelEx(greenArrowModel, (Vector3){-offset, 0.0f, 0.0f}, (Vector3){0, 1, 0}, 270, (Vector3){squeeze, 1.0f, stretch}, WHITE); 621 DrawModelEx(greenArrowModel, (Vector3){ 0.0f, 0.0f, offset}, (Vector3){0, 1, 0}, 0, (Vector3){squeeze, 1.0f, stretch}, WHITE); 622 DrawModelEx(greenArrowModel, (Vector3){ 0.0f, 0.0f, -offset}, (Vector3){0, 1, 0}, 180, (Vector3){squeeze, 1.0f, stretch}, WHITE); 623 rlPopMatrix(); 624 625 guiState.isBlocked = 0; 626 627 EndMode3D(); 628 629 TowerDrawHealthBars(level->camera); 630 631 if (level->placementPhase == PLACEMENT_PHASE_PLACING) 632 { 633 if (level->placementTimer > placementDuration) 634 { 635 Tower *tower = TowerTryAdd(level->placementMode, mapX, mapY); 636 // testing repairing 637 tower->damage = 2.5f; 638 level->playerGold -= GetTowerCosts(level->placementMode); 639 level->nextState = LEVEL_STATE_BUILDING; 640 level->placementMode = TOWER_TYPE_NONE; 641 } 642 } 643 else 644 { 645 if (Button("Cancel", 20, GetScreenHeight() - 40, 160, 30, 0)) 646 { 647 level->nextState = LEVEL_STATE_BUILDING; 648 level->placementMode = TOWER_TYPE_NONE; 649 TraceLog(LOG_INFO, "Cancel building"); 650 } 651 652 if (TowerGetAt(mapX, mapY) == 0 && Button("Build", GetScreenWidth() - 180, GetScreenHeight() - 40, 160, 30, 0)) 653 { 654 level->placementPhase = PLACEMENT_PHASE_PLACING; 655 level->placementTimer = 0.0f; 656 } 657 } 658 } 659 660 enum ContextMenuType 661 { 662 CONTEXT_MENU_TYPE_MAIN, 663 CONTEXT_MENU_TYPE_SELL_CONFIRM, 664 CONTEXT_MENU_TYPE_UPGRADE, 665 }; 666 667 enum UpgradeType 668 { 669 UPGRADE_TYPE_SPEED, 670 UPGRADE_TYPE_DAMAGE, 671 UPGRADE_TYPE_RANGE, 672 }; 673 674 typedef struct ContextMenuArgs 675 { 676 void *data; 677 uint8_t uint8; 678 int32_t int32; 679 Tower *tower; 680 } ContextMenuArgs; 681 682 int OnContextMenuBuild(Level *level, ContextMenuArgs *data) 683 { 684 uint8_t towerType = data->uint8; 685 level->placementMode = towerType; 686 level->nextState = LEVEL_STATE_BUILDING_PLACEMENT; 687 return 1; 688 } 689 690 int OnContextMenuSellConfirm(Level *level, ContextMenuArgs *data) 691 { 692 Tower *tower = data->tower; 693 int gold = data->int32; 694 level->playerGold += gold; 695 tower->towerType = TOWER_TYPE_NONE; 696 return 1; 697 } 698 699 int OnContextMenuSellCancel(Level *level, ContextMenuArgs *data) 700 { 701 return 1; 702 } 703 704 int OnContextMenuSell(Level *level, ContextMenuArgs *data) 705 { 706 level->placementContextMenuType = CONTEXT_MENU_TYPE_SELL_CONFIRM; 707 return 0; 708 } 709 710 int OnContextMenuDoUpgrade(Level *level, ContextMenuArgs *data) 711 { 712 Tower *tower = data->tower; 713 switch (data->uint8) 714 { 715 case UPGRADE_TYPE_SPEED: tower->upgradeState.speed++; break; 716 case UPGRADE_TYPE_DAMAGE: tower->upgradeState.damage++; break; 717 case UPGRADE_TYPE_RANGE: tower->upgradeState.range++; break; 718 } 719 level->playerGold -= data->int32; 720 return 0; 721 } 722 723 int OnContextMenuUpgrade(Level *level, ContextMenuArgs *data) 724 { 725 level->placementContextMenuType = CONTEXT_MENU_TYPE_UPGRADE; 726 return 0; 727 } 728 729 int OnContextMenuRepair(Level *level, ContextMenuArgs *data) 730 { 731 Tower *tower = data->tower; 732 if (level->playerGold >= 1) 733 { 734 level->playerGold -= 1; 735 tower->damage = fmaxf(0.0f, tower->damage - 1.0f); 736 } 737 return tower->damage == 0.0f; 738 } 739 740 typedef struct ContextMenuItem 741 { 742 uint8_t index; 743 char text[24]; 744 float alignX; 745 int (*action)(Level*, ContextMenuArgs*); 746 void *data; 747 ContextMenuArgs args; 748 ButtonState buttonState; 749 } ContextMenuItem; 750 751 ContextMenuItem ContextMenuItemText(uint8_t index, const char *text, float alignX) 752 { 753 ContextMenuItem item = {.index = index, .alignX = alignX}; 754 strncpy(item.text, text, 24); 755 return item; 756 } 757 758 ContextMenuItem ContextMenuItemButton(uint8_t index, const char *text, int (*action)(Level*, ContextMenuArgs*), ContextMenuArgs args) 759 { 760 ContextMenuItem item = {.index = index, .action = action, .args = args}; 761 strncpy(item.text, text, 24); 762 return item; 763 } 764 765 int DrawContextMenu(Level *level, Vector2 anchorLow, Vector2 anchorHigh, int width, ContextMenuItem *menus) 766 { 767 const int itemHeight = 28; 768 const int itemSpacing = 1; 769 const int padding = 8; 770 int itemCount = 0; 771 for (int i = 0; menus[i].text[0]; i++) 772 { 773 itemCount = itemCount > menus[i].index ? itemCount : menus[i].index; 774 } 775 776 Rectangle contextMenu = {0, 0, width, 777 (itemHeight + itemSpacing) * (itemCount + 1) + itemSpacing + padding * 2}; 778 779 Vector2 anchor = anchorHigh.y - 30 > contextMenu.height ? anchorHigh : anchorLow; 780 float anchorPivotY = anchorHigh.y - 30 > contextMenu.height ? 1.0f : 0.0f; 781 782 contextMenu.x = anchor.x - contextMenu.width * 0.5f; 783 contextMenu.y = anchor.y - contextMenu.height * anchorPivotY; 784 contextMenu.x = fmaxf(0, fminf(GetScreenWidth() - contextMenu.width, contextMenu.x)); 785 contextMenu.y = fmaxf(0, fminf(GetScreenHeight() - contextMenu.height, contextMenu.y)); 786 787 DrawTextureNPatch(spriteSheet, uiPanelPatch, contextMenu, Vector2Zero(), 0, WHITE); 788 DrawTextureRec(spriteSheet, uiDiamondMarker, (Vector2){anchor.x - uiDiamondMarker.width / 2, anchor.y - uiDiamondMarker.height / 2}, WHITE); 789 const int itemX = contextMenu.x + itemSpacing; 790 const int itemWidth = contextMenu.width - itemSpacing * 2; 791 #define ITEM_Y(idx) (contextMenu.y + itemSpacing + (itemHeight + itemSpacing) * idx + padding) 792 #define ITEM_RECT(idx, marginLR) itemX + (marginLR) + padding, ITEM_Y(idx), itemWidth - (marginLR) * 2 - padding * 2, itemHeight 793 int status = 0; 794 for (int i = 0; menus[i].text[0]; i++) 795 { 796 if (menus[i].action) 797 { 798 if (Button(menus[i].text, ITEM_RECT(menus[i].index, 0), &menus[i].buttonState)) 799 { 800 status = menus[i].action(level, &menus[i].args); 801 } 802 } 803 else 804 { 805 DrawBoxedText(menus[i].text, ITEM_RECT(menus[i].index, 0), menus[i].alignX, 0.5f, WHITE); 806 } 807 } 808 809 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && !CheckCollisionPointRec(GetMousePosition(), contextMenu)) 810 { 811 return 1; 812 } 813 814 return status; 815 } 816 817 void DrawLevelBuildingStateMainContextMenu(Level *level, Tower *tower, int32_t sellValue, float hp, float maxHitpoints, Vector2 anchorLow, Vector2 anchorHigh) 818 { 819 ContextMenuItem menu[12] = {0}; 820 int menuCount = 0; 821 int menuIndex = 0; 822 if (tower) 823 { 824 825 if (tower) { 826 menu[menuCount++] = ContextMenuItemText(menuIndex++, GetTowerName(tower->towerType), 0.5f); 827 } 828 829 // two texts, same line 830 menu[menuCount++] = ContextMenuItemText(menuIndex, "HP:", 0.0f); 831 menu[menuCount++] = ContextMenuItemText(menuIndex++, TextFormat("%.1f / %.1f", hp, maxHitpoints), 1.0f); 832 833 if (tower->towerType != TOWER_TYPE_BASE) 834 { 835 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "Upgrade", OnContextMenuUpgrade, 836 (ContextMenuArgs){.tower = tower}); 837 } 838 839 if (tower->towerType != TOWER_TYPE_BASE) 840 { 841 842 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "Sell", OnContextMenuSell, 843 (ContextMenuArgs){.tower = tower, .int32 = sellValue}); 844 } 845 if (tower->towerType != TOWER_TYPE_BASE && tower->damage > 0 && level->playerGold >= 1) 846 { 847 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "Repair 1 (1G)", OnContextMenuRepair, 848 (ContextMenuArgs){.tower = tower}); 849 } 850 } 851 else 852 { 853 menu[menuCount] = ContextMenuItemButton(menuIndex++, 854 TextFormat("Wall: %dG", GetTowerCosts(TOWER_TYPE_WALL)), 855 OnContextMenuBuild, (ContextMenuArgs){ .uint8 = (uint8_t) TOWER_TYPE_WALL}); 856 menu[menuCount++].buttonState.isDisabled = level->playerGold < GetTowerCosts(TOWER_TYPE_WALL); 857 858 menu[menuCount] = ContextMenuItemButton(menuIndex++, 859 TextFormat("Archer: %dG", GetTowerCosts(TOWER_TYPE_ARCHER)), 860 OnContextMenuBuild, (ContextMenuArgs){ .uint8 = (uint8_t) TOWER_TYPE_ARCHER}); 861 menu[menuCount++].buttonState.isDisabled = level->playerGold < GetTowerCosts(TOWER_TYPE_ARCHER); 862 863 menu[menuCount] = ContextMenuItemButton(menuIndex++, 864 TextFormat("Ballista: %dG", GetTowerCosts(TOWER_TYPE_BALLISTA)), 865 OnContextMenuBuild, (ContextMenuArgs){ .uint8 = (uint8_t) TOWER_TYPE_BALLISTA}); 866 menu[menuCount++].buttonState.isDisabled = level->playerGold < GetTowerCosts(TOWER_TYPE_BALLISTA); 867 868 menu[menuCount] = ContextMenuItemButton(menuIndex++, 869 TextFormat("Catapult: %dG", GetTowerCosts(TOWER_TYPE_CATAPULT)), 870 OnContextMenuBuild, (ContextMenuArgs){ .uint8 = (uint8_t) TOWER_TYPE_CATAPULT}); 871 menu[menuCount++].buttonState.isDisabled = level->playerGold < GetTowerCosts(TOWER_TYPE_CATAPULT); 872 } 873 874 if (DrawContextMenu(level, anchorLow, anchorHigh, 150, menu)) 875 { 876 level->placementContextMenuStatus = -1; 877 } 878 }
879 880 void DrawLevelBuildingState(Level *level) 881 { 882 // when the context menu is not active, we update the placement position 883 if (level->placementContextMenuStatus == 0) 884 { 885 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera); 886 float hitDistance = ray.position.y / -ray.direction.y; 887 float hitX = ray.direction.x * hitDistance + ray.position.x; 888 float hitY = ray.direction.z * hitDistance + ray.position.z;
889 level->placementX = (int)floorf(hitX + 0.5f);
890 level->placementY = (int)floorf(hitY + 0.5f); 891 } 892 893 // the currently hovered/selected tower
894 Tower *tower = TowerGetAt(level->placementX, level->placementY);
895 // show the range of the tower when hovering/selecting it 896 TowerUpdateRangeFade(tower, 0.0f); 897 898 BeginMode3D(level->camera); 899 DrawLevelGround(level); 900 PathFindingMapUpdate(0, 0); 901 TowerDraw(); 902 EnemyDraw(); 903 ProjectileDraw(); 904 ParticleDraw();
905 DrawEnemyPaths(level); 906 907 guiState.isBlocked = 0; 908 909 // Hover rectangle, when the mouse is over the map
910 int isHovering = level->placementX >= -5 && level->placementX <= 5 && level->placementY >= -5 && level->placementY <= 5;
911 if (isHovering) 912 { 913 DrawCubeWires((Vector3){level->placementX, 0.75f, level->placementY}, 1.0f, 1.5f, 1.0f, GREEN); 914 } 915 916 EndMode3D(); 917 918 TowerDrawHealthBars(level->camera); 919
920 DrawTitle("Building phase"); 921
922 // Draw the context menu when the context menu is active 923 if (level->placementContextMenuStatus >= 1) 924 { 925 float maxHitpoints = 0.0f; 926 float hp = 0.0f; 927 float damageFactor = 0.0f; 928 int32_t sellValue = 0; 929 930 if (tower) 931 { 932 maxHitpoints = TowerGetMaxHealth(tower); 933 hp = maxHitpoints - tower->damage; 934 damageFactor = 1.0f - tower->damage / maxHitpoints; 935 sellValue = (int32_t) ceilf(GetTowerCosts(tower->towerType) * 0.5f * damageFactor); 936 } 937 938 ContextMenuItem menu[12] = {0}; 939 int menuCount = 0; 940 int menuIndex = 0; 941 Vector2 anchorLow = GetWorldToScreen((Vector3){level->placementX, -0.25f, level->placementY}, level->camera); 942 Vector2 anchorHigh = GetWorldToScreen((Vector3){level->placementX, 2.0f, level->placementY}, level->camera); 943 944 if (level->placementContextMenuType == CONTEXT_MENU_TYPE_MAIN) 945 { 946 DrawLevelBuildingStateMainContextMenu(level, tower, sellValue, hp, maxHitpoints, anchorLow, anchorHigh); 947 } 948 else if (level->placementContextMenuType == CONTEXT_MENU_TYPE_UPGRADE) 949 { 950 int totalLevel = tower->upgradeState.damage + tower->upgradeState.speed + tower->upgradeState.range; 951 int costs = totalLevel * 4; 952 int isMaxLevel = totalLevel >= TOWER_MAX_STAGE; 953 menu[menuCount++] = ContextMenuItemText(menuIndex++, TextFormat("%s tower, stage %d%s", 954 GetTowerName(tower->towerType), totalLevel, isMaxLevel ? " (max)" : ""), 0.5f); 955 int buttonMenuIndex = menuIndex; 956 menu[menuCount++] = ContextMenuItemButton(menuIndex++, TextFormat("Speed: %d (%dG)", tower->upgradeState.speed, costs), 957 OnContextMenuDoUpgrade, (ContextMenuArgs){.tower = tower, .uint8 = UPGRADE_TYPE_SPEED, .int32 = costs}); 958 menu[menuCount++] = ContextMenuItemButton(menuIndex++, TextFormat("Damage: %d (%dG)", tower->upgradeState.damage, costs), 959 OnContextMenuDoUpgrade, (ContextMenuArgs){.tower = tower, .uint8 = UPGRADE_TYPE_DAMAGE, .int32 = costs}); 960 menu[menuCount++] = ContextMenuItemButton(menuIndex++, TextFormat("Range: %d (%dG)", tower->upgradeState.range, costs), 961 OnContextMenuDoUpgrade, (ContextMenuArgs){.tower = tower, .uint8 = UPGRADE_TYPE_RANGE, .int32 = costs}); 962 963 // check if buttons should be disabled 964 if (isMaxLevel || level->playerGold < costs) 965 { 966 for (int i = buttonMenuIndex; i < menuCount; i++) 967 { 968 menu[i].buttonState.isDisabled = 1; 969 } 970 } 971 972 if (DrawContextMenu(level, anchorLow, anchorHigh, 220, menu)) 973 { 974 level->placementContextMenuType = CONTEXT_MENU_TYPE_MAIN; 975 } 976 } 977 else if (level->placementContextMenuType == CONTEXT_MENU_TYPE_SELL_CONFIRM) 978 { 979 menu[menuCount++] = ContextMenuItemText(menuIndex++, TextFormat("Sell %s?", GetTowerName(tower->towerType)), 0.5f); 980 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "Yes", OnContextMenuSellConfirm, 981 (ContextMenuArgs){.tower = tower, .int32 = sellValue}); 982 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "No", OnContextMenuSellCancel, (ContextMenuArgs){0}); 983 Vector2 anchorLow = {GetScreenWidth() * 0.5f, GetScreenHeight() * 0.5f}; 984 if (DrawContextMenu(level, anchorLow, anchorLow, 150, menu)) 985 { 986 level->placementContextMenuStatus = -1; 987 level->placementContextMenuType = CONTEXT_MENU_TYPE_MAIN; 988 } 989 } 990 } 991 992 // Activate the context menu when the mouse is clicked and the context menu is not active 993 else if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON) && isHovering && level->placementContextMenuStatus <= 0) 994 { 995 level->placementContextMenuStatus += 1; 996 } 997 998 if (level->placementContextMenuStatus == 0) 999 { 1000 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 1001 { 1002 level->nextState = LEVEL_STATE_RESET; 1003 } 1004 1005 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0)) 1006 { 1007 level->nextState = LEVEL_STATE_BATTLE; 1008 } 1009 1010 } 1011 } 1012 1013 void InitBattleStateConditions(Level *level) 1014 { 1015 level->state = LEVEL_STATE_BATTLE; 1016 level->nextState = LEVEL_STATE_NONE; 1017 level->waveEndTimer = 0.0f; 1018 for (int i = 0; i < 10; i++) 1019 { 1020 EnemyWave *wave = &level->waves[i]; 1021 wave->spawned = 0; 1022 wave->timeToSpawnNext = wave->delay; 1023 } 1024 } 1025 1026 void DrawLevelBattleState(Level *level) 1027 { 1028 BeginMode3D(level->camera); 1029 DrawLevelGround(level); 1030 TowerDraw(); 1031 EnemyDraw(); 1032 ProjectileDraw(); 1033 ParticleDraw(); 1034 guiState.isBlocked = 0; 1035 EndMode3D(); 1036 1037 EnemyDrawHealthbars(level->camera); 1038 TowerDrawHealthBars(level->camera); 1039 1040 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 1041 { 1042 level->nextState = LEVEL_STATE_RESET; 1043 } 1044 1045 int maxCount = 0; 1046 int remainingCount = 0; 1047 for (int i = 0; i < 10; i++) 1048 { 1049 EnemyWave *wave = &level->waves[i]; 1050 if (wave->wave != level->currentWave) 1051 { 1052 continue; 1053 } 1054 maxCount += wave->count; 1055 remainingCount += wave->count - wave->spawned; 1056 } 1057 int aliveCount = EnemyCount(); 1058 remainingCount += aliveCount; 1059 1060 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount); 1061 DrawTitle(text); 1062 } 1063 1064 void DrawLevel(Level *level) 1065 { 1066 switch (level->state) 1067 { 1068 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break; 1069 case LEVEL_STATE_BUILDING_PLACEMENT: DrawLevelBuildingPlacementState(level); break; 1070 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break; 1071 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break; 1072 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break; 1073 default: break; 1074 } 1075 1076 DrawLevelHud(level); 1077 } 1078 1079 void UpdateLevel(Level *level) 1080 { 1081 if (level->state == LEVEL_STATE_BATTLE) 1082 { 1083 int activeWaves = 0; 1084 for (int i = 0; i < 10; i++) 1085 { 1086 EnemyWave *wave = &level->waves[i]; 1087 if (wave->spawned >= wave->count || wave->wave != level->currentWave) 1088 { 1089 continue; 1090 } 1091 activeWaves++; 1092 wave->timeToSpawnNext -= gameTime.deltaTime; 1093 if (wave->timeToSpawnNext <= 0.0f) 1094 { 1095 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y); 1096 if (enemy) 1097 { 1098 wave->timeToSpawnNext = wave->interval; 1099 wave->spawned++; 1100 } 1101 } 1102 } 1103 if (GetTowerByType(TOWER_TYPE_BASE) == 0) { 1104 level->waveEndTimer += gameTime.deltaTime; 1105 if (level->waveEndTimer >= 2.0f) 1106 { 1107 level->nextState = LEVEL_STATE_LOST_WAVE; 1108 } 1109 } 1110 else if (activeWaves == 0 && EnemyCount() == 0) 1111 { 1112 level->waveEndTimer += gameTime.deltaTime; 1113 if (level->waveEndTimer >= 2.0f) 1114 { 1115 level->nextState = LEVEL_STATE_WON_WAVE; 1116 } 1117 } 1118 } 1119 1120 PathFindingMapUpdate(0, 0); 1121 EnemyUpdate(); 1122 TowerUpdate(); 1123 ProjectileUpdate(); 1124 ParticleUpdate(); 1125 1126 if (level->nextState == LEVEL_STATE_RESET) 1127 { 1128 InitLevel(level); 1129 } 1130 1131 if (level->nextState == LEVEL_STATE_BATTLE) 1132 { 1133 InitBattleStateConditions(level); 1134 } 1135 1136 if (level->nextState == LEVEL_STATE_WON_WAVE) 1137 { 1138 level->currentWave++; 1139 level->state = LEVEL_STATE_WON_WAVE; 1140 } 1141 1142 if (level->nextState == LEVEL_STATE_LOST_WAVE) 1143 { 1144 level->state = LEVEL_STATE_LOST_WAVE; 1145 } 1146 1147 if (level->nextState == LEVEL_STATE_BUILDING) 1148 { 1149 level->state = LEVEL_STATE_BUILDING; 1150 level->placementContextMenuStatus = 0; 1151 } 1152 1153 if (level->nextState == LEVEL_STATE_BUILDING_PLACEMENT) 1154 { 1155 level->state = LEVEL_STATE_BUILDING_PLACEMENT; 1156 level->placementTransitionPosition = (Vector2){ 1157 level->placementX, level->placementY}; 1158 // initialize the spring to the current position 1159 level->placementTowerSpring = (PhysicsPoint){ 1160 .position = (Vector3){level->placementX, 8.0f, level->placementY}, 1161 .velocity = (Vector3){0.0f, 0.0f, 0.0f}, 1162 }; 1163 level->placementPhase = PLACEMENT_PHASE_STARTING; 1164 level->placementTimer = 0.0f; 1165 } 1166 1167 if (level->nextState == LEVEL_STATE_WON_LEVEL) 1168 { 1169 // make something of this later 1170 InitLevel(level); 1171 } 1172 1173 level->nextState = LEVEL_STATE_NONE; 1174 } 1175 1176 float nextSpawnTime = 0.0f; 1177 1178 void ResetGame() 1179 { 1180 InitLevel(currentLevel); 1181 } 1182 1183 void InitGame() 1184 { 1185 TowerInit(); 1186 EnemyInit(); 1187 ProjectileInit(); 1188 ParticleInit(); 1189 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f); 1190 1191 currentLevel = levels; 1192 InitLevel(currentLevel); 1193 } 1194 1195 //# Immediate GUI functions 1196 1197 void DrawHealthBar(Camera3D camera, Vector3 position, Vector2 screenOffset, float healthRatio, Color barColor, float healthBarWidth) 1198 { 1199 const float healthBarHeight = 6.0f; 1200 const float healthBarOffset = 15.0f; 1201 const float inset = 2.0f; 1202 const float innerWidth = healthBarWidth - inset * 2; 1203 const float innerHeight = healthBarHeight - inset * 2; 1204 1205 Vector2 screenPos = GetWorldToScreen(position, camera); 1206 screenPos = Vector2Add(screenPos, screenOffset); 1207 float centerX = screenPos.x - healthBarWidth * 0.5f; 1208 float topY = screenPos.y - healthBarOffset; 1209 DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK); 1210 float healthWidth = innerWidth * healthRatio; 1211 DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor); 1212 } 1213 1214 void DrawBoxedText(const char *text, int x, int y, int width, int height, float alignX, float alignY, Color textColor) 1215 { 1216 Vector2 textSize = MeasureTextEx(gameFontNormal, text, gameFontNormal.baseSize, 1); 1217 1218 DrawTextEx(gameFontNormal, text, (Vector2){ 1219 x + (width - textSize.x) * alignX, 1220 y + (height - textSize.y) * alignY 1221 }, gameFontNormal.baseSize, 1, textColor); 1222 } 1223 1224 int Button(const char *text, int x, int y, int width, int height, ButtonState *state) 1225 { 1226 Rectangle bounds = {x, y, width, height}; 1227 int isPressed = 0; 1228 int isSelected = state && state->isSelected; 1229 int isDisabled = state && state->isDisabled; 1230 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !isDisabled) 1231 { 1232 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) 1233 { 1234 isPressed = 1; 1235 } 1236 guiState.isBlocked = 1; 1237 DrawTextureNPatch(spriteSheet, isPressed ? uiButtonPressed : uiButtonHovered, 1238 bounds, Vector2Zero(), 0, WHITE); 1239 } 1240 else 1241 { 1242 DrawTextureNPatch(spriteSheet, isSelected ? uiButtonHovered : (isDisabled ? uiButtonDisabled : uiButtonNormal), 1243 bounds, Vector2Zero(), 0, WHITE); 1244 } 1245 Vector2 textSize = MeasureTextEx(gameFontNormal, text, gameFontNormal.baseSize, 1); 1246 Color textColor = isDisabled ? LIGHTGRAY : BLACK; 1247 DrawTextEx(gameFontNormal, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, gameFontNormal.baseSize, 1, textColor); 1248 return isPressed; 1249 } 1250 1251 //# Main game loop 1252 1253 void GameUpdate() 1254 { 1255 UpdateLevel(currentLevel); 1256 } 1257 1258 int main(void)
1259 { 1260 int screenWidth, screenHeight;
1261 GetPreferredSize(&screenWidth, &screenHeight); 1262 InitWindow(screenWidth, screenHeight, "Tower defense"); 1263 float gamespeed = 1.0f; 1264 int frameRate = 30; 1265 SetTargetFPS(30); 1266 1267 LoadAssets(); 1268 InitGame(); 1269 1270 float pause = 1.0f; 1271
1272 while (!WindowShouldClose()) 1273 { 1274 if (IsPaused()) { 1275 // canvas is not visible in browser - do nothing 1276 continue; 1277 } 1278 1279 if (IsKeyPressed(KEY_F))
1280 { 1281 frameRate = (frameRate + 5) % 30; 1282 frameRate = frameRate < 10 ? 10 : frameRate; 1283 SetTargetFPS(frameRate); 1284 } 1285 1286 if (IsKeyPressed(KEY_T)) 1287 { 1288 gamespeed += 0.1f; 1289 if (gamespeed > 1.05f) gamespeed = 0.1f; 1290 } 1291 1292 if (IsKeyPressed(KEY_P)) 1293 { 1294 pause = pause > 0.5f ? 0.0f : 1.0f; 1295 } 1296 1297 float dt = GetFrameTime() * gamespeed * pause; 1298 // cap maximum delta time to 0.1 seconds to prevent large time steps 1299 if (dt > 0.1f) dt = 0.1f; 1300 gameTime.time += dt; 1301 gameTime.deltaTime = dt; 1302 gameTime.frameCount += 1; 1303 1304 float fixedTimeDiff = gameTime.time - gameTime.fixedTimeStart; 1305 gameTime.fixedStepCount = (uint8_t)(fixedTimeDiff / gameTime.fixedDeltaTime); 1306 1307 BeginDrawing(); 1308 ClearBackground((Color){0x4E, 0x63, 0x26, 0xFF}); 1309 1310 GameUpdate(); 1311 DrawLevel(currentLevel); 1312 1313 if (gamespeed != 1.0f) 1314 DrawText(TextFormat("Speed: %.1f", gamespeed), GetScreenWidth() - 180, 60, 20, WHITE); 1315 EndDrawing(); 1316 1317 gameTime.fixedTimeStart += gameTime.fixedStepCount * gameTime.fixedDeltaTime; 1318 } 1319 1320 CloseWindow(); 1321 1322 return 0; 1323 }
  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 
 21 #define ENEMY_TYPE_MINION 1
 22 #define ENEMY_TYPE_RUNNER 2
 23 #define ENEMY_TYPE_SHIELD 3
 24 #define ENEMY_TYPE_BOSS 4
 25 
 26 #define PARTICLE_MAX_COUNT 400
 27 #define PARTICLE_TYPE_NONE 0
 28 #define PARTICLE_TYPE_EXPLOSION 1
 29 
 30 typedef struct Particle
 31 {
 32   uint8_t particleType;
 33   float spawnTime;
 34   float lifetime;
 35   Vector3 position;
 36   Vector3 velocity;
 37   Vector3 scale;
 38 } Particle;
 39 
 40 #define TOWER_MAX_COUNT 400
 41 enum TowerType
 42 {
 43   TOWER_TYPE_NONE,
 44   TOWER_TYPE_BASE,
 45   TOWER_TYPE_ARCHER,
 46   TOWER_TYPE_BALLISTA,
 47   TOWER_TYPE_CATAPULT,
 48   TOWER_TYPE_WALL,
 49   TOWER_TYPE_COUNT
 50 };
 51 
 52 typedef struct HitEffectConfig
 53 {
 54   float damage;
 55   float areaDamageRadius;
 56   float pushbackPowerDistance;
 57 } HitEffectConfig;
 58 
 59 typedef struct TowerTypeConfig
 60 {
 61   const char *name;
 62   float cooldown;
 63   float range;
 64   float projectileSpeed;
 65   
 66   uint8_t cost;
 67   uint8_t projectileType;
 68   uint16_t maxHealth;
 69 
 70   HitEffectConfig hitEffect;
 71 } TowerTypeConfig;
 72 
 73 #define TOWER_MAX_STAGE 10
 74 
 75 typedef struct TowerUpgradeState
 76 {
 77   uint8_t range;
 78   uint8_t damage;
 79   uint8_t speed;
 80 } TowerUpgradeState;
 81 
 82 typedef struct Tower
 83 {
 84   int16_t x, y;
 85   uint8_t towerType;
 86   TowerUpgradeState upgradeState;
 87   Vector2 lastTargetPosition;
 88   float cooldown;
 89   float damage;
90 // alpha value for the range circle drawing 91 float drawRangeAlpha;
92 } Tower; 93 94 typedef struct GameTime 95 { 96 float time; 97 float deltaTime; 98 uint32_t frameCount; 99 100 float fixedDeltaTime; 101 // leaving the fixed time stepping to the update functions, 102 // we need to know the fixed time at the start of the frame 103 float fixedTimeStart; 104 // and the number of fixed steps that we have to make this frame 105 // The fixedTime is fixedTimeStart + n * fixedStepCount 106 uint8_t fixedStepCount; 107 } GameTime; 108 109 typedef struct ButtonState { 110 char isSelected; 111 char isDisabled; 112 } ButtonState; 113 114 typedef struct GUIState { 115 int isBlocked; 116 } GUIState; 117 118 typedef enum LevelState 119 { 120 LEVEL_STATE_NONE, 121 LEVEL_STATE_BUILDING, 122 LEVEL_STATE_BUILDING_PLACEMENT, 123 LEVEL_STATE_BATTLE, 124 LEVEL_STATE_WON_WAVE, 125 LEVEL_STATE_LOST_WAVE, 126 LEVEL_STATE_WON_LEVEL, 127 LEVEL_STATE_RESET, 128 } LevelState; 129 130 typedef struct EnemyWave { 131 uint8_t enemyType; 132 uint8_t wave; 133 uint16_t count; 134 float interval; 135 float delay; 136 Vector2 spawnPosition; 137 138 uint16_t spawned; 139 float timeToSpawnNext; 140 } EnemyWave; 141 142 #define ENEMY_MAX_WAVE_COUNT 10 143 144 typedef enum PlacementPhase 145 { 146 PLACEMENT_PHASE_STARTING, 147 PLACEMENT_PHASE_MOVING, 148 PLACEMENT_PHASE_PLACING, 149 } PlacementPhase; 150 151 typedef struct Level 152 { 153 int seed; 154 LevelState state; 155 LevelState nextState; 156 Camera3D camera; 157 int placementMode; 158 PlacementPhase placementPhase; 159 float placementTimer; 160 161 int16_t placementX; 162 int16_t placementY; 163 int8_t placementContextMenuStatus; 164 int8_t placementContextMenuType; 165 166 Vector2 placementTransitionPosition; 167 PhysicsPoint placementTowerSpring; 168 169 int initialGold; 170 int playerGold; 171 172 EnemyWave waves[ENEMY_MAX_WAVE_COUNT]; 173 int currentWave; 174 float waveEndTimer; 175 } Level; 176 177 typedef struct DeltaSrc 178 { 179 char x, y; 180 } DeltaSrc; 181 182 typedef struct PathfindingMap 183 { 184 int width, height; 185 float scale; 186 float *distances; 187 long *towerIndex; 188 DeltaSrc *deltaSrc; 189 float maxDistance; 190 Matrix toMapSpace; 191 Matrix toWorldSpace; 192 } PathfindingMap; 193 194 // when we execute the pathfinding algorithm, we need to store the active nodes 195 // in a queue. Each node has a position, a distance from the start, and the 196 // position of the node that we came from. 197 typedef struct PathfindingNode 198 { 199 int16_t x, y, fromX, fromY; 200 float distance; 201 } PathfindingNode; 202 203 typedef struct EnemyId 204 { 205 uint16_t index; 206 uint16_t generation; 207 } EnemyId; 208 209 typedef struct EnemyClassConfig 210 { 211 float speed; 212 float health; 213 float shieldHealth; 214 float shieldDamageAbsorption; 215 float radius; 216 float maxAcceleration; 217 float requiredContactTime; 218 float explosionDamage; 219 float explosionRange; 220 float explosionPushbackPower; 221 int goldValue; 222 } EnemyClassConfig; 223 224 typedef struct Enemy 225 { 226 int16_t currentX, currentY; 227 int16_t nextX, nextY; 228 Vector2 simPosition; 229 Vector2 simVelocity; 230 uint16_t generation; 231 float walkedDistance; 232 float startMovingTime; 233 float damage, futureDamage; 234 float shieldDamage; 235 float contactTime; 236 uint8_t enemyType; 237 uint8_t movePathCount; 238 Vector2 movePath[ENEMY_MAX_PATH_COUNT]; 239 } Enemy; 240 241 // a unit that uses sprites to be drawn 242 #define SPRITE_UNIT_ANIMATION_COUNT 6 243 #define SPRITE_UNIT_PHASE_WEAPON_IDLE 1 244 #define SPRITE_UNIT_PHASE_WEAPON_COOLDOWN 2 245 #define SPRITE_UNIT_PHASE_WEAPON_SHIELD 3 246 247 typedef struct SpriteAnimation 248 { 249 Rectangle srcRect; 250 Vector2 offset; 251 uint8_t animationId; 252 uint8_t frameCount; 253 uint8_t frameWidth; 254 float frameDuration; 255 } SpriteAnimation; 256 257 typedef struct SpriteUnit 258 { 259 float scale; 260 SpriteAnimation animations[SPRITE_UNIT_ANIMATION_COUNT]; 261 } SpriteUnit; 262 263 #define PROJECTILE_MAX_COUNT 1200 264 #define PROJECTILE_TYPE_NONE 0 265 #define PROJECTILE_TYPE_ARROW 1 266 #define PROJECTILE_TYPE_CATAPULT 2 267 #define PROJECTILE_TYPE_BALLISTA 3 268 269 typedef struct Projectile 270 { 271 uint8_t projectileType; 272 float shootTime; 273 float arrivalTime; 274 float distance; 275 Vector3 position; 276 Vector3 target; 277 Vector3 directionNormal; 278 EnemyId targetEnemy; 279 HitEffectConfig hitEffectConfig; 280 } Projectile;
281 282 //# Function declarations
283 int Button(const char *text, int x, int y, int width, int height, ButtonState *state); 284 int EnemyAddDamageRange(Vector2 position, float range, float damage); 285 int EnemyAddDamage(Enemy *enemy, float damage); 286 287 //# Enemy functions 288 void EnemyInit(); 289 void EnemyDraw(); 290 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource); 291 void EnemyUpdate(); 292 float EnemyGetCurrentMaxSpeed(Enemy *enemy); 293 float EnemyGetMaxHealth(Enemy *enemy); 294 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY); 295 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount); 296 EnemyId EnemyGetId(Enemy *enemy); 297 Enemy *EnemyTryResolve(EnemyId enemyId); 298 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY); 299 int EnemyAddDamage(Enemy *enemy, float damage); 300 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range); 301 int EnemyCount(); 302 void EnemyDrawHealthbars(Camera3D camera); 303
304 //# Tower functions 305 void TowerInit(); 306 float TowerGetMaxHealth(Tower *tower);
307 Tower *GetTowerByIndex(int index); 308 Tower *TowerGetAt(int16_t x, int16_t y); 309 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y); 310 Tower *GetTowerByType(uint8_t towerType); 311 int GetTowerCosts(uint8_t towerType); 312 const char *GetTowerName(uint8_t towerType); 313 float TowerGetMaxHealth(Tower *tower);
314 void TowerDraw(); 315 void TowerDrawSingle(Tower tower); 316 void TowerDrawRange(Tower tower, float alpha); 317 void TowerUpdate();
318 void TowerUpdateRangeFade(Tower *selectedTower, float fadeoutTarget); 319 void TowerDrawHealthBars(Camera3D camera); 320 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase); 321 322 //# Particles 323 void ParticleInit(); 324 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime); 325 void ParticleUpdate(); 326 void ParticleDraw(); 327 328 //# Projectiles 329 void ProjectileInit(); 330 void ProjectileDraw(); 331 void ProjectileUpdate(); 332 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig); 333 334 //# Pathfinding map 335 void PathfindingMapInit(int width, int height, Vector3 translate, float scale); 336 float PathFindingGetDistance(int mapX, int mapY); 337 Vector2 PathFindingGetGradient(Vector3 world); 338 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY); 339 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells); 340 void PathFindingMapDraw(); 341 342 //# UI 343 void DrawHealthBar(Camera3D camera, Vector3 position, Vector2 screenOffset, float healthRatio, Color barColor, float healthBarWidth); 344 345 //# Level 346 void DrawLevelGround(Level *level); 347 void DrawEnemyPath(Level *level, Color arrowColor); 348 349 //# variables 350 extern Level *currentLevel; 351 extern Enemy enemies[ENEMY_MAX_COUNT]; 352 extern int enemyCount; 353 extern EnemyClassConfig enemyClassConfigs[]; 354 355 extern GUIState guiState; 356 extern GameTime gameTime; 357 extern Tower towers[TOWER_MAX_COUNT]; 358 extern int towerCount; 359 360 extern Texture2D palette, spriteSheet; 361 362 #endif
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 
  4 static TowerTypeConfig towerTypeConfigs[TOWER_TYPE_COUNT] = {
  5     [TOWER_TYPE_BASE] = {
  6         .name = "Castle",
  7         .maxHealth = 10,
  8     },
  9     [TOWER_TYPE_ARCHER] = {
 10         .name = "Archer",
 11         .cooldown = 0.5f,
 12         .range = 3.0f,
 13         .cost = 6,
 14         .maxHealth = 10,
 15         .projectileSpeed = 4.0f,
 16         .projectileType = PROJECTILE_TYPE_ARROW,
 17         .hitEffect = {
 18           .damage = 3.0f,
 19         }
 20     },
 21     [TOWER_TYPE_BALLISTA] = {
 22         .name = "Ballista",
 23         .cooldown = 1.5f,
 24         .range = 6.0f,
 25         .cost = 9,
 26         .maxHealth = 10,
 27         .projectileSpeed = 10.0f,
 28         .projectileType = PROJECTILE_TYPE_BALLISTA,
 29         .hitEffect = {
 30           .damage = 8.0f,
 31           .pushbackPowerDistance = 0.25f,
 32         }
 33     },
 34     [TOWER_TYPE_CATAPULT] = {
 35         .name = "Catapult",
 36         .cooldown = 1.7f,
 37         .range = 5.0f,
 38         .cost = 10,
 39         .maxHealth = 10,
 40         .projectileSpeed = 3.0f,
 41         .projectileType = PROJECTILE_TYPE_CATAPULT,
 42         .hitEffect = {
 43           .damage = 2.0f,
 44           .areaDamageRadius = 1.75f,
 45         }
 46     },
 47     [TOWER_TYPE_WALL] = {
 48         .name = "Wall",
 49         .cost = 2,
 50         .maxHealth = 10,
 51     },
 52 };
 53 
 54 Tower towers[TOWER_MAX_COUNT];
 55 int towerCount = 0;
 56 
 57 Model towerModels[TOWER_TYPE_COUNT];
 58 
 59 // definition of our archer unit
 60 SpriteUnit archerUnit = {
 61   .animations[0] = {
 62     .srcRect = {0, 0, 16, 16},
 63     .offset = {7, 1},
 64     .frameCount = 1,
 65     .frameDuration = 0.0f,
 66   },
 67   .animations[1] = {
 68     .animationId = SPRITE_UNIT_PHASE_WEAPON_COOLDOWN,
 69     .srcRect = {16, 0, 6, 16},
 70     .offset = {8, 0},
 71   },
 72   .animations[2] = {
 73     .animationId = SPRITE_UNIT_PHASE_WEAPON_IDLE,
 74     .srcRect = {22, 0, 11, 16},
 75     .offset = {10, 0},
 76   },
 77 };
 78 
 79 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase)
 80 {
 81   float unitScale = unit.scale == 0 ? 1.0f : unit.scale;
 82   float xScale = flip ? -1.0f : 1.0f;
 83   Camera3D camera = currentLevel->camera;
 84   float size = 0.5f * unitScale;
 85   // we want the sprite to face the camera, so we need to calculate the up vector
 86   Vector3 forward = Vector3Subtract(camera.target, camera.position);
 87   Vector3 up = {0, 1, 0};
 88   Vector3 right = Vector3CrossProduct(forward, up);
 89   up = Vector3Normalize(Vector3CrossProduct(right, forward));
 90   
 91   for (int i=0;i<SPRITE_UNIT_ANIMATION_COUNT;i++)
 92   {
 93     SpriteAnimation anim = unit.animations[i];
 94     if (anim.animationId != phase && anim.animationId != 0)
 95     {
 96       continue;
 97     }
 98     Rectangle srcRect = anim.srcRect;
 99     if (anim.frameCount > 1)
100     {
101       int w = anim.frameWidth > 0 ? anim.frameWidth : srcRect.width;
102       srcRect.x += (int)(t / anim.frameDuration) % anim.frameCount * w;
103     }
104     Vector2 offset = { anim.offset.x / 16.0f * size, anim.offset.y / 16.0f * size * xScale };
105     Vector2 scale = { srcRect.width / 16.0f * size, srcRect.height / 16.0f * size };
106     
107     if (flip)
108     {
109       srcRect.x += srcRect.width;
110       srcRect.width = -srcRect.width;
111       offset.x = scale.x - offset.x;
112     }
113     DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
114     // move the sprite slightly towards the camera to avoid z-fighting
115     position = Vector3Add(position, Vector3Scale(Vector3Normalize(forward), -0.01f));  
116   }
117 }
118 
119 void TowerInit()
120 {
121   for (int i = 0; i < TOWER_MAX_COUNT; i++)
122   {
123     towers[i] = (Tower){0};
124   }
125   towerCount = 0;
126 
127   towerModels[TOWER_TYPE_BASE] = LoadModel("data/keep.glb");
128   towerModels[TOWER_TYPE_WALL] = LoadModel("data/wall-0000.glb");
129 
130   for (int i = 0; i < TOWER_TYPE_COUNT; i++)
131   {
132     if (towerModels[i].materials)
133     {
134       // assign the palette texture to the material of the model (0 is not used afaik)
135       towerModels[i].materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
136     }
137   }
138 }
139 
140 static void TowerGunUpdate(Tower *tower)
141 {
142   TowerTypeConfig config = towerTypeConfigs[tower->towerType];
143   if (tower->cooldown <= 0.0f)
144   {
145     Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, config.range);
146     if (enemy)
147     {
148       tower->cooldown = config.cooldown;
149       // shoot the enemy; determine future position of the enemy
150       float bulletSpeed = config.projectileSpeed;
151       Vector2 velocity = enemy->simVelocity;
152       Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0);
153       Vector2 towerPosition = {tower->x, tower->y};
154       float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed;
155       for (int i = 0; i < 8; i++) {
156         velocity = enemy->simVelocity;
157         futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0);
158         float distance = Vector2Distance(towerPosition, futurePosition);
159         float eta2 = distance / bulletSpeed;
160         if (fabs(eta - eta2) < 0.01f) {
161           break;
162         }
163         eta = (eta2 + eta) * 0.5f;
164       }
165 
166       ProjectileTryAdd(config.projectileType, enemy, 
167         (Vector3){towerPosition.x, 1.33f, towerPosition.y}, 
168         (Vector3){futurePosition.x, 0.25f, futurePosition.y},
169         bulletSpeed, config.hitEffect);
170       enemy->futureDamage += config.hitEffect.damage;
171       tower->lastTargetPosition = futurePosition;
172     }
173   }
174   else
175   {
176     tower->cooldown -= gameTime.deltaTime;
177   }
178 }
179 
180 Tower *TowerGetAt(int16_t x, int16_t y)
181 {
182   for (int i = 0; i < towerCount; i++)
183   {
184     if (towers[i].x == x && towers[i].y == y && towers[i].towerType != TOWER_TYPE_NONE)
185     {
186       return &towers[i];
187     }
188   }
189   return 0;
190 }
191 
192 Tower *GetTowerByIndex(int index) 193 { 194 if (index < 0 || index >= towerCount) 195 { 196 return 0; 197 } 198 return &towers[index]; 199 } 200
201 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y) 202 { 203 if (towerCount >= TOWER_MAX_COUNT) 204 { 205 return 0; 206 } 207 208 Tower *tower = TowerGetAt(x, y); 209 if (tower) 210 { 211 return 0; 212 } 213 214 tower = &towers[towerCount++]; 215 *tower = (Tower){ 216 .x = x, 217 .y = y, 218 .towerType = towerType, 219 .cooldown = 0.0f, 220 .damage = 0.0f, 221 }; 222 return tower; 223 } 224 225 Tower *GetTowerByType(uint8_t towerType) 226 { 227 for (int i = 0; i < towerCount; i++) 228 { 229 if (towers[i].towerType == towerType) 230 { 231 return &towers[i]; 232 } 233 } 234 return 0; 235 } 236 237 const char *GetTowerName(uint8_t towerType) 238 { 239 return towerTypeConfigs[towerType].name; 240 } 241 242 int GetTowerCosts(uint8_t towerType) 243 { 244 return towerTypeConfigs[towerType].cost; 245 } 246 247 float TowerGetMaxHealth(Tower *tower) 248 {
249 return towerTypeConfigs[tower->towerType].maxHealth; 250 } 251 252 float GetTowerRange(Tower tower) 253 { 254 return towerTypeConfigs[tower.towerType].range; 255 } 256 257 void TowerUpdateRangeFade(Tower *selectedTower, float fadeoutTarget) 258 { 259 // animate fade in and fade out of range drawing using framerate independent lerp 260 float fadeLerp = 1.0f - powf(0.005f, gameTime.deltaTime); 261 for (int i = 0; i < TOWER_MAX_COUNT; i++) 262 { 263 Tower *fadingTower = GetTowerByIndex(i); 264 if (!fadingTower) 265 { 266 break; 267 } 268 float drawRangeTarget = fadingTower == selectedTower ? 1.0f : fadeoutTarget; 269 fadingTower->drawRangeAlpha = Lerp(fadingTower->drawRangeAlpha, drawRangeTarget, fadeLerp); 270 } 271 } 272 273 void TowerDrawRange(Tower tower, float alpha) 274 { 275 Color ringColor = (Color){255, 200, 100, 255}; 276 const int rings = 4; 277 const float radiusOffset = 0.5f; 278 const float animationSpeed = 2.0f; 279 float animation = 1.0f - fmodf(gameTime.time * animationSpeed, 1.0f); 280 float radius = GetTowerRange(tower); 281 // base circle 282 DrawCircle3D((Vector3){tower.x, 0.01f, tower.y}, radius, (Vector3){1, 0, 0}, 90, 283 Fade(ringColor, alpha)); 284 285 for (int i = 1; i < rings; i++) 286 { 287 float t = ((float)i + animation) / (float)rings; 288 float r = Lerp(radius, radius - radiusOffset, t * t); 289 float a = 1.0f - ((float)i + animation - 1) / (float) (rings - 1); 290 if (i == 1) 291 { 292 // fade out the outermost ring 293 a = animation; 294 } 295 a *= alpha; 296 297 DrawCircle3D((Vector3){tower.x, 0.01f, tower.y}, r, (Vector3){1, 0, 0}, 90, 298 Fade(ringColor, a)); 299 }
300 } 301 302 void TowerDrawSingle(Tower tower) 303 { 304 if (tower.towerType == TOWER_TYPE_NONE) 305 {
306 return; 307 } 308 309 if (tower.drawRangeAlpha > 2.0f/256.0f) 310 { 311 TowerDrawRange(tower, tower.drawRangeAlpha);
312 } 313 314 switch (tower.towerType) 315 { 316 case TOWER_TYPE_ARCHER: 317 { 318 Vector2 screenPosTower = GetWorldToScreen((Vector3){tower.x, 0.0f, tower.y}, currentLevel->camera); 319 Vector2 screenPosTarget = GetWorldToScreen((Vector3){tower.lastTargetPosition.x, 0.0f, tower.lastTargetPosition.y}, currentLevel->camera); 320 DrawModel(towerModels[TOWER_TYPE_WALL], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE); 321 DrawSpriteUnit(archerUnit, (Vector3){tower.x, 1.0f, tower.y}, 0, screenPosTarget.x > screenPosTower.x, 322 tower.cooldown > 0.2f ? SPRITE_UNIT_PHASE_WEAPON_COOLDOWN : SPRITE_UNIT_PHASE_WEAPON_IDLE); 323 } 324 break; 325 case TOWER_TYPE_BALLISTA: 326 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, BROWN); 327 break; 328 case TOWER_TYPE_CATAPULT: 329 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, DARKGRAY); 330 break; 331 default: 332 if (towerModels[tower.towerType].materials) 333 { 334 DrawModel(towerModels[tower.towerType], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE); 335 } else { 336 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY); 337 } 338 break; 339 } 340 } 341 342 void TowerDraw() 343 { 344 for (int i = 0; i < towerCount; i++) 345 { 346 TowerDrawSingle(towers[i]); 347 } 348 } 349 350 void TowerUpdate() 351 { 352 for (int i = 0; i < towerCount; i++) 353 { 354 Tower *tower = &towers[i]; 355 switch (tower->towerType) 356 { 357 case TOWER_TYPE_CATAPULT: 358 case TOWER_TYPE_BALLISTA: 359 case TOWER_TYPE_ARCHER: 360 TowerGunUpdate(tower); 361 break; 362 } 363 } 364 } 365 366 void TowerDrawHealthBars(Camera3D camera) 367 { 368 for (int i = 0; i < towerCount; i++) 369 { 370 Tower *tower = &towers[i]; 371 if (tower->towerType == TOWER_TYPE_NONE || tower->damage <= 0.0f) 372 { 373 continue; 374 } 375 376 Vector3 position = (Vector3){tower->x, 0.5f, tower->y}; 377 float maxHealth = TowerGetMaxHealth(tower); 378 float health = maxHealth - tower->damage; 379 float healthRatio = health / maxHealth; 380 381 DrawHealthBar(camera, position, Vector2Zero(), healthRatio, GREEN, 35.0f); 382 } 383 }
  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 #include <stdlib.h>
  4 #include <math.h>
  5 #include <rlgl.h>
  6 
  7 EnemyClassConfig enemyClassConfigs[] = {
  8     [ENEMY_TYPE_MINION] = {
  9       .health = 10.0f, 
 10       .speed = 0.6f, 
 11       .radius = 0.25f, 
 12       .maxAcceleration = 1.0f,
 13       .explosionDamage = 1.0f,
 14       .requiredContactTime = 0.5f,
 15       .explosionRange = 1.0f,
 16       .explosionPushbackPower = 0.25f,
 17       .goldValue = 1,
 18     },
 19     [ENEMY_TYPE_RUNNER] = {
 20       .health = 5.0f, 
 21       .speed = 1.0f, 
 22       .radius = 0.25f, 
 23       .maxAcceleration = 2.0f,
 24       .explosionDamage = 1.0f,
 25       .requiredContactTime = 0.5f,
 26       .explosionRange = 1.0f,
 27       .explosionPushbackPower = 0.25f,
 28       .goldValue = 2,
 29     },
 30     [ENEMY_TYPE_SHIELD] = {
 31       .health = 8.0f, 
 32       .speed = 0.5f, 
 33       .radius = 0.25f, 
 34       .maxAcceleration = 1.0f,
 35       .explosionDamage = 2.0f,
 36       .requiredContactTime = 0.5f,
 37       .explosionRange = 1.0f,
 38       .explosionPushbackPower = 0.25f,
 39       .goldValue = 3,
 40       .shieldDamageAbsorption = 4.0f,
 41       .shieldHealth = 25.0f,
 42     },
 43     [ENEMY_TYPE_BOSS] = {
 44       .health = 50.0f, 
 45       .speed = 0.4f, 
 46       .radius = 0.25f, 
 47       .maxAcceleration = 1.0f,
 48       .explosionDamage = 5.0f,
 49       .requiredContactTime = 0.5f,
 50       .explosionRange = 1.0f,
 51       .explosionPushbackPower = 0.25f,
 52       .goldValue = 10,
 53     },
 54 };
 55 
 56 Enemy enemies[ENEMY_MAX_COUNT];
 57 int enemyCount = 0;
 58 
 59 SpriteUnit enemySprites[] = {
 60     [ENEMY_TYPE_MINION] = {
 61       .animations[0] = {
 62         .srcRect = {0, 17, 16, 15},
 63         .offset = {8.0f, 0.0f},
 64         .frameCount = 6,
 65         .frameDuration = 0.1f,
 66       },
 67       .animations[1] = {
 68         .srcRect = {1, 33, 15, 14},
 69         .offset = {7.0f, 0.0f},
 70         .frameCount = 6,
 71         .frameWidth = 16,
 72         .frameDuration = 0.1f,
 73       },
 74     },
 75     [ENEMY_TYPE_RUNNER] = {
 76       .scale = 0.75f,
 77       .animations[0] = {
 78         .srcRect = {0, 17, 16, 15},
 79         .offset = {8.0f, 0.0f},
 80         .frameCount = 6,
 81         .frameDuration = 0.1f,
 82       },
 83     },
 84     [ENEMY_TYPE_SHIELD] = {
 85       .animations[0] = {
 86         .srcRect = {0, 17, 16, 15},
 87         .offset = {8.0f, 0.0f},
 88         .frameCount = 6,
 89         .frameDuration = 0.1f,
 90       },
 91       .animations[1] = {
 92         .animationId = SPRITE_UNIT_PHASE_WEAPON_SHIELD,
 93         .srcRect = {99, 17, 10, 11},
 94         .offset = {7.0f, 0.0f},
 95       },
 96     },
 97     [ENEMY_TYPE_BOSS] = {
 98       .scale = 1.5f,
 99       .animations[0] = {
100         .srcRect = {0, 17, 16, 15},
101         .offset = {8.0f, 0.0f},
102         .frameCount = 6,
103         .frameDuration = 0.1f,
104       },
105       .animations[1] = {
106         .srcRect = {97, 29, 14, 7},
107         .offset = {7.0f, -9.0f},
108       },
109     },
110 };
111 
112 void EnemyInit()
113 {
114   for (int i = 0; i < ENEMY_MAX_COUNT; i++)
115   {
116     enemies[i] = (Enemy){0};
117   }
118   enemyCount = 0;
119 }
120 
121 float EnemyGetCurrentMaxSpeed(Enemy *enemy)
122 {
123   return enemyClassConfigs[enemy->enemyType].speed;
124 }
125 
126 float EnemyGetMaxHealth(Enemy *enemy)
127 {
128   return enemyClassConfigs[enemy->enemyType].health;
129 }
130 
131 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY)
132 {
133   int16_t castleX = 0;
134   int16_t castleY = 0;
135   int16_t dx = castleX - currentX;
136   int16_t dy = castleY - currentY;
137   if (dx == 0 && dy == 0)
138   {
139     *nextX = currentX;
140     *nextY = currentY;
141     return 1;
142   }
143   Vector2 gradient = PathFindingGetGradient((Vector3){currentX, 0, currentY});
144 
145   if (gradient.x == 0 && gradient.y == 0)
146   {
147     *nextX = currentX;
148     *nextY = currentY;
149     return 1;
150   }
151 
152   if (fabsf(gradient.x) > fabsf(gradient.y))
153   {
154     *nextX = currentX + (int16_t)(gradient.x > 0.0f ? 1 : -1);
155     *nextY = currentY;
156     return 0;
157   }
158   *nextX = currentX;
159   *nextY = currentY + (int16_t)(gradient.y > 0.0f ? 1 : -1);
160   return 0;
161 }
162 
163 
164 // this function predicts the movement of the unit for the next deltaT seconds
165 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount)
166 {
167   const float pointReachedDistance = 0.25f;
168   const float pointReachedDistance2 = pointReachedDistance * pointReachedDistance;
169   const float maxSimStepTime = 0.015625f;
170   
171   float maxAcceleration = enemyClassConfigs[enemy->enemyType].maxAcceleration;
172   float maxSpeed = EnemyGetCurrentMaxSpeed(enemy);
173   int16_t nextX = enemy->nextX;
174   int16_t nextY = enemy->nextY;
175   Vector2 position = enemy->simPosition;
176   int passedCount = 0;
177   for (float t = 0.0f; t < deltaT; t += maxSimStepTime)
178   {
179     float stepTime = fminf(deltaT - t, maxSimStepTime);
180     Vector2 target = (Vector2){nextX, nextY};
181     float speed = Vector2Length(*velocity);
182     // draw the target position for debugging
183     //DrawCubeWires((Vector3){target.x, 0.2f, target.y}, 0.1f, 0.4f, 0.1f, RED);
184     Vector2 lookForwardPos = Vector2Add(position, Vector2Scale(*velocity, speed));
185     if (Vector2DistanceSqr(target, lookForwardPos) <= pointReachedDistance2)
186     {
187       // we reached the target position, let's move to the next waypoint
188       EnemyGetNextPosition(nextX, nextY, &nextX, &nextY);
189       target = (Vector2){nextX, nextY};
190       // track how many waypoints we passed
191       passedCount++;
192     }
193     
194     // acceleration towards the target
195     Vector2 unitDirection = Vector2Normalize(Vector2Subtract(target, lookForwardPos));
196     Vector2 acceleration = Vector2Scale(unitDirection, maxAcceleration * stepTime);
197     *velocity = Vector2Add(*velocity, acceleration);
198 
199     // limit the speed to the maximum speed
200     if (speed > maxSpeed)
201     {
202       *velocity = Vector2Scale(*velocity, maxSpeed / speed);
203     }
204 
205     // move the enemy
206     position = Vector2Add(position, Vector2Scale(*velocity, stepTime));
207   }
208 
209   if (waypointPassedCount)
210   {
211     (*waypointPassedCount) = passedCount;
212   }
213 
214   return position;
215 }
216 
217 void EnemyDraw()
218 {
219   rlDrawRenderBatchActive();
220   rlDisableDepthMask();
221   for (int i = 0; i < enemyCount; i++)
222   {
223     Enemy enemy = enemies[i];
224     if (enemy.enemyType == ENEMY_TYPE_NONE)
225     {
226       continue;
227     }
228 
229     Vector2 position = EnemyGetPosition(&enemy, gameTime.time - enemy.startMovingTime, &enemy.simVelocity, 0);
230     
231     // don't draw any trails for now; might replace this with footprints later
232     // if (enemy.movePathCount > 0)
233     // {
234     //   Vector3 p = {enemy.movePath[0].x, 0.2f, enemy.movePath[0].y};
235     //   DrawLine3D(p, (Vector3){position.x, 0.2f, position.y}, GREEN);
236     // }
237     // for (int j = 1; j < enemy.movePathCount; j++)
238     // {
239     //   Vector3 p = {enemy.movePath[j - 1].x, 0.2f, enemy.movePath[j - 1].y};
240     //   Vector3 q = {enemy.movePath[j].x, 0.2f, enemy.movePath[j].y};
241     //   DrawLine3D(p, q, GREEN);
242     // }
243 
244     float shieldHealth = enemyClassConfigs[enemy.enemyType].shieldHealth;
245     int phase = 0;
246     if (shieldHealth > 0 && enemy.shieldDamage < shieldHealth)
247     {
248       phase = SPRITE_UNIT_PHASE_WEAPON_SHIELD;
249     }
250 
251     switch (enemy.enemyType)
252     {
253     case ENEMY_TYPE_MINION:
254     case ENEMY_TYPE_RUNNER:
255     case ENEMY_TYPE_SHIELD:
256     case ENEMY_TYPE_BOSS:
257       DrawSpriteUnit(enemySprites[enemy.enemyType], (Vector3){position.x, 0.0f, position.y}, 
258         enemy.walkedDistance, 0, phase);
259       break;
260     }
261   }
262   rlDrawRenderBatchActive();
263   rlEnableDepthMask();
264 }
265 
266 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource)
267 {
268   // damage the tower
269   float explosionDamge = enemyClassConfigs[enemy->enemyType].explosionDamage;
270   float explosionRange = enemyClassConfigs[enemy->enemyType].explosionRange;
271   float explosionPushbackPower = enemyClassConfigs[enemy->enemyType].explosionPushbackPower;
272   float explosionRange2 = explosionRange * explosionRange;
273   tower->damage += enemyClassConfigs[enemy->enemyType].explosionDamage;
274   // explode the enemy
275   if (tower->damage >= TowerGetMaxHealth(tower))
276   {
277     tower->towerType = TOWER_TYPE_NONE;
278   }
279 
280   ParticleAdd(PARTICLE_TYPE_EXPLOSION, 
281     explosionSource, 
282     (Vector3){0, 0.1f, 0}, (Vector3){1.0f, 1.0f, 1.0f}, 1.0f);
283 
284   enemy->enemyType = ENEMY_TYPE_NONE;
285 
286   // push back enemies & dealing damage
287   for (int i = 0; i < enemyCount; i++)
288   {
289     Enemy *other = &enemies[i];
290     if (other->enemyType == ENEMY_TYPE_NONE)
291     {
292       continue;
293     }
294     float distanceSqr = Vector2DistanceSqr(enemy->simPosition, other->simPosition);
295     if (distanceSqr > 0 && distanceSqr < explosionRange2)
296     {
297       Vector2 direction = Vector2Normalize(Vector2Subtract(other->simPosition, enemy->simPosition));
298       other->simPosition = Vector2Add(other->simPosition, Vector2Scale(direction, explosionPushbackPower));
299       EnemyAddDamage(other, explosionDamge);
300     }
301   }
302 }
303 
304 void EnemyUpdate()
305 {
306   const float castleX = 0;
307   const float castleY = 0;
308   const float maxPathDistance2 = 0.25f * 0.25f;
309   
310   for (int i = 0; i < enemyCount; i++)
311   {
312     Enemy *enemy = &enemies[i];
313     if (enemy->enemyType == ENEMY_TYPE_NONE)
314     {
315       continue;
316     }
317 
318     int waypointPassedCount = 0;
319     Vector2 prevPosition = enemy->simPosition;
320     enemy->simPosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &enemy->simVelocity, &waypointPassedCount);
321     enemy->startMovingTime = gameTime.time;
322     enemy->walkedDistance += Vector2Distance(prevPosition, enemy->simPosition);
323     // track path of unit
324     if (enemy->movePathCount == 0 || Vector2DistanceSqr(enemy->simPosition, enemy->movePath[0]) > maxPathDistance2)
325     {
326       for (int j = ENEMY_MAX_PATH_COUNT - 1; j > 0; j--)
327       {
328         enemy->movePath[j] = enemy->movePath[j - 1];
329       }
330       enemy->movePath[0] = enemy->simPosition;
331       if (++enemy->movePathCount > ENEMY_MAX_PATH_COUNT)
332       {
333         enemy->movePathCount = ENEMY_MAX_PATH_COUNT;
334       }
335     }
336 
337     if (waypointPassedCount > 0)
338     {
339       enemy->currentX = enemy->nextX;
340       enemy->currentY = enemy->nextY;
341       if (EnemyGetNextPosition(enemy->currentX, enemy->currentY, &enemy->nextX, &enemy->nextY) &&
342         Vector2DistanceSqr(enemy->simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
343       {
344         // enemy reached the castle; remove it
345         enemy->enemyType = ENEMY_TYPE_NONE;
346         continue;
347       }
348     }
349   }
350 
351   // handle collisions between enemies
352   for (int i = 0; i < enemyCount - 1; i++)
353   {
354     Enemy *enemyA = &enemies[i];
355     if (enemyA->enemyType == ENEMY_TYPE_NONE)
356     {
357       continue;
358     }
359     for (int j = i + 1; j < enemyCount; j++)
360     {
361       Enemy *enemyB = &enemies[j];
362       if (enemyB->enemyType == ENEMY_TYPE_NONE)
363       {
364         continue;
365       }
366       float distanceSqr = Vector2DistanceSqr(enemyA->simPosition, enemyB->simPosition);
367       float radiusA = enemyClassConfigs[enemyA->enemyType].radius;
368       float radiusB = enemyClassConfigs[enemyB->enemyType].radius;
369       float radiusSum = radiusA + radiusB;
370       if (distanceSqr < radiusSum * radiusSum && distanceSqr > 0.001f)
371       {
372         // collision
373         float distance = sqrtf(distanceSqr);
374         float overlap = radiusSum - distance;
375         // move the enemies apart, but softly; if we have a clog of enemies,
376         // moving them perfectly apart can cause them to jitter
377         float positionCorrection = overlap / 5.0f;
378         Vector2 direction = (Vector2){
379             (enemyB->simPosition.x - enemyA->simPosition.x) / distance * positionCorrection,
380             (enemyB->simPosition.y - enemyA->simPosition.y) / distance * positionCorrection};
381         enemyA->simPosition = Vector2Subtract(enemyA->simPosition, direction);
382         enemyB->simPosition = Vector2Add(enemyB->simPosition, direction);
383       }
384     }
385   }
386 
387   // handle collisions between enemies and towers
388   for (int i = 0; i < enemyCount; i++)
389   {
390     Enemy *enemy = &enemies[i];
391     if (enemy->enemyType == ENEMY_TYPE_NONE)
392     {
393       continue;
394     }
395     enemy->contactTime -= gameTime.deltaTime;
396     if (enemy->contactTime < 0.0f)
397     {
398       enemy->contactTime = 0.0f;
399     }
400 
401     float enemyRadius = enemyClassConfigs[enemy->enemyType].radius;
402     // linear search over towers; could be optimized by using path finding tower map,
403     // but for now, we keep it simple
404     for (int j = 0; j < towerCount; j++)
405     {
406       Tower *tower = &towers[j];
407       if (tower->towerType == TOWER_TYPE_NONE)
408       {
409         continue;
410       }
411       float distanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){tower->x, tower->y});
412       float combinedRadius = enemyRadius + 0.708; // sqrt(0.5^2 + 0.5^2), corner-center distance of square with side length 1
413       if (distanceSqr > combinedRadius * combinedRadius)
414       {
415         continue;
416       }
417       // potential collision; square / circle intersection
418       float dx = tower->x - enemy->simPosition.x;
419       float dy = tower->y - enemy->simPosition.y;
420       float absDx = fabsf(dx);
421       float absDy = fabsf(dy);
422       Vector3 contactPoint = {0};
423       if (absDx <= 0.5f && absDx <= absDy) {
424         // vertical collision; push the enemy out horizontally
425         float overlap = enemyRadius + 0.5f - absDy;
426         if (overlap < 0.0f)
427         {
428           continue;
429         }
430         float direction = dy > 0.0f ? -1.0f : 1.0f;
431         enemy->simPosition.y += direction * overlap;
432         contactPoint = (Vector3){enemy->simPosition.x, 0.2f, tower->y + direction * 0.5f};
433       }
434       else if (absDy <= 0.5f && absDy <= absDx)
435       {
436         // horizontal collision; push the enemy out vertically
437         float overlap = enemyRadius + 0.5f - absDx;
438         if (overlap < 0.0f)
439         {
440           continue;
441         }
442         float direction = dx > 0.0f ? -1.0f : 1.0f;
443         enemy->simPosition.x += direction * overlap;
444         contactPoint = (Vector3){tower->x + direction * 0.5f, 0.2f, enemy->simPosition.y};
445       }
446       else
447       {
448         // possible collision with a corner
449         float cornerDX = dx > 0.0f ? -0.5f : 0.5f;
450         float cornerDY = dy > 0.0f ? -0.5f : 0.5f;
451         float cornerX = tower->x + cornerDX;
452         float cornerY = tower->y + cornerDY;
453         float cornerDistanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){cornerX, cornerY});
454         if (cornerDistanceSqr > enemyRadius * enemyRadius)
455         {
456           continue;
457         }
458         // push the enemy out along the diagonal
459         float cornerDistance = sqrtf(cornerDistanceSqr);
460         float overlap = enemyRadius - cornerDistance;
461         float directionX = cornerDistance > 0.0f ? (cornerX - enemy->simPosition.x) / cornerDistance : -cornerDX;
462         float directionY = cornerDistance > 0.0f ? (cornerY - enemy->simPosition.y) / cornerDistance : -cornerDY;
463         enemy->simPosition.x -= directionX * overlap;
464         enemy->simPosition.y -= directionY * overlap;
465         contactPoint = (Vector3){cornerX, 0.2f, cornerY};
466       }
467 
468       if (enemyClassConfigs[enemy->enemyType].explosionDamage > 0.0f)
469       {
470         enemy->contactTime += gameTime.deltaTime * 2.0f; // * 2 to undo the subtraction above
471         if (enemy->contactTime >= enemyClassConfigs[enemy->enemyType].requiredContactTime)
472         {
473           EnemyTriggerExplode(enemy, tower, contactPoint);
474         }
475       }
476     }
477   }
478 }
479 
480 EnemyId EnemyGetId(Enemy *enemy)
481 {
482   return (EnemyId){enemy - enemies, enemy->generation};
483 }
484 
485 Enemy *EnemyTryResolve(EnemyId enemyId)
486 {
487   if (enemyId.index >= ENEMY_MAX_COUNT)
488   {
489     return 0;
490   }
491   Enemy *enemy = &enemies[enemyId.index];
492   if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE)
493   {
494     return 0;
495   }
496   return enemy;
497 }
498 
499 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY)
500 {
501   Enemy *spawn = 0;
502   for (int i = 0; i < enemyCount; i++)
503   {
504     Enemy *enemy = &enemies[i];
505     if (enemy->enemyType == ENEMY_TYPE_NONE)
506     {
507       spawn = enemy;
508       break;
509     }
510   }
511 
512   if (enemyCount < ENEMY_MAX_COUNT && !spawn)
513   {
514     spawn = &enemies[enemyCount++];
515   }
516 
517   if (spawn)
518   {
519     *spawn = (Enemy){
520       .currentX = currentX,
521       .currentY = currentY,
522       .nextX = currentX,
523       .nextY = currentY,
524       .simPosition = (Vector2){currentX, currentY},
525       .simVelocity = (Vector2){0, 0},
526       .enemyType = enemyType,
527       .startMovingTime = gameTime.time,
528       .movePathCount = 0,
529       .walkedDistance = 0.0f,
530       .shieldDamage = 0.0f,
531       .damage = 0.0f,
532       .futureDamage = 0.0f,
533       .contactTime = 0.0f,
534       .generation = spawn->generation + 1,
535     };
536   }
537 
538   return spawn;
539 }
540 
541 int EnemyAddDamageRange(Vector2 position, float range, float damage)
542 {
543   int count = 0;
544   float range2 = range * range;
545   for (int i = 0; i < enemyCount; i++)
546   {
547     Enemy *enemy = &enemies[i];
548     if (enemy->enemyType == ENEMY_TYPE_NONE)
549     {
550       continue;
551     }
552     float distance2 = Vector2DistanceSqr(position, enemy->simPosition);
553     if (distance2 <= range2)
554     {
555       EnemyAddDamage(enemy, damage);
556       count++;
557     }
558   }
559   return count;
560 }
561 
562 int EnemyAddDamage(Enemy *enemy, float damage)
563 {
564   float shieldHealth = enemyClassConfigs[enemy->enemyType].shieldHealth;
565   if (shieldHealth > 0.0f && enemy->shieldDamage < shieldHealth)
566   {
567     float shieldDamageAbsorption = enemyClassConfigs[enemy->enemyType].shieldDamageAbsorption;
568     float shieldDamage = fminf(fminf(shieldDamageAbsorption, damage), shieldHealth - enemy->shieldDamage);
569     enemy->shieldDamage += shieldDamage;
570     damage -= shieldDamage;
571   }
572   enemy->damage += damage;
573   if (enemy->damage >= EnemyGetMaxHealth(enemy))
574   {
575     currentLevel->playerGold += enemyClassConfigs[enemy->enemyType].goldValue;
576     enemy->enemyType = ENEMY_TYPE_NONE;
577     return 1;
578   }
579 
580   return 0;
581 }
582 
583 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
584 {
585   int16_t castleX = 0;
586   int16_t castleY = 0;
587   Enemy* closest = 0;
588   int16_t closestDistance = 0;
589   float range2 = range * range;
590   for (int i = 0; i < enemyCount; i++)
591   {
592     Enemy* enemy = &enemies[i];
593     if (enemy->enemyType == ENEMY_TYPE_NONE)
594     {
595       continue;
596     }
597     float maxHealth = EnemyGetMaxHealth(enemy) + enemyClassConfigs[enemy->enemyType].shieldHealth;
598     if (enemy->futureDamage >= maxHealth)
599     {
600       // ignore enemies that will die soon
601       continue;
602     }
603     int16_t dx = castleX - enemy->currentX;
604     int16_t dy = castleY - enemy->currentY;
605     int16_t distance = abs(dx) + abs(dy);
606     if (!closest || distance < closestDistance)
607     {
608       float tdx = towerX - enemy->currentX;
609       float tdy = towerY - enemy->currentY;
610       float tdistance2 = tdx * tdx + tdy * tdy;
611       if (tdistance2 <= range2)
612       {
613         closest = enemy;
614         closestDistance = distance;
615       }
616     }
617   }
618   return closest;
619 }
620 
621 int EnemyCount()
622 {
623   int count = 0;
624   for (int i = 0; i < enemyCount; i++)
625   {
626     if (enemies[i].enemyType != ENEMY_TYPE_NONE)
627     {
628       count++;
629     }
630   }
631   return count;
632 }
633 
634 void EnemyDrawHealthbars(Camera3D camera)
635 {
636   for (int i = 0; i < enemyCount; i++)
637   {
638     Enemy *enemy = &enemies[i];
639     
640     float maxShieldHealth = enemyClassConfigs[enemy->enemyType].shieldHealth;
641     if (maxShieldHealth > 0.0f && enemy->shieldDamage < maxShieldHealth && enemy->shieldDamage > 0.0f)
642     {
643       float shieldHealth = maxShieldHealth - enemy->shieldDamage;
644       float shieldHealthRatio = shieldHealth / maxShieldHealth;
645       Vector3 position = (Vector3){enemy->simPosition.x, 0.5f, enemy->simPosition.y};
646       DrawHealthBar(camera, position, (Vector2) {.y = -4}, shieldHealthRatio, BLUE, 20.0f);
647     }
648 
649     if (enemy->enemyType == ENEMY_TYPE_NONE || enemy->damage == 0.0f)
650     {
651       continue;
652     }
653     Vector3 position = (Vector3){enemy->simPosition.x, 0.5f, enemy->simPosition.y};
654     float maxHealth = EnemyGetMaxHealth(enemy);
655     float health = maxHealth - enemy->damage;
656     float healthRatio = health / maxHealth;
657     
658     DrawHealthBar(camera, position, Vector2Zero(), healthRatio, GREEN, 15.0f);
659   }
660 }
  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

With the applied changes, we can now see the range of the tower when placing it, as well when hovering with the mouse over the tower or when opening the context menu.

The animation for the growing circles is once again a procedurally generated effect:

  1 void TowerDrawRange(Tower tower, float alpha)
  2 {
  3   Color ringColor = (Color){255, 200, 100, 255};
  4   const int rings = 4;
  5   const float radiusOffset = 0.5f;
  6   const float animationSpeed = 2.0f;
  7   float animation = 1.0f - fmodf(gameTime.time * animationSpeed, 1.0f);
  8   float radius = GetTowerRange(tower);
  9   // base circle
 10   DrawCircle3D((Vector3){tower.x, 0.01f, tower.y}, radius, (Vector3){1, 0, 0}, 90, 
 11     Fade(ringColor, alpha));
 12   
 13   for (int i = 1; i < rings; i++)
 14   {
 15     float t = ((float)i + animation) / (float)rings;
 16     float r = Lerp(radius, radius - radiusOffset, t * t);
 17     float a = 1.0f - ((float)i + animation - 1) / (float) (rings - 1);
 18     if (i == 1)
 19     {
 20       // fade out the outermost ring
 21       a = animation;
 22     }
 23     a *= alpha;
 24     
 25     DrawCircle3D((Vector3){tower.x, 0.01f, tower.y}, r, (Vector3){1, 0, 0}, 90, 
 26       Fade(ringColor, a));
 27   }
 28 }

The way this works is to draw a few rings (4) that grow at a slowing pace. The outermost ring stays at the same size and marks the boundary of the tower's range. The animation repeats via fmodf(gameTime.time * animationSpeed, 1.0f). Since I explained the approach to animating things via functions, I won't go into detail here.

Now we have to replace the logic in the tower_system.c file to handle the range upgrade.

  • 💾
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 #include <rlgl.h>
  4 #include <stdlib.h>
  5 #include <math.h>
  6 #include <string.h>
  7 
  8 //# Variables
  9 Font gameFontNormal = {0};
 10 GUIState guiState = {0};
 11 GameTime gameTime = {
 12   .fixedDeltaTime = 1.0f / 60.0f,
 13 };
 14 
 15 Model floorTileAModel = {0};
 16 Model floorTileBModel = {0};
 17 Model treeModel[2] = {0};
 18 Model firTreeModel[2] = {0};
 19 Model rockModels[5] = {0};
 20 Model grassPatchModel[1] = {0};
 21 
 22 Model pathArrowModel = {0};
 23 Model greenArrowModel = {0};
 24 
 25 Texture2D palette, spriteSheet;
 26 
 27 NPatchInfo uiPanelPatch = {
 28   .layout = NPATCH_NINE_PATCH,
 29   .source = {145, 1, 46, 46},
 30   .top = 18, .bottom = 18,
 31   .left = 16, .right = 16
 32 };
 33 NPatchInfo uiButtonNormal = {
 34   .layout = NPATCH_NINE_PATCH,
 35   .source = {193, 1, 32, 20},
 36   .top = 7, .bottom = 7,
 37   .left = 10, .right = 10
 38 };
 39 NPatchInfo uiButtonDisabled = {
 40   .layout = NPATCH_NINE_PATCH,
 41   .source = {193, 22, 32, 20},
 42   .top = 7, .bottom = 7,
 43   .left = 10, .right = 10
 44 };
 45 NPatchInfo uiButtonHovered = {
 46   .layout = NPATCH_NINE_PATCH,
 47   .source = {193, 43, 32, 20},
 48   .top = 7, .bottom = 7,
 49   .left = 10, .right = 10
 50 };
 51 NPatchInfo uiButtonPressed = {
 52   .layout = NPATCH_NINE_PATCH,
 53   .source = {193, 64, 32, 20},
 54   .top = 7, .bottom = 7,
 55   .left = 10, .right = 10
 56 };
 57 Rectangle uiDiamondMarker = {145, 48, 15, 15};
 58 
 59 Level levels[] = {
 60   [0] = {
 61     .state = LEVEL_STATE_BUILDING,
 62     .initialGold = 20,
 63     .waves[0] = {
 64       .enemyType = ENEMY_TYPE_SHIELD,
 65       .wave = 0,
 66       .count = 1,
 67       .interval = 2.5f,
 68       .delay = 1.0f,
 69       .spawnPosition = {2, 6},
 70     },
 71     .waves[1] = {
 72       .enemyType = ENEMY_TYPE_RUNNER,
 73       .wave = 0,
 74       .count = 5,
 75       .interval = 0.5f,
 76       .delay = 1.0f,
 77       .spawnPosition = {-2, 6},
 78     },
 79     .waves[2] = {
 80       .enemyType = ENEMY_TYPE_SHIELD,
 81       .wave = 1,
 82       .count = 20,
 83       .interval = 1.5f,
 84       .delay = 1.0f,
 85       .spawnPosition = {0, 6},
 86     },
 87     .waves[3] = {
 88       .enemyType = ENEMY_TYPE_MINION,
 89       .wave = 2,
 90       .count = 30,
 91       .interval = 1.2f,
 92       .delay = 1.0f,
 93       .spawnPosition = {2, 6},
 94     },
 95     .waves[4] = {
 96       .enemyType = ENEMY_TYPE_BOSS,
 97       .wave = 2,
 98       .count = 2,
 99       .interval = 5.0f,
100       .delay = 2.0f,
101       .spawnPosition = {-2, 4},
102     }
103   },
104 };
105 
106 Level *currentLevel = levels;
107 
108 void DrawBoxedText(const char *text, int x, int y, int width, int height, float alignX, float alignY, Color textColor);
109 
110 void DrawTitleText(const char *text, int anchorX, float alignX, Color color)
111 {
112   int textWidth = MeasureTextEx(gameFontNormal, text, gameFontNormal.baseSize, 0).x;
113   int panelWidth = textWidth + 40;
114   int posX = anchorX - panelWidth * alignX;
115   int textOffset = 20;
116   DrawTextureNPatch(spriteSheet, uiPanelPatch, (Rectangle){ posX, -10, panelWidth, 40}, Vector2Zero(), 0, WHITE);
117   DrawTextEx(gameFontNormal, text, (Vector2){posX + textOffset + 1, 6 + 1}, gameFontNormal.baseSize, 0, BLACK);
118   DrawTextEx(gameFontNormal, text, (Vector2){posX + textOffset, 6}, gameFontNormal.baseSize, 0, color);
119 }
120 
121 void DrawTitle(const char *text)
122 {
123   DrawTitleText(text, GetScreenWidth() / 2, 0.5f, WHITE);
124 }
125 
126 //# Game
127 
128 static Model LoadGLBModel(char *filename)
129 {
130   Model model = LoadModel(TextFormat("data/%s.glb",filename));
131   for (int i = 0; i < model.materialCount; i++)
132   {
133     model.materials[i].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
134   }
135   return model;
136 }
137 
138 void LoadAssets()
139 {
140   // load a sprite sheet that contains all units
141   spriteSheet = LoadTexture("data/spritesheet.png");
142   SetTextureFilter(spriteSheet, TEXTURE_FILTER_BILINEAR);
143 
144   // we'll use a palette texture to colorize the all buildings and environment art
145   palette = LoadTexture("data/palette.png");
146   // The texture uses gradients on very small space, so we'll enable bilinear filtering
147   SetTextureFilter(palette, TEXTURE_FILTER_BILINEAR);
148 
149   gameFontNormal = LoadFont("data/alagard.png");
150 
151   floorTileAModel = LoadGLBModel("floor-tile-a");
152   floorTileBModel = LoadGLBModel("floor-tile-b");
153   treeModel[0] = LoadGLBModel("leaftree-large-1-a");
154   treeModel[1] = LoadGLBModel("leaftree-large-1-b");
155   firTreeModel[0] = LoadGLBModel("firtree-1-a");
156   firTreeModel[1] = LoadGLBModel("firtree-1-b");
157   rockModels[0] = LoadGLBModel("rock-1");
158   rockModels[1] = LoadGLBModel("rock-2");
159   rockModels[2] = LoadGLBModel("rock-3");
160   rockModels[3] = LoadGLBModel("rock-4");
161   rockModels[4] = LoadGLBModel("rock-5");
162   grassPatchModel[0] = LoadGLBModel("grass-patch-1");
163 
164   pathArrowModel = LoadGLBModel("direction-arrow-x");
165   greenArrowModel = LoadGLBModel("green-arrow");
166 }
167 
168 void InitLevel(Level *level)
169 {
170   level->seed = (int)(GetTime() * 100.0f);
171 
172   TowerInit();
173   EnemyInit();
174   ProjectileInit();
175   ParticleInit();
176   TowerTryAdd(TOWER_TYPE_BASE, 0, 0);
177 
178   level->placementMode = 0;
179   level->state = LEVEL_STATE_BUILDING;
180   level->nextState = LEVEL_STATE_NONE;
181   level->playerGold = level->initialGold;
182   level->currentWave = 0;
183   level->placementX = -1;
184   level->placementY = 0;
185 
186   Camera *camera = &level->camera;
187   camera->position = (Vector3){4.0f, 8.0f, 8.0f};
188   camera->target = (Vector3){0.0f, 0.0f, 0.0f};
189   camera->up = (Vector3){0.0f, 1.0f, 0.0f};
190   camera->fovy = 11.5f;
191   camera->projection = CAMERA_ORTHOGRAPHIC;
192 }
193 
194 void DrawLevelHud(Level *level)
195 {
196   const char *text = TextFormat("Gold: %d", level->playerGold);
197   DrawTitleText(text, GetScreenWidth() - 10, 1.0f, YELLOW);
198 }
199 
200 void DrawLevelReportLostWave(Level *level)
201 {
202   BeginMode3D(level->camera);
203   DrawLevelGround(level);
204   TowerDraw();
205   EnemyDraw();
206   ProjectileDraw();
207   ParticleDraw();
208   guiState.isBlocked = 0;
209   EndMode3D();
210 
211   TowerDrawHealthBars(level->camera);
212 
213   DrawTitle("Wave lost");
214   
215   if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
216   {
217     level->nextState = LEVEL_STATE_RESET;
218   }
219 }
220 
221 int HasLevelNextWave(Level *level)
222 {
223   for (int i = 0; i < 10; i++)
224   {
225     EnemyWave *wave = &level->waves[i];
226     if (wave->wave == level->currentWave)
227     {
228       return 1;
229     }
230   }
231   return 0;
232 }
233 
234 void DrawLevelReportWonWave(Level *level)
235 {
236   BeginMode3D(level->camera);
237   DrawLevelGround(level);
238   TowerDraw();
239   EnemyDraw();
240   ProjectileDraw();
241   ParticleDraw();
242   guiState.isBlocked = 0;
243   EndMode3D();
244 
245   TowerDrawHealthBars(level->camera);
246 
247   DrawTitle("Wave won");
248 
249 
250   if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
251   {
252     level->nextState = LEVEL_STATE_RESET;
253   }
254 
255   if (HasLevelNextWave(level))
256   {
257     if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
258     {
259       level->nextState = LEVEL_STATE_BUILDING;
260     }
261   }
262   else {
263     if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
264     {
265       level->nextState = LEVEL_STATE_WON_LEVEL;
266     }
267   }
268 }
269 
270 int DrawBuildingBuildButton(Level *level, int x, int y, int width, int height, uint8_t towerType, const char *name)
271 {
272   static ButtonState buttonStates[8] = {0};
273   int cost = GetTowerCosts(towerType);
274   const char *text = TextFormat("%s: %d", name, cost);
275   buttonStates[towerType].isSelected = level->placementMode == towerType;
276   buttonStates[towerType].isDisabled = level->playerGold < cost;
277   if (Button(text, x, y, width, height, &buttonStates[towerType]))
278   {
279     level->placementMode = buttonStates[towerType].isSelected ? 0 : towerType;
280     level->nextState = LEVEL_STATE_BUILDING_PLACEMENT;
281     return 1;
282   }
283   return 0;
284 }
285 
286 float GetRandomFloat(float min, float max)
287 {
288   int random = GetRandomValue(0, 0xfffffff);
289   return ((float)random / (float)0xfffffff) * (max - min) + min;
290 }
291 
292 void DrawLevelGround(Level *level)
293 {
294   // draw checkerboard ground pattern
295   for (int x = -5; x <= 5; x += 1)
296   {
297     for (int y = -5; y <= 5; y += 1)
298     {
299       Model *model = (x + y) % 2 == 0 ? &floorTileAModel : &floorTileBModel;
300       DrawModel(*model, (Vector3){x, 0.0f, y}, 1.0f, WHITE);
301     }
302   }
303 
304   int oldSeed = GetRandomValue(0, 0xfffffff);
305   SetRandomSeed(level->seed);
306   // increase probability for trees via duplicated entries
307   Model borderModels[64];
308   int maxRockCount = GetRandomValue(2, 6);
309   int maxTreeCount = GetRandomValue(10, 20);
310   int maxFirTreeCount = GetRandomValue(5, 10);
311   int maxLeafTreeCount = maxTreeCount - maxFirTreeCount;
312   int grassPatchCount = GetRandomValue(5, 30);
313 
314   int modelCount = 0;
315   for (int i = 0; i < maxRockCount && modelCount < 63; i++)
316   {
317     borderModels[modelCount++] = rockModels[GetRandomValue(0, 5)];
318   }
319   for (int i = 0; i < maxLeafTreeCount && modelCount < 63; i++)
320   {
321     borderModels[modelCount++] = treeModel[GetRandomValue(0, 1)];
322   }
323   for (int i = 0; i < maxFirTreeCount && modelCount < 63; i++)
324   {
325     borderModels[modelCount++] = firTreeModel[GetRandomValue(0, 1)];
326   }
327   for (int i = 0; i < grassPatchCount && modelCount < 63; i++)
328   {
329     borderModels[modelCount++] = grassPatchModel[0];
330   }
331 
332   // draw some objects around the border of the map
333   Vector3 up = {0, 1, 0};
334   // a pseudo random number generator to get the same result every time
335   const float wiggle = 0.75f;
336   const int layerCount = 3;
337   for (int layer = 0; layer <= layerCount; layer++)
338   {
339     int layerPos = 6 + layer;
340     Model *selectedModels = borderModels;
341     int selectedModelCount = modelCount;
342     if (layer == 0)
343     {
344       selectedModels = grassPatchModel;
345       selectedModelCount = 1;
346     }
347     for (int x = -6 - layer; x <= 6 + layer; x += 1)
348     {
349       DrawModelEx(selectedModels[GetRandomValue(0, selectedModelCount - 1)], 
350         (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, -layerPos + GetRandomFloat(0.0f, wiggle)}, 
351         up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
352       DrawModelEx(selectedModels[GetRandomValue(0, selectedModelCount - 1)], 
353         (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, layerPos + GetRandomFloat(0.0f, wiggle)}, 
354         up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
355     }
356 
357     for (int z = -5 - layer; z <= 5 + layer; z += 1)
358     {
359       DrawModelEx(selectedModels[GetRandomValue(0, selectedModelCount - 1)], 
360         (Vector3){-layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)}, 
361         up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
362       DrawModelEx(selectedModels[GetRandomValue(0, selectedModelCount - 1)], 
363         (Vector3){layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)}, 
364         up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
365     }
366   }
367 
368   SetRandomSeed(oldSeed);
369 }
370 
371 void DrawEnemyPath(Level *level, Color arrowColor)
372 {
373   const int castleX = 0, castleY = 0;
374   const int maxWaypointCount = 200;
375   const float timeStep = 1.0f;
376   Vector3 arrowScale = {0.75f, 0.75f, 0.75f};
377 
378   // we start with a time offset to simulate the path, 
379   // this way the arrows are animated in a forward moving direction
380   // The time is wrapped around the time step to get a smooth animation
381   float timeOffset = fmodf(GetTime(), timeStep);
382 
383   for (int i = 0; i < ENEMY_MAX_WAVE_COUNT; i++)
384   {
385     EnemyWave *wave = &level->waves[i];
386     if (wave->wave != level->currentWave)
387     {
388       continue;
389     }
390 
391     // use this dummy enemy to simulate the path
392     Enemy dummy = {
393       .enemyType = ENEMY_TYPE_MINION,
394       .simPosition = (Vector2){wave->spawnPosition.x, wave->spawnPosition.y},
395       .nextX = wave->spawnPosition.x,
396       .nextY = wave->spawnPosition.y,
397       .currentX = wave->spawnPosition.x,
398       .currentY = wave->spawnPosition.y,
399     };
400 
401     float deltaTime = timeOffset;
402     for (int j = 0; j < maxWaypointCount; j++)
403     {
404       int waypointPassedCount = 0;
405       Vector2 pos = EnemyGetPosition(&dummy, deltaTime, &dummy.simVelocity, &waypointPassedCount);
406       // after the initial variable starting offset, we use a fixed time step
407       deltaTime = timeStep;
408       dummy.simPosition = pos;
409 
410       // Update the dummy's position just like we do in the regular enemy update loop
411       for (int k = 0; k < waypointPassedCount; k++)
412       {
413         dummy.currentX = dummy.nextX;
414         dummy.currentY = dummy.nextY;
415         if (EnemyGetNextPosition(dummy.currentX, dummy.currentY, &dummy.nextX, &dummy.nextY) &&
416           Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
417         {
418           break;
419         }
420       }
421       if (Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
422       {
423         break;
424       }
425       
426       // get the angle we need to rotate the arrow model. The velocity is just fine for this.
427       float angle = atan2f(dummy.simVelocity.x, dummy.simVelocity.y) * RAD2DEG - 90.0f;
428       DrawModelEx(pathArrowModel, (Vector3){pos.x, 0.15f, pos.y}, (Vector3){0, 1, 0}, angle, arrowScale, arrowColor);
429     }
430   }
431 }
432 
433 void DrawEnemyPaths(Level *level)
434 {
435   // disable depth testing for the path arrows
436   // flush the 3D batch to draw the arrows on top of everything
437   rlDrawRenderBatchActive();
438   rlDisableDepthTest();
439   DrawEnemyPath(level, (Color){64, 64, 64, 160});
440 
441   rlDrawRenderBatchActive();
442   rlEnableDepthTest();
443   DrawEnemyPath(level, WHITE);
444 }
445 
446 static void SimulateTowerPlacementBehavior(Level *level, float towerFloatHeight, float mapX, float mapY)
447 {
448   float dt = gameTime.fixedDeltaTime;
449   // smooth transition for the placement position using exponential decay
450   const float lambda = 15.0f;
451   float factor = 1.0f - expf(-lambda * dt);
452 
453   float damping = 0.5f;
454   float springStiffness = 300.0f;
455   float springDecay = 95.0f;
456   float minHeight = 0.35f;
457 
458   if (level->placementPhase == PLACEMENT_PHASE_STARTING)
459   {
460     damping = 1.0f;
461     springDecay = 90.0f;
462     springStiffness = 100.0f;
463     minHeight = 0.70f;
464   }
465 
466   for (int i = 0; i < gameTime.fixedStepCount; i++)
467   {
468     level->placementTransitionPosition = 
469       Vector2Lerp(
470         level->placementTransitionPosition, 
471         (Vector2){mapX, mapY}, factor);
472 
473     // draw the spring position for debugging the spring simulation
474     // first step: stiff spring, no simulation
475     Vector3 worldPlacementPosition = (Vector3){
476       level->placementTransitionPosition.x, towerFloatHeight, level->placementTransitionPosition.y};
477     Vector3 springTargetPosition = (Vector3){
478       worldPlacementPosition.x, 1.0f + towerFloatHeight, worldPlacementPosition.z};
479     // consider the current velocity to predict the future position in order to dampen
480     // the spring simulation. Longer prediction times will result in more damping
481     Vector3 predictedSpringPosition = Vector3Add(level->placementTowerSpring.position, 
482       Vector3Scale(level->placementTowerSpring.velocity, dt * damping));
483     Vector3 springPointDelta = Vector3Subtract(springTargetPosition, predictedSpringPosition);
484     Vector3 velocityChange = Vector3Scale(springPointDelta, dt * springStiffness);
485     // decay velocity of the upright forcing spring
486     // This force acts like a 2nd spring that pulls the tip upright into the air above the
487     // base position
488     level->placementTowerSpring.velocity = Vector3Scale(level->placementTowerSpring.velocity, 1.0f - expf(-springDecay * dt));
489     level->placementTowerSpring.velocity = Vector3Add(level->placementTowerSpring.velocity, velocityChange);
490 
491     // calculate length of the spring to calculate the force to apply to the placementTowerSpring position
492     // we use a simple spring model with a rest length of 1.0f
493     Vector3 springDelta = Vector3Subtract(worldPlacementPosition, level->placementTowerSpring.position);
494     float springLength = Vector3Length(springDelta);
495     float springForce = (springLength - 1.0f) * springStiffness;
496     Vector3 springForceVector = Vector3Normalize(springDelta);
497     springForceVector = Vector3Scale(springForceVector, springForce);
498     level->placementTowerSpring.velocity = Vector3Add(level->placementTowerSpring.velocity, 
499       Vector3Scale(springForceVector, dt));
500 
501     level->placementTowerSpring.position = Vector3Add(level->placementTowerSpring.position, 
502       Vector3Scale(level->placementTowerSpring.velocity, dt));
503     if (level->placementTowerSpring.position.y < minHeight + towerFloatHeight)
504     {
505       level->placementTowerSpring.velocity.y *= -1.0f;
506       level->placementTowerSpring.position.y = fmaxf(level->placementTowerSpring.position.y, minHeight + towerFloatHeight);
507     }
508   }
509 }
510 
511 void DrawLevelBuildingPlacementState(Level *level)
512 {
513   const float placementDuration = 0.5f;
514 
515   level->placementTimer += gameTime.deltaTime;
516   if (level->placementTimer > 1.0f && level->placementPhase == PLACEMENT_PHASE_STARTING)
517   {
518     level->placementPhase = PLACEMENT_PHASE_MOVING;
519     level->placementTimer = 0.0f;
520   }
521 
522   BeginMode3D(level->camera);
523   DrawLevelGround(level);
524 
525   int blockedCellCount = 0;
526   Vector2 blockedCells[1];
527   Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera);
528   float planeDistance = ray.position.y / -ray.direction.y;
529   float planeX = ray.direction.x * planeDistance + ray.position.x;
530   float planeY = ray.direction.z * planeDistance + ray.position.z;
531   int16_t mapX = (int16_t)floorf(planeX + 0.5f);
532   int16_t mapY = (int16_t)floorf(planeY + 0.5f);
533   if (level->placementPhase == PLACEMENT_PHASE_MOVING && 
534     level->placementMode && !guiState.isBlocked && 
535     mapX >= -5 && mapX <= 5 && mapY >= -5 && mapY <= 5 && IsMouseButtonDown(MOUSE_LEFT_BUTTON))
536   {
537     level->placementX = mapX;
538     level->placementY = mapY;
539   }
540   else
541   {
542     mapX = level->placementX;
543     mapY = level->placementY;
544   }
545   blockedCells[blockedCellCount++] = (Vector2){mapX, mapY};
546   PathFindingMapUpdate(blockedCellCount, blockedCells);
547 
548   TowerDraw();
549   EnemyDraw();
550   ProjectileDraw();
551   ParticleDraw();
552   DrawEnemyPaths(level);
553 
554   // let the tower float up and down. Consider this height in the spring simulation as well
555   float towerFloatHeight = sinf(gameTime.time * 4.0f) * 0.2f + 0.3f;
556 
557   if (level->placementPhase == PLACEMENT_PHASE_PLACING)
558   {
559     // The bouncing spring needs a bit of outro time to look nice and complete. 
560     // So we scale the time so that the first 2/3rd of the placing phase handles the motion
561     // and the last 1/3rd is the outro physics (bouncing)
562     float t = fminf(1.0f, level->placementTimer / placementDuration * 1.5f);
563     // let towerFloatHeight describe parabola curve, starting at towerFloatHeight and ending at 0
564     float linearBlendHeight = (1.0f - t) * towerFloatHeight;
565     float parabola = (1.0f - ((t - 0.5f) * (t - 0.5f) * 4.0f)) * 2.0f;
566     towerFloatHeight = linearBlendHeight + parabola;
567   }
568 
569   SimulateTowerPlacementBehavior(level, towerFloatHeight, mapX, mapY);
570   
571   rlPushMatrix();
572   rlTranslatef(level->placementTransitionPosition.x, 0, level->placementTransitionPosition.y);
573 
574   // calculate x and z rotation to align the model with the spring
575   Vector3 position = {level->placementTransitionPosition.x, towerFloatHeight, level->placementTransitionPosition.y};
576   Vector3 towerUp = Vector3Subtract(level->placementTowerSpring.position, position);
577   Vector3 rotationAxis = Vector3CrossProduct(towerUp, (Vector3){0, 1, 0});
578   float angle = acosf(Vector3DotProduct(towerUp, (Vector3){0, 1, 0}) / Vector3Length(towerUp)) * RAD2DEG;
579   float springLength = Vector3Length(towerUp);
580   float towerStretch = fminf(fmaxf(springLength, 0.5f), 4.5f);
581   float towerSquash = 1.0f / towerStretch;
582 
583   Tower dummy = {
584     .towerType = level->placementMode,
585   };
586   
587   float rangeAlpha = fminf(1.0f, level->placementTimer / placementDuration);
588   if (level->placementPhase == PLACEMENT_PHASE_PLACING)
589   {
590     rangeAlpha = 1.0f - rangeAlpha;
591   }
592   else if (level->placementPhase == PLACEMENT_PHASE_MOVING)
593   {
594     rangeAlpha = 1.0f;
595   }
596 
597   TowerDrawRange(dummy, rangeAlpha);
598   
599   rlPushMatrix();
600   rlTranslatef(0.0f, towerFloatHeight, 0.0f);
601   
602   rlRotatef(-angle, rotationAxis.x, rotationAxis.y, rotationAxis.z);
603   rlScalef(towerSquash, towerStretch, towerSquash);
604   TowerDrawSingle(dummy);
605   rlPopMatrix();
606 
607 
608   // draw a shadow for the tower
609   float umbrasize = 0.8 + sqrtf(towerFloatHeight);
610   DrawCube((Vector3){0.0f, 0.05f, 0.0f}, umbrasize, 0.0f, umbrasize, (Color){0, 0, 0, 32});
611   DrawCube((Vector3){0.0f, 0.075f, 0.0f}, 0.85f, 0.0f, 0.85f, (Color){0, 0, 0, 64});
612 
613 
614   float bounce = sinf(gameTime.time * 8.0f) * 0.5f + 0.5f;
615   float offset = fmaxf(0.0f, bounce - 0.4f) * 0.35f + 0.7f;
616   float squeeze = -fminf(0.0f, bounce - 0.4f) * 0.7f + 0.7f;
617   float stretch = fmaxf(0.0f, bounce - 0.3f) * 0.5f + 0.8f;
618   
619   DrawModelEx(greenArrowModel, (Vector3){ offset, 0.0f,  0.0f}, (Vector3){0, 1, 0}, 90, (Vector3){squeeze, 1.0f, stretch}, WHITE);
620   DrawModelEx(greenArrowModel, (Vector3){-offset, 0.0f,  0.0f}, (Vector3){0, 1, 0}, 270, (Vector3){squeeze, 1.0f, stretch}, WHITE);
621   DrawModelEx(greenArrowModel, (Vector3){ 0.0f, 0.0f,  offset}, (Vector3){0, 1, 0}, 0, (Vector3){squeeze, 1.0f, stretch}, WHITE);
622   DrawModelEx(greenArrowModel, (Vector3){ 0.0f, 0.0f, -offset}, (Vector3){0, 1, 0}, 180, (Vector3){squeeze, 1.0f, stretch}, WHITE);
623   rlPopMatrix();
624 
625   guiState.isBlocked = 0;
626 
627   EndMode3D();
628 
629   TowerDrawHealthBars(level->camera);
630 
631   if (level->placementPhase == PLACEMENT_PHASE_PLACING)
632   {
633     if (level->placementTimer > placementDuration)
634     {
635         Tower *tower = TowerTryAdd(level->placementMode, mapX, mapY);
636         // testing repairing
637         tower->damage = 2.5f;
638         level->playerGold -= GetTowerCosts(level->placementMode);
639         level->nextState = LEVEL_STATE_BUILDING;
640         level->placementMode = TOWER_TYPE_NONE;
641     }
642   }
643   else
644   {   
645     if (Button("Cancel", 20, GetScreenHeight() - 40, 160, 30, 0))
646     {
647       level->nextState = LEVEL_STATE_BUILDING;
648       level->placementMode = TOWER_TYPE_NONE;
649       TraceLog(LOG_INFO, "Cancel building");
650     }
651     
652     if (TowerGetAt(mapX, mapY) == 0 &&  Button("Build", GetScreenWidth() - 180, GetScreenHeight() - 40, 160, 30, 0))
653     {
654       level->placementPhase = PLACEMENT_PHASE_PLACING;
655       level->placementTimer = 0.0f;
656     }
657   }
658 }
659 
660 enum ContextMenuType
661 {
662   CONTEXT_MENU_TYPE_MAIN,
663   CONTEXT_MENU_TYPE_SELL_CONFIRM,
664   CONTEXT_MENU_TYPE_UPGRADE,
665 };
666 
667 enum UpgradeType
668 {
669   UPGRADE_TYPE_SPEED,
670   UPGRADE_TYPE_DAMAGE,
671   UPGRADE_TYPE_RANGE,
672 };
673 
674 typedef struct ContextMenuArgs
675 {
676   void *data;
677   uint8_t uint8;
678   int32_t int32;
679   Tower *tower;
680 } ContextMenuArgs;
681 
682 int OnContextMenuBuild(Level *level, ContextMenuArgs *data)
683 {
684   uint8_t towerType = data->uint8;
685   level->placementMode = towerType;
686   level->nextState = LEVEL_STATE_BUILDING_PLACEMENT;
687   return 1;
688 }
689 
690 int OnContextMenuSellConfirm(Level *level, ContextMenuArgs *data)
691 {
692   Tower *tower = data->tower;
693   int gold = data->int32;
694   level->playerGold += gold;
695   tower->towerType = TOWER_TYPE_NONE;
696   return 1;
697 }
698 
699 int OnContextMenuSellCancel(Level *level, ContextMenuArgs *data)
700 {
701   return 1;
702 }
703 
704 int OnContextMenuSell(Level *level, ContextMenuArgs *data)
705 {
706   level->placementContextMenuType = CONTEXT_MENU_TYPE_SELL_CONFIRM;
707   return 0;
708 }
709 
710 int OnContextMenuDoUpgrade(Level *level, ContextMenuArgs *data)
711 {
712   Tower *tower = data->tower;
713   switch (data->uint8)
714   {
715     case UPGRADE_TYPE_SPEED: tower->upgradeState.speed++; break;
716     case UPGRADE_TYPE_DAMAGE: tower->upgradeState.damage++; break;
717     case UPGRADE_TYPE_RANGE: tower->upgradeState.range++; break;
718   }
719   level->playerGold -= data->int32;
720   return 0;
721 }
722 
723 int OnContextMenuUpgrade(Level *level, ContextMenuArgs *data)
724 {
725   level->placementContextMenuType = CONTEXT_MENU_TYPE_UPGRADE;
726   return 0;
727 }
728 
729 int OnContextMenuRepair(Level *level, ContextMenuArgs *data)
730 {
731   Tower *tower = data->tower;
732   if (level->playerGold >= 1)
733   {
734     level->playerGold -= 1;
735     tower->damage = fmaxf(0.0f, tower->damage - 1.0f);
736   }
737   return tower->damage == 0.0f;
738 }
739 
740 typedef struct ContextMenuItem
741 {
742   uint8_t index;
743   char text[24];
744   float alignX;
745   int (*action)(Level*, ContextMenuArgs*);
746   void *data;
747   ContextMenuArgs args;
748   ButtonState buttonState;
749 } ContextMenuItem;
750 
751 ContextMenuItem ContextMenuItemText(uint8_t index, const char *text, float alignX)
752 {
753   ContextMenuItem item = {.index = index, .alignX = alignX};
754   strncpy(item.text, text, 24);
755   return item;
756 }
757 
758 ContextMenuItem ContextMenuItemButton(uint8_t index, const char *text, int (*action)(Level*, ContextMenuArgs*), ContextMenuArgs args)
759 {
760   ContextMenuItem item = {.index = index, .action = action, .args = args};
761   strncpy(item.text, text, 24);
762   return item;
763 }
764 
765 int DrawContextMenu(Level *level, Vector2 anchorLow, Vector2 anchorHigh, int width, ContextMenuItem *menus)
766 {
767   const int itemHeight = 28;
768   const int itemSpacing = 1;
769   const int padding = 8;
770   int itemCount = 0;
771   for (int i = 0; menus[i].text[0]; i++)
772   {
773     itemCount = itemCount > menus[i].index ? itemCount : menus[i].index;
774   }
775 
776   Rectangle contextMenu = {0, 0, width, 
777     (itemHeight + itemSpacing) * (itemCount + 1) + itemSpacing + padding * 2};
778   
779   Vector2 anchor = anchorHigh.y - 30 > contextMenu.height ? anchorHigh : anchorLow;
780   float anchorPivotY = anchorHigh.y - 30 > contextMenu.height ? 1.0f : 0.0f;
781   
782   contextMenu.x = anchor.x - contextMenu.width * 0.5f;
783   contextMenu.y = anchor.y - contextMenu.height * anchorPivotY;
784   contextMenu.x = fmaxf(0, fminf(GetScreenWidth() - contextMenu.width, contextMenu.x));
785   contextMenu.y = fmaxf(0, fminf(GetScreenHeight() - contextMenu.height, contextMenu.y));
786 
787   DrawTextureNPatch(spriteSheet, uiPanelPatch, contextMenu, Vector2Zero(), 0, WHITE);
788   DrawTextureRec(spriteSheet, uiDiamondMarker, (Vector2){anchor.x - uiDiamondMarker.width / 2, anchor.y - uiDiamondMarker.height / 2}, WHITE);
789   const int itemX = contextMenu.x + itemSpacing;
790   const int itemWidth = contextMenu.width - itemSpacing * 2;
791   #define ITEM_Y(idx) (contextMenu.y + itemSpacing + (itemHeight + itemSpacing) * idx + padding)
792   #define ITEM_RECT(idx, marginLR) itemX + (marginLR) + padding, ITEM_Y(idx), itemWidth - (marginLR) * 2 - padding * 2, itemHeight
793   int status = 0;
794   for (int i = 0; menus[i].text[0]; i++)
795   {
796     if (menus[i].action)
797     {
798       if (Button(menus[i].text, ITEM_RECT(menus[i].index, 0), &menus[i].buttonState))
799       {
800         status = menus[i].action(level, &menus[i].args);
801       }
802     }
803     else
804     {
805       DrawBoxedText(menus[i].text, ITEM_RECT(menus[i].index, 0), menus[i].alignX, 0.5f, WHITE);
806     }
807   }
808 
809   if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && !CheckCollisionPointRec(GetMousePosition(), contextMenu))
810   {
811     return 1;
812   }
813 
814   return status;
815 }
816 
817 void DrawLevelBuildingStateMainContextMenu(Level *level, Tower *tower, int32_t sellValue, float hp, float maxHitpoints, Vector2 anchorLow, Vector2 anchorHigh)
818 {
819   ContextMenuItem menu[12] = {0};
820   int menuCount = 0;
821   int menuIndex = 0;
822   if (tower)
823   {
824 
825     if (tower) {
826       menu[menuCount++] = ContextMenuItemText(menuIndex++, GetTowerName(tower->towerType), 0.5f);
827     }
828 
829     // two texts, same line
830     menu[menuCount++] = ContextMenuItemText(menuIndex, "HP:", 0.0f);
831     menu[menuCount++] = ContextMenuItemText(menuIndex++, TextFormat("%.1f / %.1f", hp, maxHitpoints), 1.0f);
832 
833     if (tower->towerType != TOWER_TYPE_BASE)
834     {
835       menu[menuCount++] = ContextMenuItemButton(menuIndex++, "Upgrade", OnContextMenuUpgrade, 
836         (ContextMenuArgs){.tower = tower});
837     }
838 
839     if (tower->towerType != TOWER_TYPE_BASE)
840     {
841       
842       menu[menuCount++] = ContextMenuItemButton(menuIndex++, "Sell", OnContextMenuSell, 
843         (ContextMenuArgs){.tower = tower, .int32 = sellValue});
844     }
845     if (tower->towerType != TOWER_TYPE_BASE && tower->damage > 0 && level->playerGold >= 1)
846     {
847       menu[menuCount++] = ContextMenuItemButton(menuIndex++, "Repair 1 (1G)", OnContextMenuRepair, 
848         (ContextMenuArgs){.tower = tower});
849     }
850   }
851   else
852   {
853     menu[menuCount] = ContextMenuItemButton(menuIndex++, 
854       TextFormat("Wall: %dG", GetTowerCosts(TOWER_TYPE_WALL)),
855       OnContextMenuBuild, (ContextMenuArgs){ .uint8 = (uint8_t) TOWER_TYPE_WALL});
856     menu[menuCount++].buttonState.isDisabled = level->playerGold < GetTowerCosts(TOWER_TYPE_WALL);
857 
858     menu[menuCount] = ContextMenuItemButton(menuIndex++, 
859       TextFormat("Archer: %dG", GetTowerCosts(TOWER_TYPE_ARCHER)),
860       OnContextMenuBuild, (ContextMenuArgs){ .uint8 = (uint8_t) TOWER_TYPE_ARCHER});
861     menu[menuCount++].buttonState.isDisabled = level->playerGold < GetTowerCosts(TOWER_TYPE_ARCHER);
862 
863     menu[menuCount] = ContextMenuItemButton(menuIndex++, 
864       TextFormat("Ballista: %dG", GetTowerCosts(TOWER_TYPE_BALLISTA)),
865       OnContextMenuBuild, (ContextMenuArgs){ .uint8 = (uint8_t) TOWER_TYPE_BALLISTA});
866     menu[menuCount++].buttonState.isDisabled = level->playerGold < GetTowerCosts(TOWER_TYPE_BALLISTA);
867 
868     menu[menuCount] = ContextMenuItemButton(menuIndex++, 
869       TextFormat("Catapult: %dG", GetTowerCosts(TOWER_TYPE_CATAPULT)),
870       OnContextMenuBuild, (ContextMenuArgs){ .uint8 = (uint8_t) TOWER_TYPE_CATAPULT});
871     menu[menuCount++].buttonState.isDisabled = level->playerGold < GetTowerCosts(TOWER_TYPE_CATAPULT);
872   }
873   
874   if (DrawContextMenu(level, anchorLow, anchorHigh, 150, menu))
875   {
876     level->placementContextMenuStatus = -1;
877   }
878 }
879 
880 void DrawLevelBuildingState(Level *level)
881 {
882   // when the context menu is not active, we update the placement position
883   if (level->placementContextMenuStatus == 0)
884   {
885     Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera);
886     float hitDistance = ray.position.y / -ray.direction.y;
887     float hitX = ray.direction.x * hitDistance + ray.position.x;
888     float hitY = ray.direction.z * hitDistance + ray.position.z;
889     level->placementX = (int)floorf(hitX + 0.5f);
890     level->placementY = (int)floorf(hitY + 0.5f);
891   }
892 
893   // the currently hovered/selected tower
894   Tower *tower = TowerGetAt(level->placementX, level->placementY);
895   // show the range of the tower when hovering/selecting it
896   TowerUpdateRangeFade(tower, 0.0f);
897 
898   BeginMode3D(level->camera);
899   DrawLevelGround(level);
900   PathFindingMapUpdate(0, 0);
901   TowerDraw();
902   EnemyDraw();
903   ProjectileDraw();
904   ParticleDraw();
905   DrawEnemyPaths(level);
906 
907   guiState.isBlocked = 0;
908 
909   // Hover rectangle, when the mouse is over the map
910   int isHovering = level->placementX >= -5 && level->placementX <= 5 && level->placementY >= -5 && level->placementY <= 5;
911   if (isHovering)
912   {
913     DrawCubeWires((Vector3){level->placementX, 0.75f, level->placementY}, 1.0f, 1.5f, 1.0f, GREEN);
914   }
915 
916   EndMode3D();
917 
918   TowerDrawHealthBars(level->camera);
919 
920   DrawTitle("Building phase");
921 
922   // Draw the context menu when the context menu is active
923   if (level->placementContextMenuStatus >= 1)
924   {
925     float maxHitpoints = 0.0f;
926     float hp = 0.0f;
927     float damageFactor = 0.0f;
928     int32_t sellValue = 0;
929 
930     if (tower)
931     {
932       maxHitpoints = TowerGetMaxHealth(tower);
933       hp = maxHitpoints - tower->damage;
934       damageFactor = 1.0f - tower->damage / maxHitpoints;
935       sellValue = (int32_t) ceilf(GetTowerCosts(tower->towerType) * 0.5f * damageFactor);
936     }
937 
938     ContextMenuItem menu[12] = {0};
939     int menuCount = 0;
940     int menuIndex = 0;
941     Vector2 anchorLow = GetWorldToScreen((Vector3){level->placementX, -0.25f, level->placementY}, level->camera);
942     Vector2 anchorHigh = GetWorldToScreen((Vector3){level->placementX, 2.0f, level->placementY}, level->camera);
943     
944     if (level->placementContextMenuType == CONTEXT_MENU_TYPE_MAIN)
945     {
946       DrawLevelBuildingStateMainContextMenu(level, tower, sellValue, hp, maxHitpoints, anchorLow, anchorHigh);
947     }
948     else if (level->placementContextMenuType == CONTEXT_MENU_TYPE_UPGRADE)
949     {
950       int totalLevel = tower->upgradeState.damage + tower->upgradeState.speed + tower->upgradeState.range;
951       int costs = totalLevel * 4;
952       int isMaxLevel = totalLevel >= TOWER_MAX_STAGE;
953       menu[menuCount++] = ContextMenuItemText(menuIndex++, TextFormat("%s tower, stage %d%s", 
954         GetTowerName(tower->towerType), totalLevel, isMaxLevel ? " (max)" : ""), 0.5f);
955       int buttonMenuIndex = menuIndex;
956       menu[menuCount++] = ContextMenuItemButton(menuIndex++, TextFormat("Speed: %d (%dG)", tower->upgradeState.speed, costs), 
957         OnContextMenuDoUpgrade, (ContextMenuArgs){.tower = tower, .uint8 = UPGRADE_TYPE_SPEED, .int32 = costs});
958       menu[menuCount++] = ContextMenuItemButton(menuIndex++, TextFormat("Damage: %d (%dG)", tower->upgradeState.damage, costs),
959         OnContextMenuDoUpgrade, (ContextMenuArgs){.tower = tower, .uint8 = UPGRADE_TYPE_DAMAGE, .int32 = costs});
960       menu[menuCount++] = ContextMenuItemButton(menuIndex++, TextFormat("Range: %d (%dG)", tower->upgradeState.range, costs),
961         OnContextMenuDoUpgrade, (ContextMenuArgs){.tower = tower, .uint8 = UPGRADE_TYPE_RANGE, .int32 = costs});
962 
963       // check if buttons should be disabled
964       if (isMaxLevel || level->playerGold < costs)
965       {
966         for (int i = buttonMenuIndex; i < menuCount; i++)
967         {
968           menu[i].buttonState.isDisabled = 1;
969         }
970       }
971 
972       if (DrawContextMenu(level, anchorLow, anchorHigh, 220, menu))
973       {
974         level->placementContextMenuType = CONTEXT_MENU_TYPE_MAIN;
975       }
976     }
977     else if (level->placementContextMenuType == CONTEXT_MENU_TYPE_SELL_CONFIRM)
978     {
979       menu[menuCount++] = ContextMenuItemText(menuIndex++, TextFormat("Sell %s?", GetTowerName(tower->towerType)), 0.5f);
980       menu[menuCount++] = ContextMenuItemButton(menuIndex++, "Yes", OnContextMenuSellConfirm, 
981         (ContextMenuArgs){.tower = tower, .int32 = sellValue});
982       menu[menuCount++] = ContextMenuItemButton(menuIndex++, "No", OnContextMenuSellCancel, (ContextMenuArgs){0});
983       Vector2 anchorLow = {GetScreenWidth() * 0.5f, GetScreenHeight() * 0.5f};
984       if (DrawContextMenu(level, anchorLow, anchorLow, 150, menu))
985       {
986         level->placementContextMenuStatus = -1;
987         level->placementContextMenuType = CONTEXT_MENU_TYPE_MAIN;
988       }
989     }
990   }
991   
992   // Activate the context menu when the mouse is clicked and the context menu is not active
993   else if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON) && isHovering && level->placementContextMenuStatus <= 0)
994   {
995     level->placementContextMenuStatus += 1;
996   }
997 
998   if (level->placementContextMenuStatus == 0)
999   {
1000     if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
1001     {
1002       level->nextState = LEVEL_STATE_RESET;
1003     }
1004     
1005     if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0))
1006     {
1007       level->nextState = LEVEL_STATE_BATTLE;
1008     }
1009 
1010   }  
1011 }
1012 
1013 void InitBattleStateConditions(Level *level)
1014 {
1015   level->state = LEVEL_STATE_BATTLE;
1016   level->nextState = LEVEL_STATE_NONE;
1017   level->waveEndTimer = 0.0f;
1018   for (int i = 0; i < 10; i++)
1019   {
1020     EnemyWave *wave = &level->waves[i];
1021     wave->spawned = 0;
1022     wave->timeToSpawnNext = wave->delay;
1023   }
1024 }
1025 
1026 void DrawLevelBattleState(Level *level)
1027 {
1028   BeginMode3D(level->camera);
1029   DrawLevelGround(level);
1030   TowerDraw();
1031   EnemyDraw();
1032   ProjectileDraw();
1033   ParticleDraw();
1034   guiState.isBlocked = 0;
1035   EndMode3D();
1036 
1037   EnemyDrawHealthbars(level->camera);
1038   TowerDrawHealthBars(level->camera);
1039 
1040   if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
1041   {
1042     level->nextState = LEVEL_STATE_RESET;
1043   }
1044 
1045   int maxCount = 0;
1046   int remainingCount = 0;
1047   for (int i = 0; i < 10; i++)
1048   {
1049     EnemyWave *wave = &level->waves[i];
1050     if (wave->wave != level->currentWave)
1051     {
1052       continue;
1053     }
1054     maxCount += wave->count;
1055     remainingCount += wave->count - wave->spawned;
1056   }
1057   int aliveCount = EnemyCount();
1058   remainingCount += aliveCount;
1059 
1060   const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount);
1061   DrawTitle(text);
1062 }
1063 
1064 void DrawLevel(Level *level)
1065 {
1066   switch (level->state)
1067   {
1068     case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break;
1069     case LEVEL_STATE_BUILDING_PLACEMENT: DrawLevelBuildingPlacementState(level); break;
1070     case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break;
1071     case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break;
1072     case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break;
1073     default: break;
1074   }
1075 
1076   DrawLevelHud(level);
1077 }
1078 
1079 void UpdateLevel(Level *level)
1080 {
1081   if (level->state == LEVEL_STATE_BATTLE)
1082   {
1083     int activeWaves = 0;
1084     for (int i = 0; i < 10; i++)
1085     {
1086       EnemyWave *wave = &level->waves[i];
1087       if (wave->spawned >= wave->count || wave->wave != level->currentWave)
1088       {
1089         continue;
1090       }
1091       activeWaves++;
1092       wave->timeToSpawnNext -= gameTime.deltaTime;
1093       if (wave->timeToSpawnNext <= 0.0f)
1094       {
1095         Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y);
1096         if (enemy)
1097         {
1098           wave->timeToSpawnNext = wave->interval;
1099           wave->spawned++;
1100         }
1101       }
1102     }
1103     if (GetTowerByType(TOWER_TYPE_BASE) == 0) {
1104       level->waveEndTimer += gameTime.deltaTime;
1105       if (level->waveEndTimer >= 2.0f)
1106       {
1107         level->nextState = LEVEL_STATE_LOST_WAVE;
1108       }
1109     }
1110     else if (activeWaves == 0 && EnemyCount() == 0)
1111     {
1112       level->waveEndTimer += gameTime.deltaTime;
1113       if (level->waveEndTimer >= 2.0f)
1114       {
1115         level->nextState = LEVEL_STATE_WON_WAVE;
1116       }
1117     }
1118   }
1119 
1120   PathFindingMapUpdate(0, 0);
1121   EnemyUpdate();
1122   TowerUpdate();
1123   ProjectileUpdate();
1124   ParticleUpdate();
1125 
1126   if (level->nextState == LEVEL_STATE_RESET)
1127   {
1128     InitLevel(level);
1129   }
1130   
1131   if (level->nextState == LEVEL_STATE_BATTLE)
1132   {
1133     InitBattleStateConditions(level);
1134   }
1135   
1136   if (level->nextState == LEVEL_STATE_WON_WAVE)
1137   {
1138     level->currentWave++;
1139     level->state = LEVEL_STATE_WON_WAVE;
1140   }
1141   
1142   if (level->nextState == LEVEL_STATE_LOST_WAVE)
1143   {
1144     level->state = LEVEL_STATE_LOST_WAVE;
1145   }
1146 
1147   if (level->nextState == LEVEL_STATE_BUILDING)
1148   {
1149     level->state = LEVEL_STATE_BUILDING;
1150     level->placementContextMenuStatus = 0;
1151   }
1152 
1153   if (level->nextState == LEVEL_STATE_BUILDING_PLACEMENT)
1154   {
1155     level->state = LEVEL_STATE_BUILDING_PLACEMENT;
1156     level->placementTransitionPosition = (Vector2){
1157       level->placementX, level->placementY};
1158     // initialize the spring to the current position
1159     level->placementTowerSpring = (PhysicsPoint){
1160       .position = (Vector3){level->placementX, 8.0f, level->placementY},
1161       .velocity = (Vector3){0.0f, 0.0f, 0.0f},
1162     };
1163     level->placementPhase = PLACEMENT_PHASE_STARTING;
1164     level->placementTimer = 0.0f;
1165   }
1166 
1167   if (level->nextState == LEVEL_STATE_WON_LEVEL)
1168   {
1169     // make something of this later
1170     InitLevel(level);
1171   }
1172 
1173   level->nextState = LEVEL_STATE_NONE;
1174 }
1175 
1176 float nextSpawnTime = 0.0f;
1177 
1178 void ResetGame()
1179 {
1180   InitLevel(currentLevel);
1181 }
1182 
1183 void InitGame()
1184 {
1185   TowerInit();
1186   EnemyInit();
1187   ProjectileInit();
1188   ParticleInit();
1189   PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f);
1190 
1191   currentLevel = levels;
1192   InitLevel(currentLevel);
1193 }
1194 
1195 //# Immediate GUI functions
1196 
1197 void DrawHealthBar(Camera3D camera, Vector3 position, Vector2 screenOffset, float healthRatio, Color barColor, float healthBarWidth)
1198 {
1199   const float healthBarHeight = 6.0f;
1200   const float healthBarOffset = 15.0f;
1201   const float inset = 2.0f;
1202   const float innerWidth = healthBarWidth - inset * 2;
1203   const float innerHeight = healthBarHeight - inset * 2;
1204 
1205   Vector2 screenPos = GetWorldToScreen(position, camera);
1206   screenPos = Vector2Add(screenPos, screenOffset);
1207   float centerX = screenPos.x - healthBarWidth * 0.5f;
1208   float topY = screenPos.y - healthBarOffset;
1209   DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK);
1210   float healthWidth = innerWidth * healthRatio;
1211   DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor);
1212 }
1213 
1214 void DrawBoxedText(const char *text, int x, int y, int width, int height, float alignX, float alignY, Color textColor)
1215 {
1216   Vector2 textSize = MeasureTextEx(gameFontNormal, text, gameFontNormal.baseSize, 1);
1217   
1218   DrawTextEx(gameFontNormal, text, (Vector2){
1219     x + (width - textSize.x) * alignX, 
1220     y + (height - textSize.y) * alignY
1221   }, gameFontNormal.baseSize, 1, textColor);
1222 }
1223 
1224 int Button(const char *text, int x, int y, int width, int height, ButtonState *state)
1225 {
1226   Rectangle bounds = {x, y, width, height};
1227   int isPressed = 0;
1228   int isSelected = state && state->isSelected;
1229   int isDisabled = state && state->isDisabled;
1230   if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !isDisabled)
1231   {
1232     if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
1233     {
1234       isPressed = 1;
1235     }
1236     guiState.isBlocked = 1;
1237     DrawTextureNPatch(spriteSheet, isPressed ? uiButtonPressed : uiButtonHovered,
1238       bounds, Vector2Zero(), 0, WHITE);
1239   }
1240   else
1241   {
1242     DrawTextureNPatch(spriteSheet, isSelected ? uiButtonHovered : (isDisabled ? uiButtonDisabled : uiButtonNormal),
1243       bounds, Vector2Zero(), 0, WHITE);
1244   }
1245   Vector2 textSize = MeasureTextEx(gameFontNormal, text, gameFontNormal.baseSize, 1);
1246   Color textColor = isDisabled ? LIGHTGRAY : BLACK;
1247   DrawTextEx(gameFontNormal, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, gameFontNormal.baseSize, 1, textColor);
1248   return isPressed;
1249 }
1250 
1251 //# Main game loop
1252 
1253 void GameUpdate()
1254 {
1255   UpdateLevel(currentLevel);
1256 }
1257 
1258 int main(void)
1259 {
1260   int screenWidth, screenHeight;
1261   GetPreferredSize(&screenWidth, &screenHeight);
1262   InitWindow(screenWidth, screenHeight, "Tower defense");
1263   float gamespeed = 1.0f;
1264   int frameRate = 30;
1265   SetTargetFPS(30);
1266 
1267   LoadAssets();
1268   InitGame();
1269 
1270   float pause = 1.0f;
1271 
1272   while (!WindowShouldClose())
1273   {
1274     if (IsPaused()) {
1275       // canvas is not visible in browser - do nothing
1276       continue;
1277     }
1278 
1279     if (IsKeyPressed(KEY_F))
1280     {
1281       frameRate = (frameRate + 5) % 30; 
1282       frameRate = frameRate < 10 ? 10 : frameRate;
1283       SetTargetFPS(frameRate);
1284     }
1285 
1286     if (IsKeyPressed(KEY_T))
1287     {
1288       gamespeed += 0.1f;
1289       if (gamespeed > 1.05f) gamespeed = 0.1f;
1290     }
1291 
1292     if (IsKeyPressed(KEY_P))
1293     {
1294       pause = pause > 0.5f ? 0.0f : 1.0f;
1295     }
1296 
1297     float dt = GetFrameTime() * gamespeed * pause;
1298     // cap maximum delta time to 0.1 seconds to prevent large time steps
1299     if (dt > 0.1f) dt = 0.1f;
1300     gameTime.time += dt;
1301     gameTime.deltaTime = dt;
1302     gameTime.frameCount += 1;
1303 
1304     float fixedTimeDiff = gameTime.time - gameTime.fixedTimeStart;
1305     gameTime.fixedStepCount = (uint8_t)(fixedTimeDiff / gameTime.fixedDeltaTime);
1306 
1307     BeginDrawing();
1308     ClearBackground((Color){0x4E, 0x63, 0x26, 0xFF});
1309 
1310     GameUpdate();
1311     DrawLevel(currentLevel);
1312 
1313     if (gamespeed != 1.0f)
1314       DrawText(TextFormat("Speed: %.1f", gamespeed), GetScreenWidth() - 180, 60, 20, WHITE);
1315     EndDrawing();
1316 
1317     gameTime.fixedTimeStart += gameTime.fixedStepCount * gameTime.fixedDeltaTime;
1318   }
1319 
1320   CloseWindow();
1321 
1322   return 0;
1323 }
  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 
 21 #define ENEMY_TYPE_MINION 1
 22 #define ENEMY_TYPE_RUNNER 2
 23 #define ENEMY_TYPE_SHIELD 3
 24 #define ENEMY_TYPE_BOSS 4
 25 
 26 #define PARTICLE_MAX_COUNT 400
 27 #define PARTICLE_TYPE_NONE 0
 28 #define PARTICLE_TYPE_EXPLOSION 1
 29 
 30 typedef struct Particle
 31 {
 32   uint8_t particleType;
 33   float spawnTime;
 34   float lifetime;
 35   Vector3 position;
 36   Vector3 velocity;
 37   Vector3 scale;
 38 } Particle;
 39 
 40 #define TOWER_MAX_COUNT 400
 41 enum TowerType
 42 {
 43   TOWER_TYPE_NONE,
 44   TOWER_TYPE_BASE,
 45   TOWER_TYPE_ARCHER,
 46   TOWER_TYPE_BALLISTA,
 47   TOWER_TYPE_CATAPULT,
 48   TOWER_TYPE_WALL,
 49   TOWER_TYPE_COUNT
 50 };
 51 
 52 typedef struct HitEffectConfig
 53 {
 54   float damage;
 55   float areaDamageRadius;
 56   float pushbackPowerDistance;
 57 } HitEffectConfig;
 58 
 59 typedef struct TowerTypeConfig
 60 {
 61   const char *name;
 62   float cooldown;
 63   float range;
64 float maxUpgradeRange;
65 float projectileSpeed; 66 67 uint8_t cost; 68 uint8_t projectileType; 69 uint16_t maxHealth; 70 71 HitEffectConfig hitEffect; 72 } TowerTypeConfig; 73 74 #define TOWER_MAX_STAGE 10 75 76 typedef struct TowerUpgradeState 77 { 78 uint8_t range; 79 uint8_t damage; 80 uint8_t speed; 81 } TowerUpgradeState; 82 83 typedef struct Tower 84 { 85 int16_t x, y; 86 uint8_t towerType; 87 TowerUpgradeState upgradeState; 88 Vector2 lastTargetPosition; 89 float cooldown; 90 float damage; 91 // alpha value for the range circle drawing 92 float drawRangeAlpha; 93 } Tower; 94 95 typedef struct GameTime 96 { 97 float time; 98 float deltaTime; 99 uint32_t frameCount; 100 101 float fixedDeltaTime; 102 // leaving the fixed time stepping to the update functions, 103 // we need to know the fixed time at the start of the frame 104 float fixedTimeStart; 105 // and the number of fixed steps that we have to make this frame 106 // The fixedTime is fixedTimeStart + n * fixedStepCount 107 uint8_t fixedStepCount; 108 } GameTime; 109 110 typedef struct ButtonState { 111 char isSelected; 112 char isDisabled; 113 } ButtonState; 114 115 typedef struct GUIState { 116 int isBlocked; 117 } GUIState; 118 119 typedef enum LevelState 120 { 121 LEVEL_STATE_NONE, 122 LEVEL_STATE_BUILDING, 123 LEVEL_STATE_BUILDING_PLACEMENT, 124 LEVEL_STATE_BATTLE, 125 LEVEL_STATE_WON_WAVE, 126 LEVEL_STATE_LOST_WAVE, 127 LEVEL_STATE_WON_LEVEL, 128 LEVEL_STATE_RESET, 129 } LevelState; 130 131 typedef struct EnemyWave { 132 uint8_t enemyType; 133 uint8_t wave; 134 uint16_t count; 135 float interval; 136 float delay; 137 Vector2 spawnPosition; 138 139 uint16_t spawned; 140 float timeToSpawnNext; 141 } EnemyWave; 142 143 #define ENEMY_MAX_WAVE_COUNT 10 144 145 typedef enum PlacementPhase 146 { 147 PLACEMENT_PHASE_STARTING, 148 PLACEMENT_PHASE_MOVING, 149 PLACEMENT_PHASE_PLACING, 150 } PlacementPhase; 151 152 typedef struct Level 153 { 154 int seed; 155 LevelState state; 156 LevelState nextState; 157 Camera3D camera; 158 int placementMode; 159 PlacementPhase placementPhase; 160 float placementTimer; 161 162 int16_t placementX; 163 int16_t placementY; 164 int8_t placementContextMenuStatus; 165 int8_t placementContextMenuType; 166 167 Vector2 placementTransitionPosition; 168 PhysicsPoint placementTowerSpring; 169 170 int initialGold; 171 int playerGold; 172 173 EnemyWave waves[ENEMY_MAX_WAVE_COUNT]; 174 int currentWave; 175 float waveEndTimer; 176 } Level; 177 178 typedef struct DeltaSrc 179 { 180 char x, y; 181 } DeltaSrc; 182 183 typedef struct PathfindingMap 184 { 185 int width, height; 186 float scale; 187 float *distances; 188 long *towerIndex; 189 DeltaSrc *deltaSrc; 190 float maxDistance; 191 Matrix toMapSpace; 192 Matrix toWorldSpace; 193 } PathfindingMap; 194 195 // when we execute the pathfinding algorithm, we need to store the active nodes 196 // in a queue. Each node has a position, a distance from the start, and the 197 // position of the node that we came from. 198 typedef struct PathfindingNode 199 { 200 int16_t x, y, fromX, fromY; 201 float distance; 202 } PathfindingNode; 203 204 typedef struct EnemyId 205 { 206 uint16_t index; 207 uint16_t generation; 208 } EnemyId; 209 210 typedef struct EnemyClassConfig 211 { 212 float speed; 213 float health; 214 float shieldHealth; 215 float shieldDamageAbsorption; 216 float radius; 217 float maxAcceleration; 218 float requiredContactTime; 219 float explosionDamage; 220 float explosionRange; 221 float explosionPushbackPower; 222 int goldValue; 223 } EnemyClassConfig; 224 225 typedef struct Enemy 226 { 227 int16_t currentX, currentY; 228 int16_t nextX, nextY; 229 Vector2 simPosition; 230 Vector2 simVelocity; 231 uint16_t generation; 232 float walkedDistance; 233 float startMovingTime; 234 float damage, futureDamage; 235 float shieldDamage; 236 float contactTime; 237 uint8_t enemyType; 238 uint8_t movePathCount; 239 Vector2 movePath[ENEMY_MAX_PATH_COUNT]; 240 } Enemy; 241 242 // a unit that uses sprites to be drawn 243 #define SPRITE_UNIT_ANIMATION_COUNT 6 244 #define SPRITE_UNIT_PHASE_WEAPON_IDLE 1 245 #define SPRITE_UNIT_PHASE_WEAPON_COOLDOWN 2 246 #define SPRITE_UNIT_PHASE_WEAPON_SHIELD 3 247 248 typedef struct SpriteAnimation 249 { 250 Rectangle srcRect; 251 Vector2 offset; 252 uint8_t animationId; 253 uint8_t frameCount; 254 uint8_t frameWidth; 255 float frameDuration; 256 } SpriteAnimation; 257 258 typedef struct SpriteUnit 259 { 260 float scale; 261 SpriteAnimation animations[SPRITE_UNIT_ANIMATION_COUNT]; 262 } SpriteUnit; 263 264 #define PROJECTILE_MAX_COUNT 1200 265 #define PROJECTILE_TYPE_NONE 0 266 #define PROJECTILE_TYPE_ARROW 1 267 #define PROJECTILE_TYPE_CATAPULT 2 268 #define PROJECTILE_TYPE_BALLISTA 3 269 270 typedef struct Projectile 271 { 272 uint8_t projectileType; 273 float shootTime; 274 float arrivalTime; 275 float distance; 276 Vector3 position; 277 Vector3 target; 278 Vector3 directionNormal; 279 EnemyId targetEnemy; 280 HitEffectConfig hitEffectConfig; 281 } Projectile; 282 283 //# Function declarations 284 int Button(const char *text, int x, int y, int width, int height, ButtonState *state); 285 int EnemyAddDamageRange(Vector2 position, float range, float damage); 286 int EnemyAddDamage(Enemy *enemy, float damage); 287 288 //# Enemy functions 289 void EnemyInit(); 290 void EnemyDraw(); 291 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource); 292 void EnemyUpdate(); 293 float EnemyGetCurrentMaxSpeed(Enemy *enemy); 294 float EnemyGetMaxHealth(Enemy *enemy); 295 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY); 296 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount); 297 EnemyId EnemyGetId(Enemy *enemy); 298 Enemy *EnemyTryResolve(EnemyId enemyId); 299 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY); 300 int EnemyAddDamage(Enemy *enemy, float damage); 301 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range); 302 int EnemyCount(); 303 void EnemyDrawHealthbars(Camera3D camera); 304 305 //# Tower functions 306 void TowerInit(); 307 float TowerGetMaxHealth(Tower *tower); 308 Tower *GetTowerByIndex(int index); 309 Tower *TowerGetAt(int16_t x, int16_t y); 310 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y); 311 Tower *GetTowerByType(uint8_t towerType); 312 int GetTowerCosts(uint8_t towerType);
313 const char *GetTowerName(uint8_t towerType); 314 float GetTowerRange(Tower tower);
315 float TowerGetMaxHealth(Tower *tower); 316 void TowerDraw(); 317 void TowerDrawSingle(Tower tower); 318 void TowerDrawRange(Tower tower, float alpha); 319 void TowerUpdate(); 320 void TowerUpdateRangeFade(Tower *selectedTower, float fadeoutTarget); 321 void TowerDrawHealthBars(Camera3D camera); 322 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase); 323 324 //# Particles 325 void ParticleInit(); 326 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime); 327 void ParticleUpdate(); 328 void ParticleDraw(); 329 330 //# Projectiles 331 void ProjectileInit(); 332 void ProjectileDraw(); 333 void ProjectileUpdate(); 334 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig); 335 336 //# Pathfinding map 337 void PathfindingMapInit(int width, int height, Vector3 translate, float scale); 338 float PathFindingGetDistance(int mapX, int mapY); 339 Vector2 PathFindingGetGradient(Vector3 world); 340 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY); 341 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells); 342 void PathFindingMapDraw(); 343 344 //# UI 345 void DrawHealthBar(Camera3D camera, Vector3 position, Vector2 screenOffset, float healthRatio, Color barColor, float healthBarWidth); 346 347 //# Level 348 void DrawLevelGround(Level *level); 349 void DrawEnemyPath(Level *level, Color arrowColor); 350 351 //# variables 352 extern Level *currentLevel; 353 extern Enemy enemies[ENEMY_MAX_COUNT]; 354 extern int enemyCount; 355 extern EnemyClassConfig enemyClassConfigs[]; 356 357 extern GUIState guiState; 358 extern GameTime gameTime; 359 extern Tower towers[TOWER_MAX_COUNT]; 360 extern int towerCount; 361 362 extern Texture2D palette, spriteSheet; 363 364 #endif
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 
  4 static TowerTypeConfig towerTypeConfigs[TOWER_TYPE_COUNT] = {
  5     [TOWER_TYPE_BASE] = {
  6         .name = "Castle",
  7         .maxHealth = 10,
  8     },
  9     [TOWER_TYPE_ARCHER] = {
 10         .name = "Archer",
 11         .cooldown = 0.5f,
 12         .range = 3.0f,
13 .maxUpgradeRange = 5.0f,
14 .cost = 6, 15 .maxHealth = 10, 16 .projectileSpeed = 4.0f, 17 .projectileType = PROJECTILE_TYPE_ARROW, 18 .hitEffect = { 19 .damage = 3.0f, 20 } 21 }, 22 [TOWER_TYPE_BALLISTA] = { 23 .name = "Ballista", 24 .cooldown = 1.5f,
25 .range = 6.0f, 26 .maxUpgradeRange = 8.0f,
27 .cost = 9, 28 .maxHealth = 10, 29 .projectileSpeed = 10.0f, 30 .projectileType = PROJECTILE_TYPE_BALLISTA, 31 .hitEffect = { 32 .damage = 8.0f, 33 .pushbackPowerDistance = 0.25f, 34 } 35 }, 36 [TOWER_TYPE_CATAPULT] = { 37 .name = "Catapult", 38 .cooldown = 1.7f,
39 .range = 5.0f, 40 .maxUpgradeRange = 7.0f,
41 .cost = 10, 42 .maxHealth = 10, 43 .projectileSpeed = 3.0f, 44 .projectileType = PROJECTILE_TYPE_CATAPULT, 45 .hitEffect = { 46 .damage = 2.0f, 47 .areaDamageRadius = 1.75f, 48 } 49 }, 50 [TOWER_TYPE_WALL] = { 51 .name = "Wall", 52 .cost = 2, 53 .maxHealth = 10, 54 }, 55 }; 56 57 Tower towers[TOWER_MAX_COUNT]; 58 int towerCount = 0; 59 60 Model towerModels[TOWER_TYPE_COUNT]; 61 62 // definition of our archer unit 63 SpriteUnit archerUnit = { 64 .animations[0] = { 65 .srcRect = {0, 0, 16, 16}, 66 .offset = {7, 1}, 67 .frameCount = 1, 68 .frameDuration = 0.0f, 69 }, 70 .animations[1] = { 71 .animationId = SPRITE_UNIT_PHASE_WEAPON_COOLDOWN, 72 .srcRect = {16, 0, 6, 16}, 73 .offset = {8, 0}, 74 }, 75 .animations[2] = { 76 .animationId = SPRITE_UNIT_PHASE_WEAPON_IDLE, 77 .srcRect = {22, 0, 11, 16}, 78 .offset = {10, 0}, 79 }, 80 }; 81 82 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase) 83 { 84 float unitScale = unit.scale == 0 ? 1.0f : unit.scale; 85 float xScale = flip ? -1.0f : 1.0f; 86 Camera3D camera = currentLevel->camera; 87 float size = 0.5f * unitScale; 88 // we want the sprite to face the camera, so we need to calculate the up vector 89 Vector3 forward = Vector3Subtract(camera.target, camera.position); 90 Vector3 up = {0, 1, 0}; 91 Vector3 right = Vector3CrossProduct(forward, up); 92 up = Vector3Normalize(Vector3CrossProduct(right, forward)); 93 94 for (int i=0;i<SPRITE_UNIT_ANIMATION_COUNT;i++) 95 { 96 SpriteAnimation anim = unit.animations[i]; 97 if (anim.animationId != phase && anim.animationId != 0) 98 { 99 continue; 100 } 101 Rectangle srcRect = anim.srcRect; 102 if (anim.frameCount > 1) 103 { 104 int w = anim.frameWidth > 0 ? anim.frameWidth : srcRect.width; 105 srcRect.x += (int)(t / anim.frameDuration) % anim.frameCount * w; 106 } 107 Vector2 offset = { anim.offset.x / 16.0f * size, anim.offset.y / 16.0f * size * xScale }; 108 Vector2 scale = { srcRect.width / 16.0f * size, srcRect.height / 16.0f * size }; 109 110 if (flip) 111 { 112 srcRect.x += srcRect.width; 113 srcRect.width = -srcRect.width; 114 offset.x = scale.x - offset.x; 115 } 116 DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE); 117 // move the sprite slightly towards the camera to avoid z-fighting 118 position = Vector3Add(position, Vector3Scale(Vector3Normalize(forward), -0.01f)); 119 } 120 } 121 122 void TowerInit() 123 { 124 for (int i = 0; i < TOWER_MAX_COUNT; i++) 125 { 126 towers[i] = (Tower){0}; 127 } 128 towerCount = 0; 129 130 towerModels[TOWER_TYPE_BASE] = LoadModel("data/keep.glb"); 131 towerModels[TOWER_TYPE_WALL] = LoadModel("data/wall-0000.glb"); 132 133 for (int i = 0; i < TOWER_TYPE_COUNT; i++) 134 { 135 if (towerModels[i].materials) 136 { 137 // assign the palette texture to the material of the model (0 is not used afaik) 138 towerModels[i].materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette; 139 } 140 } 141 } 142 143 static void TowerGunUpdate(Tower *tower) 144 { 145 TowerTypeConfig config = towerTypeConfigs[tower->towerType]; 146 if (tower->cooldown <= 0.0f) 147 {
148 Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, GetTowerRange(*tower));
149 if (enemy) 150 { 151 tower->cooldown = config.cooldown; 152 // shoot the enemy; determine future position of the enemy 153 float bulletSpeed = config.projectileSpeed; 154 Vector2 velocity = enemy->simVelocity; 155 Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0); 156 Vector2 towerPosition = {tower->x, tower->y}; 157 float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed; 158 for (int i = 0; i < 8; i++) { 159 velocity = enemy->simVelocity; 160 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0); 161 float distance = Vector2Distance(towerPosition, futurePosition); 162 float eta2 = distance / bulletSpeed; 163 if (fabs(eta - eta2) < 0.01f) { 164 break; 165 } 166 eta = (eta2 + eta) * 0.5f; 167 } 168 169 ProjectileTryAdd(config.projectileType, enemy, 170 (Vector3){towerPosition.x, 1.33f, towerPosition.y}, 171 (Vector3){futurePosition.x, 0.25f, futurePosition.y}, 172 bulletSpeed, config.hitEffect); 173 enemy->futureDamage += config.hitEffect.damage; 174 tower->lastTargetPosition = futurePosition; 175 } 176 } 177 else 178 { 179 tower->cooldown -= gameTime.deltaTime; 180 } 181 } 182 183 Tower *TowerGetAt(int16_t x, int16_t y) 184 { 185 for (int i = 0; i < towerCount; i++) 186 { 187 if (towers[i].x == x && towers[i].y == y && towers[i].towerType != TOWER_TYPE_NONE) 188 { 189 return &towers[i]; 190 } 191 } 192 return 0; 193 } 194 195 Tower *GetTowerByIndex(int index) 196 { 197 if (index < 0 || index >= towerCount) 198 { 199 return 0; 200 } 201 return &towers[index]; 202 } 203 204 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y) 205 { 206 if (towerCount >= TOWER_MAX_COUNT) 207 { 208 return 0; 209 } 210 211 Tower *tower = TowerGetAt(x, y); 212 if (tower) 213 { 214 return 0; 215 } 216 217 tower = &towers[towerCount++]; 218 *tower = (Tower){ 219 .x = x, 220 .y = y, 221 .towerType = towerType, 222 .cooldown = 0.0f, 223 .damage = 0.0f, 224 }; 225 return tower; 226 } 227 228 Tower *GetTowerByType(uint8_t towerType) 229 { 230 for (int i = 0; i < towerCount; i++) 231 { 232 if (towers[i].towerType == towerType) 233 { 234 return &towers[i]; 235 } 236 } 237 return 0; 238 } 239 240 const char *GetTowerName(uint8_t towerType) 241 { 242 return towerTypeConfigs[towerType].name; 243 } 244 245 int GetTowerCosts(uint8_t towerType) 246 { 247 return towerTypeConfigs[towerType].cost; 248 } 249 250 float TowerGetMaxHealth(Tower *tower) 251 { 252 return towerTypeConfigs[tower->towerType].maxHealth; 253 } 254 255 float GetTowerRange(Tower tower) 256 {
257 float range = towerTypeConfigs[tower.towerType].range; 258 float maxUpgradeRange = towerTypeConfigs[tower.towerType].maxUpgradeRange; 259 if (tower.upgradeState.range > 0) 260 { 261 range = Lerp(range, maxUpgradeRange, tower.upgradeState.range / (float)TOWER_MAX_STAGE); 262 } 263 return range;
264 } 265 266 void TowerUpdateRangeFade(Tower *selectedTower, float fadeoutTarget) 267 { 268 // animate fade in and fade out of range drawing using framerate independent lerp 269 float fadeLerp = 1.0f - powf(0.005f, gameTime.deltaTime); 270 for (int i = 0; i < TOWER_MAX_COUNT; i++) 271 { 272 Tower *fadingTower = GetTowerByIndex(i); 273 if (!fadingTower) 274 { 275 break; 276 } 277 float drawRangeTarget = fadingTower == selectedTower ? 1.0f : fadeoutTarget; 278 fadingTower->drawRangeAlpha = Lerp(fadingTower->drawRangeAlpha, drawRangeTarget, fadeLerp); 279 } 280 } 281 282 void TowerDrawRange(Tower tower, float alpha) 283 { 284 Color ringColor = (Color){255, 200, 100, 255}; 285 const int rings = 4; 286 const float radiusOffset = 0.5f; 287 const float animationSpeed = 2.0f; 288 float animation = 1.0f - fmodf(gameTime.time * animationSpeed, 1.0f); 289 float radius = GetTowerRange(tower); 290 // base circle 291 DrawCircle3D((Vector3){tower.x, 0.01f, tower.y}, radius, (Vector3){1, 0, 0}, 90, 292 Fade(ringColor, alpha)); 293 294 for (int i = 1; i < rings; i++) 295 { 296 float t = ((float)i + animation) / (float)rings; 297 float r = Lerp(radius, radius - radiusOffset, t * t); 298 float a = 1.0f - ((float)i + animation - 1) / (float) (rings - 1); 299 if (i == 1) 300 { 301 // fade out the outermost ring 302 a = animation; 303 } 304 a *= alpha; 305 306 DrawCircle3D((Vector3){tower.x, 0.01f, tower.y}, r, (Vector3){1, 0, 0}, 90, 307 Fade(ringColor, a)); 308 } 309 } 310 311 void TowerDrawSingle(Tower tower) 312 { 313 if (tower.towerType == TOWER_TYPE_NONE) 314 { 315 return; 316 } 317 318 if (tower.drawRangeAlpha > 2.0f/256.0f) 319 { 320 TowerDrawRange(tower, tower.drawRangeAlpha); 321 } 322 323 switch (tower.towerType) 324 { 325 case TOWER_TYPE_ARCHER: 326 { 327 Vector2 screenPosTower = GetWorldToScreen((Vector3){tower.x, 0.0f, tower.y}, currentLevel->camera); 328 Vector2 screenPosTarget = GetWorldToScreen((Vector3){tower.lastTargetPosition.x, 0.0f, tower.lastTargetPosition.y}, currentLevel->camera); 329 DrawModel(towerModels[TOWER_TYPE_WALL], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE); 330 DrawSpriteUnit(archerUnit, (Vector3){tower.x, 1.0f, tower.y}, 0, screenPosTarget.x > screenPosTower.x, 331 tower.cooldown > 0.2f ? SPRITE_UNIT_PHASE_WEAPON_COOLDOWN : SPRITE_UNIT_PHASE_WEAPON_IDLE); 332 } 333 break; 334 case TOWER_TYPE_BALLISTA: 335 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, BROWN); 336 break; 337 case TOWER_TYPE_CATAPULT: 338 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, DARKGRAY); 339 break; 340 default: 341 if (towerModels[tower.towerType].materials) 342 { 343 DrawModel(towerModels[tower.towerType], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE); 344 } else { 345 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY); 346 } 347 break; 348 } 349 } 350 351 void TowerDraw() 352 { 353 for (int i = 0; i < towerCount; i++) 354 { 355 TowerDrawSingle(towers[i]); 356 } 357 } 358 359 void TowerUpdate() 360 { 361 for (int i = 0; i < towerCount; i++) 362 { 363 Tower *tower = &towers[i]; 364 switch (tower->towerType) 365 { 366 case TOWER_TYPE_CATAPULT: 367 case TOWER_TYPE_BALLISTA: 368 case TOWER_TYPE_ARCHER: 369 TowerGunUpdate(tower); 370 break; 371 } 372 } 373 } 374 375 void TowerDrawHealthBars(Camera3D camera) 376 { 377 for (int i = 0; i < towerCount; i++) 378 { 379 Tower *tower = &towers[i]; 380 if (tower->towerType == TOWER_TYPE_NONE || tower->damage <= 0.0f) 381 { 382 continue; 383 } 384 385 Vector3 position = (Vector3){tower->x, 0.5f, tower->y}; 386 float maxHealth = TowerGetMaxHealth(tower); 387 float health = maxHealth - tower->damage; 388 float healthRatio = health / maxHealth; 389 390 DrawHealthBar(camera, position, Vector2Zero(), healthRatio, GREEN, 35.0f); 391 } 392 }
  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 #include <stdlib.h>
  4 #include <math.h>
  5 #include <rlgl.h>
  6 
  7 EnemyClassConfig enemyClassConfigs[] = {
  8     [ENEMY_TYPE_MINION] = {
  9       .health = 10.0f, 
 10       .speed = 0.6f, 
 11       .radius = 0.25f, 
 12       .maxAcceleration = 1.0f,
 13       .explosionDamage = 1.0f,
 14       .requiredContactTime = 0.5f,
 15       .explosionRange = 1.0f,
 16       .explosionPushbackPower = 0.25f,
 17       .goldValue = 1,
 18     },
 19     [ENEMY_TYPE_RUNNER] = {
 20       .health = 5.0f, 
 21       .speed = 1.0f, 
 22       .radius = 0.25f, 
 23       .maxAcceleration = 2.0f,
 24       .explosionDamage = 1.0f,
 25       .requiredContactTime = 0.5f,
 26       .explosionRange = 1.0f,
 27       .explosionPushbackPower = 0.25f,
 28       .goldValue = 2,
 29     },
 30     [ENEMY_TYPE_SHIELD] = {
 31       .health = 8.0f, 
 32       .speed = 0.5f, 
 33       .radius = 0.25f, 
 34       .maxAcceleration = 1.0f,
 35       .explosionDamage = 2.0f,
 36       .requiredContactTime = 0.5f,
 37       .explosionRange = 1.0f,
 38       .explosionPushbackPower = 0.25f,
 39       .goldValue = 3,
 40       .shieldDamageAbsorption = 4.0f,
 41       .shieldHealth = 25.0f,
 42     },
 43     [ENEMY_TYPE_BOSS] = {
 44       .health = 50.0f, 
 45       .speed = 0.4f, 
 46       .radius = 0.25f, 
 47       .maxAcceleration = 1.0f,
 48       .explosionDamage = 5.0f,
 49       .requiredContactTime = 0.5f,
 50       .explosionRange = 1.0f,
 51       .explosionPushbackPower = 0.25f,
 52       .goldValue = 10,
 53     },
 54 };
 55 
 56 Enemy enemies[ENEMY_MAX_COUNT];
 57 int enemyCount = 0;
 58 
 59 SpriteUnit enemySprites[] = {
 60     [ENEMY_TYPE_MINION] = {
 61       .animations[0] = {
 62         .srcRect = {0, 17, 16, 15},
 63         .offset = {8.0f, 0.0f},
 64         .frameCount = 6,
 65         .frameDuration = 0.1f,
 66       },
 67       .animations[1] = {
 68         .srcRect = {1, 33, 15, 14},
 69         .offset = {7.0f, 0.0f},
 70         .frameCount = 6,
 71         .frameWidth = 16,
 72         .frameDuration = 0.1f,
 73       },
 74     },
 75     [ENEMY_TYPE_RUNNER] = {
 76       .scale = 0.75f,
 77       .animations[0] = {
 78         .srcRect = {0, 17, 16, 15},
 79         .offset = {8.0f, 0.0f},
 80         .frameCount = 6,
 81         .frameDuration = 0.1f,
 82       },
 83     },
 84     [ENEMY_TYPE_SHIELD] = {
 85       .animations[0] = {
 86         .srcRect = {0, 17, 16, 15},
 87         .offset = {8.0f, 0.0f},
 88         .frameCount = 6,
 89         .frameDuration = 0.1f,
 90       },
 91       .animations[1] = {
 92         .animationId = SPRITE_UNIT_PHASE_WEAPON_SHIELD,
 93         .srcRect = {99, 17, 10, 11},
 94         .offset = {7.0f, 0.0f},
 95       },
 96     },
 97     [ENEMY_TYPE_BOSS] = {
 98       .scale = 1.5f,
 99       .animations[0] = {
100         .srcRect = {0, 17, 16, 15},
101         .offset = {8.0f, 0.0f},
102         .frameCount = 6,
103         .frameDuration = 0.1f,
104       },
105       .animations[1] = {
106         .srcRect = {97, 29, 14, 7},
107         .offset = {7.0f, -9.0f},
108       },
109     },
110 };
111 
112 void EnemyInit()
113 {
114   for (int i = 0; i < ENEMY_MAX_COUNT; i++)
115   {
116     enemies[i] = (Enemy){0};
117   }
118   enemyCount = 0;
119 }
120 
121 float EnemyGetCurrentMaxSpeed(Enemy *enemy)
122 {
123   return enemyClassConfigs[enemy->enemyType].speed;
124 }
125 
126 float EnemyGetMaxHealth(Enemy *enemy)
127 {
128   return enemyClassConfigs[enemy->enemyType].health;
129 }
130 
131 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY)
132 {
133   int16_t castleX = 0;
134   int16_t castleY = 0;
135   int16_t dx = castleX - currentX;
136   int16_t dy = castleY - currentY;
137   if (dx == 0 && dy == 0)
138   {
139     *nextX = currentX;
140     *nextY = currentY;
141     return 1;
142   }
143   Vector2 gradient = PathFindingGetGradient((Vector3){currentX, 0, currentY});
144 
145   if (gradient.x == 0 && gradient.y == 0)
146   {
147     *nextX = currentX;
148     *nextY = currentY;
149     return 1;
150   }
151 
152   if (fabsf(gradient.x) > fabsf(gradient.y))
153   {
154     *nextX = currentX + (int16_t)(gradient.x > 0.0f ? 1 : -1);
155     *nextY = currentY;
156     return 0;
157   }
158   *nextX = currentX;
159   *nextY = currentY + (int16_t)(gradient.y > 0.0f ? 1 : -1);
160   return 0;
161 }
162 
163 
164 // this function predicts the movement of the unit for the next deltaT seconds
165 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount)
166 {
167   const float pointReachedDistance = 0.25f;
168   const float pointReachedDistance2 = pointReachedDistance * pointReachedDistance;
169   const float maxSimStepTime = 0.015625f;
170   
171   float maxAcceleration = enemyClassConfigs[enemy->enemyType].maxAcceleration;
172   float maxSpeed = EnemyGetCurrentMaxSpeed(enemy);
173   int16_t nextX = enemy->nextX;
174   int16_t nextY = enemy->nextY;
175   Vector2 position = enemy->simPosition;
176   int passedCount = 0;
177   for (float t = 0.0f; t < deltaT; t += maxSimStepTime)
178   {
179     float stepTime = fminf(deltaT - t, maxSimStepTime);
180     Vector2 target = (Vector2){nextX, nextY};
181     float speed = Vector2Length(*velocity);
182     // draw the target position for debugging
183     //DrawCubeWires((Vector3){target.x, 0.2f, target.y}, 0.1f, 0.4f, 0.1f, RED);
184     Vector2 lookForwardPos = Vector2Add(position, Vector2Scale(*velocity, speed));
185     if (Vector2DistanceSqr(target, lookForwardPos) <= pointReachedDistance2)
186     {
187       // we reached the target position, let's move to the next waypoint
188       EnemyGetNextPosition(nextX, nextY, &nextX, &nextY);
189       target = (Vector2){nextX, nextY};
190       // track how many waypoints we passed
191       passedCount++;
192     }
193     
194     // acceleration towards the target
195     Vector2 unitDirection = Vector2Normalize(Vector2Subtract(target, lookForwardPos));
196     Vector2 acceleration = Vector2Scale(unitDirection, maxAcceleration * stepTime);
197     *velocity = Vector2Add(*velocity, acceleration);
198 
199     // limit the speed to the maximum speed
200     if (speed > maxSpeed)
201     {
202       *velocity = Vector2Scale(*velocity, maxSpeed / speed);
203     }
204 
205     // move the enemy
206     position = Vector2Add(position, Vector2Scale(*velocity, stepTime));
207   }
208 
209   if (waypointPassedCount)
210   {
211     (*waypointPassedCount) = passedCount;
212   }
213 
214   return position;
215 }
216 
217 void EnemyDraw()
218 {
219   rlDrawRenderBatchActive();
220   rlDisableDepthMask();
221   for (int i = 0; i < enemyCount; i++)
222   {
223     Enemy enemy = enemies[i];
224     if (enemy.enemyType == ENEMY_TYPE_NONE)
225     {
226       continue;
227     }
228 
229     Vector2 position = EnemyGetPosition(&enemy, gameTime.time - enemy.startMovingTime, &enemy.simVelocity, 0);
230     
231     // don't draw any trails for now; might replace this with footprints later
232     // if (enemy.movePathCount > 0)
233     // {
234     //   Vector3 p = {enemy.movePath[0].x, 0.2f, enemy.movePath[0].y};
235     //   DrawLine3D(p, (Vector3){position.x, 0.2f, position.y}, GREEN);
236     // }
237     // for (int j = 1; j < enemy.movePathCount; j++)
238     // {
239     //   Vector3 p = {enemy.movePath[j - 1].x, 0.2f, enemy.movePath[j - 1].y};
240     //   Vector3 q = {enemy.movePath[j].x, 0.2f, enemy.movePath[j].y};
241     //   DrawLine3D(p, q, GREEN);
242     // }
243 
244     float shieldHealth = enemyClassConfigs[enemy.enemyType].shieldHealth;
245     int phase = 0;
246     if (shieldHealth > 0 && enemy.shieldDamage < shieldHealth)
247     {
248       phase = SPRITE_UNIT_PHASE_WEAPON_SHIELD;
249     }
250 
251     switch (enemy.enemyType)
252     {
253     case ENEMY_TYPE_MINION:
254     case ENEMY_TYPE_RUNNER:
255     case ENEMY_TYPE_SHIELD:
256     case ENEMY_TYPE_BOSS:
257       DrawSpriteUnit(enemySprites[enemy.enemyType], (Vector3){position.x, 0.0f, position.y}, 
258         enemy.walkedDistance, 0, phase);
259       break;
260     }
261   }
262   rlDrawRenderBatchActive();
263   rlEnableDepthMask();
264 }
265 
266 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource)
267 {
268   // damage the tower
269   float explosionDamge = enemyClassConfigs[enemy->enemyType].explosionDamage;
270   float explosionRange = enemyClassConfigs[enemy->enemyType].explosionRange;
271   float explosionPushbackPower = enemyClassConfigs[enemy->enemyType].explosionPushbackPower;
272   float explosionRange2 = explosionRange * explosionRange;
273   tower->damage += enemyClassConfigs[enemy->enemyType].explosionDamage;
274   // explode the enemy
275   if (tower->damage >= TowerGetMaxHealth(tower))
276   {
277     tower->towerType = TOWER_TYPE_NONE;
278   }
279 
280   ParticleAdd(PARTICLE_TYPE_EXPLOSION, 
281     explosionSource, 
282     (Vector3){0, 0.1f, 0}, (Vector3){1.0f, 1.0f, 1.0f}, 1.0f);
283 
284   enemy->enemyType = ENEMY_TYPE_NONE;
285 
286   // push back enemies & dealing damage
287   for (int i = 0; i < enemyCount; i++)
288   {
289     Enemy *other = &enemies[i];
290     if (other->enemyType == ENEMY_TYPE_NONE)
291     {
292       continue;
293     }
294     float distanceSqr = Vector2DistanceSqr(enemy->simPosition, other->simPosition);
295     if (distanceSqr > 0 && distanceSqr < explosionRange2)
296     {
297       Vector2 direction = Vector2Normalize(Vector2Subtract(other->simPosition, enemy->simPosition));
298       other->simPosition = Vector2Add(other->simPosition, Vector2Scale(direction, explosionPushbackPower));
299       EnemyAddDamage(other, explosionDamge);
300     }
301   }
302 }
303 
304 void EnemyUpdate()
305 {
306   const float castleX = 0;
307   const float castleY = 0;
308   const float maxPathDistance2 = 0.25f * 0.25f;
309   
310   for (int i = 0; i < enemyCount; i++)
311   {
312     Enemy *enemy = &enemies[i];
313     if (enemy->enemyType == ENEMY_TYPE_NONE)
314     {
315       continue;
316     }
317 
318     int waypointPassedCount = 0;
319     Vector2 prevPosition = enemy->simPosition;
320     enemy->simPosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &enemy->simVelocity, &waypointPassedCount);
321     enemy->startMovingTime = gameTime.time;
322     enemy->walkedDistance += Vector2Distance(prevPosition, enemy->simPosition);
323     // track path of unit
324     if (enemy->movePathCount == 0 || Vector2DistanceSqr(enemy->simPosition, enemy->movePath[0]) > maxPathDistance2)
325     {
326       for (int j = ENEMY_MAX_PATH_COUNT - 1; j > 0; j--)
327       {
328         enemy->movePath[j] = enemy->movePath[j - 1];
329       }
330       enemy->movePath[0] = enemy->simPosition;
331       if (++enemy->movePathCount > ENEMY_MAX_PATH_COUNT)
332       {
333         enemy->movePathCount = ENEMY_MAX_PATH_COUNT;
334       }
335     }
336 
337     if (waypointPassedCount > 0)
338     {
339       enemy->currentX = enemy->nextX;
340       enemy->currentY = enemy->nextY;
341       if (EnemyGetNextPosition(enemy->currentX, enemy->currentY, &enemy->nextX, &enemy->nextY) &&
342         Vector2DistanceSqr(enemy->simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
343       {
344         // enemy reached the castle; remove it
345         enemy->enemyType = ENEMY_TYPE_NONE;
346         continue;
347       }
348     }
349   }
350 
351   // handle collisions between enemies
352   for (int i = 0; i < enemyCount - 1; i++)
353   {
354     Enemy *enemyA = &enemies[i];
355     if (enemyA->enemyType == ENEMY_TYPE_NONE)
356     {
357       continue;
358     }
359     for (int j = i + 1; j < enemyCount; j++)
360     {
361       Enemy *enemyB = &enemies[j];
362       if (enemyB->enemyType == ENEMY_TYPE_NONE)
363       {
364         continue;
365       }
366       float distanceSqr = Vector2DistanceSqr(enemyA->simPosition, enemyB->simPosition);
367       float radiusA = enemyClassConfigs[enemyA->enemyType].radius;
368       float radiusB = enemyClassConfigs[enemyB->enemyType].radius;
369       float radiusSum = radiusA + radiusB;
370       if (distanceSqr < radiusSum * radiusSum && distanceSqr > 0.001f)
371       {
372         // collision
373         float distance = sqrtf(distanceSqr);
374         float overlap = radiusSum - distance;
375         // move the enemies apart, but softly; if we have a clog of enemies,
376         // moving them perfectly apart can cause them to jitter
377         float positionCorrection = overlap / 5.0f;
378         Vector2 direction = (Vector2){
379             (enemyB->simPosition.x - enemyA->simPosition.x) / distance * positionCorrection,
380             (enemyB->simPosition.y - enemyA->simPosition.y) / distance * positionCorrection};
381         enemyA->simPosition = Vector2Subtract(enemyA->simPosition, direction);
382         enemyB->simPosition = Vector2Add(enemyB->simPosition, direction);
383       }
384     }
385   }
386 
387   // handle collisions between enemies and towers
388   for (int i = 0; i < enemyCount; i++)
389   {
390     Enemy *enemy = &enemies[i];
391     if (enemy->enemyType == ENEMY_TYPE_NONE)
392     {
393       continue;
394     }
395     enemy->contactTime -= gameTime.deltaTime;
396     if (enemy->contactTime < 0.0f)
397     {
398       enemy->contactTime = 0.0f;
399     }
400 
401     float enemyRadius = enemyClassConfigs[enemy->enemyType].radius;
402     // linear search over towers; could be optimized by using path finding tower map,
403     // but for now, we keep it simple
404     for (int j = 0; j < towerCount; j++)
405     {
406       Tower *tower = &towers[j];
407       if (tower->towerType == TOWER_TYPE_NONE)
408       {
409         continue;
410       }
411       float distanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){tower->x, tower->y});
412       float combinedRadius = enemyRadius + 0.708; // sqrt(0.5^2 + 0.5^2), corner-center distance of square with side length 1
413       if (distanceSqr > combinedRadius * combinedRadius)
414       {
415         continue;
416       }
417       // potential collision; square / circle intersection
418       float dx = tower->x - enemy->simPosition.x;
419       float dy = tower->y - enemy->simPosition.y;
420       float absDx = fabsf(dx);
421       float absDy = fabsf(dy);
422       Vector3 contactPoint = {0};
423       if (absDx <= 0.5f && absDx <= absDy) {
424         // vertical collision; push the enemy out horizontally
425         float overlap = enemyRadius + 0.5f - absDy;
426         if (overlap < 0.0f)
427         {
428           continue;
429         }
430         float direction = dy > 0.0f ? -1.0f : 1.0f;
431         enemy->simPosition.y += direction * overlap;
432         contactPoint = (Vector3){enemy->simPosition.x, 0.2f, tower->y + direction * 0.5f};
433       }
434       else if (absDy <= 0.5f && absDy <= absDx)
435       {
436         // horizontal collision; push the enemy out vertically
437         float overlap = enemyRadius + 0.5f - absDx;
438         if (overlap < 0.0f)
439         {
440           continue;
441         }
442         float direction = dx > 0.0f ? -1.0f : 1.0f;
443         enemy->simPosition.x += direction * overlap;
444         contactPoint = (Vector3){tower->x + direction * 0.5f, 0.2f, enemy->simPosition.y};
445       }
446       else
447       {
448         // possible collision with a corner
449         float cornerDX = dx > 0.0f ? -0.5f : 0.5f;
450         float cornerDY = dy > 0.0f ? -0.5f : 0.5f;
451         float cornerX = tower->x + cornerDX;
452         float cornerY = tower->y + cornerDY;
453         float cornerDistanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){cornerX, cornerY});
454         if (cornerDistanceSqr > enemyRadius * enemyRadius)
455         {
456           continue;
457         }
458         // push the enemy out along the diagonal
459         float cornerDistance = sqrtf(cornerDistanceSqr);
460         float overlap = enemyRadius - cornerDistance;
461         float directionX = cornerDistance > 0.0f ? (cornerX - enemy->simPosition.x) / cornerDistance : -cornerDX;
462         float directionY = cornerDistance > 0.0f ? (cornerY - enemy->simPosition.y) / cornerDistance : -cornerDY;
463         enemy->simPosition.x -= directionX * overlap;
464         enemy->simPosition.y -= directionY * overlap;
465         contactPoint = (Vector3){cornerX, 0.2f, cornerY};
466       }
467 
468       if (enemyClassConfigs[enemy->enemyType].explosionDamage > 0.0f)
469       {
470         enemy->contactTime += gameTime.deltaTime * 2.0f; // * 2 to undo the subtraction above
471         if (enemy->contactTime >= enemyClassConfigs[enemy->enemyType].requiredContactTime)
472         {
473           EnemyTriggerExplode(enemy, tower, contactPoint);
474         }
475       }
476     }
477   }
478 }
479 
480 EnemyId EnemyGetId(Enemy *enemy)
481 {
482   return (EnemyId){enemy - enemies, enemy->generation};
483 }
484 
485 Enemy *EnemyTryResolve(EnemyId enemyId)
486 {
487   if (enemyId.index >= ENEMY_MAX_COUNT)
488   {
489     return 0;
490   }
491   Enemy *enemy = &enemies[enemyId.index];
492   if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE)
493   {
494     return 0;
495   }
496   return enemy;
497 }
498 
499 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY)
500 {
501   Enemy *spawn = 0;
502   for (int i = 0; i < enemyCount; i++)
503   {
504     Enemy *enemy = &enemies[i];
505     if (enemy->enemyType == ENEMY_TYPE_NONE)
506     {
507       spawn = enemy;
508       break;
509     }
510   }
511 
512   if (enemyCount < ENEMY_MAX_COUNT && !spawn)
513   {
514     spawn = &enemies[enemyCount++];
515   }
516 
517   if (spawn)
518   {
519     *spawn = (Enemy){
520       .currentX = currentX,
521       .currentY = currentY,
522       .nextX = currentX,
523       .nextY = currentY,
524       .simPosition = (Vector2){currentX, currentY},
525       .simVelocity = (Vector2){0, 0},
526       .enemyType = enemyType,
527       .startMovingTime = gameTime.time,
528       .movePathCount = 0,
529       .walkedDistance = 0.0f,
530       .shieldDamage = 0.0f,
531       .damage = 0.0f,
532       .futureDamage = 0.0f,
533       .contactTime = 0.0f,
534       .generation = spawn->generation + 1,
535     };
536   }
537 
538   return spawn;
539 }
540 
541 int EnemyAddDamageRange(Vector2 position, float range, float damage)
542 {
543   int count = 0;
544   float range2 = range * range;
545   for (int i = 0; i < enemyCount; i++)
546   {
547     Enemy *enemy = &enemies[i];
548     if (enemy->enemyType == ENEMY_TYPE_NONE)
549     {
550       continue;
551     }
552     float distance2 = Vector2DistanceSqr(position, enemy->simPosition);
553     if (distance2 <= range2)
554     {
555       EnemyAddDamage(enemy, damage);
556       count++;
557     }
558   }
559   return count;
560 }
561 
562 int EnemyAddDamage(Enemy *enemy, float damage)
563 {
564   float shieldHealth = enemyClassConfigs[enemy->enemyType].shieldHealth;
565   if (shieldHealth > 0.0f && enemy->shieldDamage < shieldHealth)
566   {
567     float shieldDamageAbsorption = enemyClassConfigs[enemy->enemyType].shieldDamageAbsorption;
568     float shieldDamage = fminf(fminf(shieldDamageAbsorption, damage), shieldHealth - enemy->shieldDamage);
569     enemy->shieldDamage += shieldDamage;
570     damage -= shieldDamage;
571   }
572   enemy->damage += damage;
573   if (enemy->damage >= EnemyGetMaxHealth(enemy))
574   {
575     currentLevel->playerGold += enemyClassConfigs[enemy->enemyType].goldValue;
576     enemy->enemyType = ENEMY_TYPE_NONE;
577     return 1;
578   }
579 
580   return 0;
581 }
582 
583 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
584 {
585   int16_t castleX = 0;
586   int16_t castleY = 0;
587   Enemy* closest = 0;
588   int16_t closestDistance = 0;
589   float range2 = range * range;
590   for (int i = 0; i < enemyCount; i++)
591   {
592     Enemy* enemy = &enemies[i];
593     if (enemy->enemyType == ENEMY_TYPE_NONE)
594     {
595       continue;
596     }
597     float maxHealth = EnemyGetMaxHealth(enemy) + enemyClassConfigs[enemy->enemyType].shieldHealth;
598     if (enemy->futureDamage >= maxHealth)
599     {
600       // ignore enemies that will die soon
601       continue;
602     }
603     int16_t dx = castleX - enemy->currentX;
604     int16_t dy = castleY - enemy->currentY;
605     int16_t distance = abs(dx) + abs(dy);
606     if (!closest || distance < closestDistance)
607     {
608       float tdx = towerX - enemy->currentX;
609       float tdy = towerY - enemy->currentY;
610       float tdistance2 = tdx * tdx + tdy * tdy;
611       if (tdistance2 <= range2)
612       {
613         closest = enemy;
614         closestDistance = distance;
615       }
616     }
617   }
618   return closest;
619 }
620 
621 int EnemyCount()
622 {
623   int count = 0;
624   for (int i = 0; i < enemyCount; i++)
625   {
626     if (enemies[i].enemyType != ENEMY_TYPE_NONE)
627     {
628       count++;
629     }
630   }
631   return count;
632 }
633 
634 void EnemyDrawHealthbars(Camera3D camera)
635 {
636   for (int i = 0; i < enemyCount; i++)
637   {
638     Enemy *enemy = &enemies[i];
639     
640     float maxShieldHealth = enemyClassConfigs[enemy->enemyType].shieldHealth;
641     if (maxShieldHealth > 0.0f && enemy->shieldDamage < maxShieldHealth && enemy->shieldDamage > 0.0f)
642     {
643       float shieldHealth = maxShieldHealth - enemy->shieldDamage;
644       float shieldHealthRatio = shieldHealth / maxShieldHealth;
645       Vector3 position = (Vector3){enemy->simPosition.x, 0.5f, enemy->simPosition.y};
646       DrawHealthBar(camera, position, (Vector2) {.y = -4}, shieldHealthRatio, BLUE, 20.0f);
647     }
648 
649     if (enemy->enemyType == ENEMY_TYPE_NONE || enemy->damage == 0.0f)
650     {
651       continue;
652     }
653     Vector3 position = (Vector3){enemy->simPosition.x, 0.5f, enemy->simPosition.y};
654     float maxHealth = EnemyGetMaxHealth(enemy);
655     float health = maxHealth - enemy->damage;
656     float healthRatio = health / maxHealth;
657     
658     DrawHealthBar(camera, position, Vector2Zero(), healthRatio, GREEN, 15.0f);
659   }
660 }
  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 range function of the tower returns now the range depending on the upgrade level. It is a simple linear interpolation (Lerp) between the base range and the maximum range. The range is properly used and displayed in the UI, just like intended.

But looking at the various function signatures of the tower system ... :

  1 void TowerInit();
  2 float TowerGetMaxHealth(Tower *tower);
  3 Tower *GetTowerByIndex(int index);
  4 Tower *TowerGetAt(int16_t x, int16_t y);
  5 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y);
  6 Tower *GetTowerByType(uint8_t towerType);
  7 int GetTowerCosts(uint8_t towerType);
  8 const char *GetTowerName(uint8_t towerType);
  9 float GetTowerRange(Tower tower);
 10 float TowerGetMaxHealth(Tower *tower);
 11 void TowerDraw();
 12 void TowerDrawSingle(Tower tower);
 13 void TowerDrawRange(Tower tower, float alpha);
 14 void TowerUpdate();
 15 void TowerUpdateRangeFade(Tower *selectedTower, float fadeoutTarget);
 16 void TowerDrawHealthBars(Camera3D camera);

... it is clear that there is quite some inconsistency in naming and passing arguments:

Accumulating such inconsistencies is common and it just happens. Cleaning this up from time to time is a good idea, just like thinking about why this happenend in the first place.

Naming the functions consistently helps navigating the codebase. Raylib uses the convention to start function names with a verb. My own practice I use to follow is to start function names with the system they primarily belong to, which is typically a noun. I like that when typing "Tower" in the editor, I get a list of all functions related to the tower system. Another point is visual aesthetics to me:

  1 void TowerInit();
  2 void TowerUpdate();
  3 void TowerDraw();
  4 float TowerGetRange(Tower *tower);
  5 
  6 // vs
  7 
  8 void InitTower();
  9 void UpdateTower();
 10 void DrawTower();
 11 float GetTowerRange(Tower *tower);

I find the first version more pleasing to the eye since the verbs after the nouns start most often at the same column since they all start with the same prefix ( void Tower... ).

But this is just a personal preference; people have different tastes, which is fine. Getting confused by inconsistent naming makes it however harder to use the code. So whatever convention you choose, stick to it. The majority of functions in the tower system start with Tower, so I will stick to that.

Another point is the inconsistency in passing the tower as a pointer or as a value. Again, it is common in raylib's codebase to pass structs as values. Using pointers can be confusing for beginners, which is why raylib does it this way.

Since most functions in the tower system take a pointer to a tower, I will stick to that as well. Pass by pointer tends to be more efficient since the struct is not copied.

There is much that could be said about this topic, but I think most of the arguments are quite subjective when it comes to naming. The most important thing is to be consistent. Since quite a few functions modify the tower struct, passing pointers feels more natural and sticking to a single convention makes the code easier to remember and use. So let's do the cleanups for the tower module:

  • 💾
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 #include <rlgl.h>
  4 #include <stdlib.h>
  5 #include <math.h>
  6 #include <string.h>
  7 
  8 //# Variables
  9 Font gameFontNormal = {0};
 10 GUIState guiState = {0};
 11 GameTime gameTime = {
 12   .fixedDeltaTime = 1.0f / 60.0f,
 13 };
 14 
 15 Model floorTileAModel = {0};
 16 Model floorTileBModel = {0};
 17 Model treeModel[2] = {0};
 18 Model firTreeModel[2] = {0};
 19 Model rockModels[5] = {0};
 20 Model grassPatchModel[1] = {0};
 21 
 22 Model pathArrowModel = {0};
 23 Model greenArrowModel = {0};
 24 
 25 Texture2D palette, spriteSheet;
 26 
 27 NPatchInfo uiPanelPatch = {
 28   .layout = NPATCH_NINE_PATCH,
 29   .source = {145, 1, 46, 46},
 30   .top = 18, .bottom = 18,
 31   .left = 16, .right = 16
 32 };
 33 NPatchInfo uiButtonNormal = {
 34   .layout = NPATCH_NINE_PATCH,
 35   .source = {193, 1, 32, 20},
 36   .top = 7, .bottom = 7,
 37   .left = 10, .right = 10
 38 };
 39 NPatchInfo uiButtonDisabled = {
 40   .layout = NPATCH_NINE_PATCH,
 41   .source = {193, 22, 32, 20},
 42   .top = 7, .bottom = 7,
 43   .left = 10, .right = 10
 44 };
 45 NPatchInfo uiButtonHovered = {
 46   .layout = NPATCH_NINE_PATCH,
 47   .source = {193, 43, 32, 20},
 48   .top = 7, .bottom = 7,
 49   .left = 10, .right = 10
 50 };
 51 NPatchInfo uiButtonPressed = {
 52   .layout = NPATCH_NINE_PATCH,
 53   .source = {193, 64, 32, 20},
 54   .top = 7, .bottom = 7,
 55   .left = 10, .right = 10
 56 };
 57 Rectangle uiDiamondMarker = {145, 48, 15, 15};
 58 
 59 Level levels[] = {
 60   [0] = {
 61     .state = LEVEL_STATE_BUILDING,
 62     .initialGold = 20,
 63     .waves[0] = {
 64       .enemyType = ENEMY_TYPE_SHIELD,
 65       .wave = 0,
 66       .count = 1,
 67       .interval = 2.5f,
 68       .delay = 1.0f,
 69       .spawnPosition = {2, 6},
 70     },
 71     .waves[1] = {
 72       .enemyType = ENEMY_TYPE_RUNNER,
 73       .wave = 0,
 74       .count = 5,
 75       .interval = 0.5f,
 76       .delay = 1.0f,
 77       .spawnPosition = {-2, 6},
 78     },
 79     .waves[2] = {
 80       .enemyType = ENEMY_TYPE_SHIELD,
 81       .wave = 1,
 82       .count = 20,
 83       .interval = 1.5f,
 84       .delay = 1.0f,
 85       .spawnPosition = {0, 6},
 86     },
 87     .waves[3] = {
 88       .enemyType = ENEMY_TYPE_MINION,
 89       .wave = 2,
 90       .count = 30,
 91       .interval = 1.2f,
 92       .delay = 1.0f,
 93       .spawnPosition = {2, 6},
 94     },
 95     .waves[4] = {
 96       .enemyType = ENEMY_TYPE_BOSS,
 97       .wave = 2,
 98       .count = 2,
 99       .interval = 5.0f,
100       .delay = 2.0f,
101       .spawnPosition = {-2, 4},
102     }
103   },
104 };
105 
106 Level *currentLevel = levels;
107 
108 void DrawBoxedText(const char *text, int x, int y, int width, int height, float alignX, float alignY, Color textColor);
109 
110 void DrawTitleText(const char *text, int anchorX, float alignX, Color color)
111 {
112   int textWidth = MeasureTextEx(gameFontNormal, text, gameFontNormal.baseSize, 0).x;
113   int panelWidth = textWidth + 40;
114   int posX = anchorX - panelWidth * alignX;
115   int textOffset = 20;
116   DrawTextureNPatch(spriteSheet, uiPanelPatch, (Rectangle){ posX, -10, panelWidth, 40}, Vector2Zero(), 0, WHITE);
117   DrawTextEx(gameFontNormal, text, (Vector2){posX + textOffset + 1, 6 + 1}, gameFontNormal.baseSize, 0, BLACK);
118   DrawTextEx(gameFontNormal, text, (Vector2){posX + textOffset, 6}, gameFontNormal.baseSize, 0, color);
119 }
120 
121 void DrawTitle(const char *text)
122 {
123   DrawTitleText(text, GetScreenWidth() / 2, 0.5f, WHITE);
124 }
125 
126 //# Game
127 
128 static Model LoadGLBModel(char *filename)
129 {
130   Model model = LoadModel(TextFormat("data/%s.glb",filename));
131   for (int i = 0; i < model.materialCount; i++)
132   {
133     model.materials[i].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
134   }
135   return model;
136 }
137 
138 void LoadAssets()
139 {
140   // load a sprite sheet that contains all units
141   spriteSheet = LoadTexture("data/spritesheet.png");
142   SetTextureFilter(spriteSheet, TEXTURE_FILTER_BILINEAR);
143 
144   // we'll use a palette texture to colorize the all buildings and environment art
145   palette = LoadTexture("data/palette.png");
146   // The texture uses gradients on very small space, so we'll enable bilinear filtering
147   SetTextureFilter(palette, TEXTURE_FILTER_BILINEAR);
148 
149   gameFontNormal = LoadFont("data/alagard.png");
150 
151   floorTileAModel = LoadGLBModel("floor-tile-a");
152   floorTileBModel = LoadGLBModel("floor-tile-b");
153   treeModel[0] = LoadGLBModel("leaftree-large-1-a");
154   treeModel[1] = LoadGLBModel("leaftree-large-1-b");
155   firTreeModel[0] = LoadGLBModel("firtree-1-a");
156   firTreeModel[1] = LoadGLBModel("firtree-1-b");
157   rockModels[0] = LoadGLBModel("rock-1");
158   rockModels[1] = LoadGLBModel("rock-2");
159   rockModels[2] = LoadGLBModel("rock-3");
160   rockModels[3] = LoadGLBModel("rock-4");
161   rockModels[4] = LoadGLBModel("rock-5");
162   grassPatchModel[0] = LoadGLBModel("grass-patch-1");
163 
164   pathArrowModel = LoadGLBModel("direction-arrow-x");
165   greenArrowModel = LoadGLBModel("green-arrow");
166 }
167 
168 void InitLevel(Level *level)
169 {
170   level->seed = (int)(GetTime() * 100.0f);
171 
172   TowerInit();
173   EnemyInit();
174   ProjectileInit();
175   ParticleInit();
176   TowerTryAdd(TOWER_TYPE_BASE, 0, 0);
177 
178   level->placementMode = 0;
179   level->state = LEVEL_STATE_BUILDING;
180   level->nextState = LEVEL_STATE_NONE;
181   level->playerGold = level->initialGold;
182   level->currentWave = 0;
183   level->placementX = -1;
184   level->placementY = 0;
185 
186   Camera *camera = &level->camera;
187   camera->position = (Vector3){4.0f, 8.0f, 8.0f};
188   camera->target = (Vector3){0.0f, 0.0f, 0.0f};
189   camera->up = (Vector3){0.0f, 1.0f, 0.0f};
190   camera->fovy = 11.5f;
191   camera->projection = CAMERA_ORTHOGRAPHIC;
192 }
193 
194 void DrawLevelHud(Level *level)
195 {
196   const char *text = TextFormat("Gold: %d", level->playerGold);
197   DrawTitleText(text, GetScreenWidth() - 10, 1.0f, YELLOW);
198 }
199 
200 void DrawLevelReportLostWave(Level *level)
201 {
202   BeginMode3D(level->camera);
203   DrawLevelGround(level);
204 TowerDrawAll();
205 EnemyDraw(); 206 ProjectileDraw(); 207 ParticleDraw(); 208 guiState.isBlocked = 0; 209 EndMode3D(); 210
211 TowerDrawAllHealthBars(level->camera);
212 213 DrawTitle("Wave lost"); 214 215 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 216 { 217 level->nextState = LEVEL_STATE_RESET; 218 } 219 } 220 221 int HasLevelNextWave(Level *level) 222 { 223 for (int i = 0; i < 10; i++) 224 { 225 EnemyWave *wave = &level->waves[i]; 226 if (wave->wave == level->currentWave) 227 { 228 return 1; 229 } 230 } 231 return 0; 232 } 233 234 void DrawLevelReportWonWave(Level *level) 235 { 236 BeginMode3D(level->camera); 237 DrawLevelGround(level);
238 TowerDrawAll();
239 EnemyDraw(); 240 ProjectileDraw(); 241 ParticleDraw(); 242 guiState.isBlocked = 0; 243 EndMode3D(); 244
245 TowerDrawAllHealthBars(level->camera);
246 247 DrawTitle("Wave won"); 248 249 250 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 251 { 252 level->nextState = LEVEL_STATE_RESET; 253 } 254 255 if (HasLevelNextWave(level)) 256 { 257 if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0)) 258 { 259 level->nextState = LEVEL_STATE_BUILDING; 260 } 261 } 262 else { 263 if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0)) 264 { 265 level->nextState = LEVEL_STATE_WON_LEVEL; 266 } 267 } 268 } 269 270 int DrawBuildingBuildButton(Level *level, int x, int y, int width, int height, uint8_t towerType, const char *name) 271 { 272 static ButtonState buttonStates[8] = {0};
273 int cost = TowerTypeGetCosts(towerType);
274 const char *text = TextFormat("%s: %d", name, cost); 275 buttonStates[towerType].isSelected = level->placementMode == towerType; 276 buttonStates[towerType].isDisabled = level->playerGold < cost; 277 if (Button(text, x, y, width, height, &buttonStates[towerType])) 278 { 279 level->placementMode = buttonStates[towerType].isSelected ? 0 : towerType; 280 level->nextState = LEVEL_STATE_BUILDING_PLACEMENT; 281 return 1; 282 } 283 return 0; 284 } 285 286 float GetRandomFloat(float min, float max) 287 { 288 int random = GetRandomValue(0, 0xfffffff); 289 return ((float)random / (float)0xfffffff) * (max - min) + min; 290 } 291 292 void DrawLevelGround(Level *level) 293 { 294 // draw checkerboard ground pattern 295 for (int x = -5; x <= 5; x += 1) 296 { 297 for (int y = -5; y <= 5; y += 1) 298 { 299 Model *model = (x + y) % 2 == 0 ? &floorTileAModel : &floorTileBModel; 300 DrawModel(*model, (Vector3){x, 0.0f, y}, 1.0f, WHITE); 301 } 302 } 303 304 int oldSeed = GetRandomValue(0, 0xfffffff); 305 SetRandomSeed(level->seed); 306 // increase probability for trees via duplicated entries 307 Model borderModels[64]; 308 int maxRockCount = GetRandomValue(2, 6); 309 int maxTreeCount = GetRandomValue(10, 20); 310 int maxFirTreeCount = GetRandomValue(5, 10); 311 int maxLeafTreeCount = maxTreeCount - maxFirTreeCount; 312 int grassPatchCount = GetRandomValue(5, 30); 313 314 int modelCount = 0; 315 for (int i = 0; i < maxRockCount && modelCount < 63; i++) 316 { 317 borderModels[modelCount++] = rockModels[GetRandomValue(0, 5)]; 318 } 319 for (int i = 0; i < maxLeafTreeCount && modelCount < 63; i++) 320 { 321 borderModels[modelCount++] = treeModel[GetRandomValue(0, 1)]; 322 } 323 for (int i = 0; i < maxFirTreeCount && modelCount < 63; i++) 324 { 325 borderModels[modelCount++] = firTreeModel[GetRandomValue(0, 1)]; 326 } 327 for (int i = 0; i < grassPatchCount && modelCount < 63; i++) 328 { 329 borderModels[modelCount++] = grassPatchModel[0]; 330 } 331 332 // draw some objects around the border of the map 333 Vector3 up = {0, 1, 0}; 334 // a pseudo random number generator to get the same result every time 335 const float wiggle = 0.75f; 336 const int layerCount = 3; 337 for (int layer = 0; layer <= layerCount; layer++) 338 { 339 int layerPos = 6 + layer; 340 Model *selectedModels = borderModels; 341 int selectedModelCount = modelCount; 342 if (layer == 0) 343 { 344 selectedModels = grassPatchModel; 345 selectedModelCount = 1; 346 } 347 for (int x = -6 - layer; x <= 6 + layer; x += 1) 348 { 349 DrawModelEx(selectedModels[GetRandomValue(0, selectedModelCount - 1)], 350 (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, -layerPos + GetRandomFloat(0.0f, wiggle)}, 351 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE); 352 DrawModelEx(selectedModels[GetRandomValue(0, selectedModelCount - 1)], 353 (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, layerPos + GetRandomFloat(0.0f, wiggle)}, 354 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE); 355 } 356 357 for (int z = -5 - layer; z <= 5 + layer; z += 1) 358 { 359 DrawModelEx(selectedModels[GetRandomValue(0, selectedModelCount - 1)], 360 (Vector3){-layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)}, 361 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE); 362 DrawModelEx(selectedModels[GetRandomValue(0, selectedModelCount - 1)], 363 (Vector3){layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)}, 364 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE); 365 } 366 } 367 368 SetRandomSeed(oldSeed); 369 } 370 371 void DrawEnemyPath(Level *level, Color arrowColor) 372 { 373 const int castleX = 0, castleY = 0; 374 const int maxWaypointCount = 200; 375 const float timeStep = 1.0f; 376 Vector3 arrowScale = {0.75f, 0.75f, 0.75f}; 377 378 // we start with a time offset to simulate the path, 379 // this way the arrows are animated in a forward moving direction 380 // The time is wrapped around the time step to get a smooth animation 381 float timeOffset = fmodf(GetTime(), timeStep); 382 383 for (int i = 0; i < ENEMY_MAX_WAVE_COUNT; i++) 384 { 385 EnemyWave *wave = &level->waves[i]; 386 if (wave->wave != level->currentWave) 387 { 388 continue; 389 } 390 391 // use this dummy enemy to simulate the path 392 Enemy dummy = { 393 .enemyType = ENEMY_TYPE_MINION, 394 .simPosition = (Vector2){wave->spawnPosition.x, wave->spawnPosition.y}, 395 .nextX = wave->spawnPosition.x, 396 .nextY = wave->spawnPosition.y, 397 .currentX = wave->spawnPosition.x, 398 .currentY = wave->spawnPosition.y, 399 }; 400 401 float deltaTime = timeOffset; 402 for (int j = 0; j < maxWaypointCount; j++) 403 { 404 int waypointPassedCount = 0; 405 Vector2 pos = EnemyGetPosition(&dummy, deltaTime, &dummy.simVelocity, &waypointPassedCount); 406 // after the initial variable starting offset, we use a fixed time step 407 deltaTime = timeStep; 408 dummy.simPosition = pos; 409 410 // Update the dummy's position just like we do in the regular enemy update loop 411 for (int k = 0; k < waypointPassedCount; k++) 412 { 413 dummy.currentX = dummy.nextX; 414 dummy.currentY = dummy.nextY; 415 if (EnemyGetNextPosition(dummy.currentX, dummy.currentY, &dummy.nextX, &dummy.nextY) && 416 Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f) 417 { 418 break; 419 } 420 } 421 if (Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f) 422 { 423 break; 424 } 425 426 // get the angle we need to rotate the arrow model. The velocity is just fine for this. 427 float angle = atan2f(dummy.simVelocity.x, dummy.simVelocity.y) * RAD2DEG - 90.0f; 428 DrawModelEx(pathArrowModel, (Vector3){pos.x, 0.15f, pos.y}, (Vector3){0, 1, 0}, angle, arrowScale, arrowColor); 429 } 430 } 431 } 432 433 void DrawEnemyPaths(Level *level) 434 { 435 // disable depth testing for the path arrows 436 // flush the 3D batch to draw the arrows on top of everything 437 rlDrawRenderBatchActive(); 438 rlDisableDepthTest(); 439 DrawEnemyPath(level, (Color){64, 64, 64, 160}); 440 441 rlDrawRenderBatchActive(); 442 rlEnableDepthTest(); 443 DrawEnemyPath(level, WHITE); 444 } 445 446 static void SimulateTowerPlacementBehavior(Level *level, float towerFloatHeight, float mapX, float mapY) 447 { 448 float dt = gameTime.fixedDeltaTime; 449 // smooth transition for the placement position using exponential decay 450 const float lambda = 15.0f; 451 float factor = 1.0f - expf(-lambda * dt); 452 453 float damping = 0.5f; 454 float springStiffness = 300.0f; 455 float springDecay = 95.0f; 456 float minHeight = 0.35f; 457 458 if (level->placementPhase == PLACEMENT_PHASE_STARTING) 459 { 460 damping = 1.0f; 461 springDecay = 90.0f; 462 springStiffness = 100.0f; 463 minHeight = 0.70f; 464 } 465 466 for (int i = 0; i < gameTime.fixedStepCount; i++) 467 { 468 level->placementTransitionPosition = 469 Vector2Lerp( 470 level->placementTransitionPosition, 471 (Vector2){mapX, mapY}, factor); 472 473 // draw the spring position for debugging the spring simulation 474 // first step: stiff spring, no simulation 475 Vector3 worldPlacementPosition = (Vector3){ 476 level->placementTransitionPosition.x, towerFloatHeight, level->placementTransitionPosition.y}; 477 Vector3 springTargetPosition = (Vector3){ 478 worldPlacementPosition.x, 1.0f + towerFloatHeight, worldPlacementPosition.z}; 479 // consider the current velocity to predict the future position in order to dampen 480 // the spring simulation. Longer prediction times will result in more damping 481 Vector3 predictedSpringPosition = Vector3Add(level->placementTowerSpring.position, 482 Vector3Scale(level->placementTowerSpring.velocity, dt * damping)); 483 Vector3 springPointDelta = Vector3Subtract(springTargetPosition, predictedSpringPosition); 484 Vector3 velocityChange = Vector3Scale(springPointDelta, dt * springStiffness); 485 // decay velocity of the upright forcing spring 486 // This force acts like a 2nd spring that pulls the tip upright into the air above the 487 // base position 488 level->placementTowerSpring.velocity = Vector3Scale(level->placementTowerSpring.velocity, 1.0f - expf(-springDecay * dt)); 489 level->placementTowerSpring.velocity = Vector3Add(level->placementTowerSpring.velocity, velocityChange); 490 491 // calculate length of the spring to calculate the force to apply to the placementTowerSpring position 492 // we use a simple spring model with a rest length of 1.0f 493 Vector3 springDelta = Vector3Subtract(worldPlacementPosition, level->placementTowerSpring.position); 494 float springLength = Vector3Length(springDelta); 495 float springForce = (springLength - 1.0f) * springStiffness; 496 Vector3 springForceVector = Vector3Normalize(springDelta); 497 springForceVector = Vector3Scale(springForceVector, springForce); 498 level->placementTowerSpring.velocity = Vector3Add(level->placementTowerSpring.velocity, 499 Vector3Scale(springForceVector, dt)); 500 501 level->placementTowerSpring.position = Vector3Add(level->placementTowerSpring.position, 502 Vector3Scale(level->placementTowerSpring.velocity, dt)); 503 if (level->placementTowerSpring.position.y < minHeight + towerFloatHeight) 504 { 505 level->placementTowerSpring.velocity.y *= -1.0f; 506 level->placementTowerSpring.position.y = fmaxf(level->placementTowerSpring.position.y, minHeight + towerFloatHeight); 507 } 508 } 509 } 510 511 void DrawLevelBuildingPlacementState(Level *level) 512 { 513 const float placementDuration = 0.5f; 514 515 level->placementTimer += gameTime.deltaTime; 516 if (level->placementTimer > 1.0f && level->placementPhase == PLACEMENT_PHASE_STARTING) 517 { 518 level->placementPhase = PLACEMENT_PHASE_MOVING; 519 level->placementTimer = 0.0f; 520 } 521 522 BeginMode3D(level->camera); 523 DrawLevelGround(level); 524 525 int blockedCellCount = 0; 526 Vector2 blockedCells[1]; 527 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera); 528 float planeDistance = ray.position.y / -ray.direction.y; 529 float planeX = ray.direction.x * planeDistance + ray.position.x; 530 float planeY = ray.direction.z * planeDistance + ray.position.z; 531 int16_t mapX = (int16_t)floorf(planeX + 0.5f); 532 int16_t mapY = (int16_t)floorf(planeY + 0.5f); 533 if (level->placementPhase == PLACEMENT_PHASE_MOVING && 534 level->placementMode && !guiState.isBlocked && 535 mapX >= -5 && mapX <= 5 && mapY >= -5 && mapY <= 5 && IsMouseButtonDown(MOUSE_LEFT_BUTTON)) 536 { 537 level->placementX = mapX; 538 level->placementY = mapY; 539 } 540 else 541 { 542 mapX = level->placementX; 543 mapY = level->placementY; 544 } 545 blockedCells[blockedCellCount++] = (Vector2){mapX, mapY}; 546 PathFindingMapUpdate(blockedCellCount, blockedCells); 547
548 TowerDrawAll();
549 EnemyDraw(); 550 ProjectileDraw(); 551 ParticleDraw(); 552 DrawEnemyPaths(level); 553 554 // let the tower float up and down. Consider this height in the spring simulation as well 555 float towerFloatHeight = sinf(gameTime.time * 4.0f) * 0.2f + 0.3f; 556 557 if (level->placementPhase == PLACEMENT_PHASE_PLACING) 558 { 559 // The bouncing spring needs a bit of outro time to look nice and complete. 560 // So we scale the time so that the first 2/3rd of the placing phase handles the motion 561 // and the last 1/3rd is the outro physics (bouncing) 562 float t = fminf(1.0f, level->placementTimer / placementDuration * 1.5f); 563 // let towerFloatHeight describe parabola curve, starting at towerFloatHeight and ending at 0 564 float linearBlendHeight = (1.0f - t) * towerFloatHeight; 565 float parabola = (1.0f - ((t - 0.5f) * (t - 0.5f) * 4.0f)) * 2.0f; 566 towerFloatHeight = linearBlendHeight + parabola; 567 } 568 569 SimulateTowerPlacementBehavior(level, towerFloatHeight, mapX, mapY); 570 571 rlPushMatrix(); 572 rlTranslatef(level->placementTransitionPosition.x, 0, level->placementTransitionPosition.y); 573 574 // calculate x and z rotation to align the model with the spring 575 Vector3 position = {level->placementTransitionPosition.x, towerFloatHeight, level->placementTransitionPosition.y}; 576 Vector3 towerUp = Vector3Subtract(level->placementTowerSpring.position, position); 577 Vector3 rotationAxis = Vector3CrossProduct(towerUp, (Vector3){0, 1, 0}); 578 float angle = acosf(Vector3DotProduct(towerUp, (Vector3){0, 1, 0}) / Vector3Length(towerUp)) * RAD2DEG; 579 float springLength = Vector3Length(towerUp); 580 float towerStretch = fminf(fmaxf(springLength, 0.5f), 4.5f); 581 float towerSquash = 1.0f / towerStretch; 582 583 Tower dummy = { 584 .towerType = level->placementMode, 585 }; 586 587 float rangeAlpha = fminf(1.0f, level->placementTimer / placementDuration); 588 if (level->placementPhase == PLACEMENT_PHASE_PLACING) 589 { 590 rangeAlpha = 1.0f - rangeAlpha; 591 } 592 else if (level->placementPhase == PLACEMENT_PHASE_MOVING) 593 { 594 rangeAlpha = 1.0f; 595 } 596
597 TowerDrawRange(&dummy, rangeAlpha);
598 599 rlPushMatrix(); 600 rlTranslatef(0.0f, towerFloatHeight, 0.0f); 601 602 rlRotatef(-angle, rotationAxis.x, rotationAxis.y, rotationAxis.z); 603 rlScalef(towerSquash, towerStretch, towerSquash);
604 TowerDrawModel(&dummy);
605 rlPopMatrix(); 606 607 608 // draw a shadow for the tower 609 float umbrasize = 0.8 + sqrtf(towerFloatHeight); 610 DrawCube((Vector3){0.0f, 0.05f, 0.0f}, umbrasize, 0.0f, umbrasize, (Color){0, 0, 0, 32}); 611 DrawCube((Vector3){0.0f, 0.075f, 0.0f}, 0.85f, 0.0f, 0.85f, (Color){0, 0, 0, 64}); 612 613 614 float bounce = sinf(gameTime.time * 8.0f) * 0.5f + 0.5f; 615 float offset = fmaxf(0.0f, bounce - 0.4f) * 0.35f + 0.7f; 616 float squeeze = -fminf(0.0f, bounce - 0.4f) * 0.7f + 0.7f; 617 float stretch = fmaxf(0.0f, bounce - 0.3f) * 0.5f + 0.8f; 618 619 DrawModelEx(greenArrowModel, (Vector3){ offset, 0.0f, 0.0f}, (Vector3){0, 1, 0}, 90, (Vector3){squeeze, 1.0f, stretch}, WHITE); 620 DrawModelEx(greenArrowModel, (Vector3){-offset, 0.0f, 0.0f}, (Vector3){0, 1, 0}, 270, (Vector3){squeeze, 1.0f, stretch}, WHITE); 621 DrawModelEx(greenArrowModel, (Vector3){ 0.0f, 0.0f, offset}, (Vector3){0, 1, 0}, 0, (Vector3){squeeze, 1.0f, stretch}, WHITE); 622 DrawModelEx(greenArrowModel, (Vector3){ 0.0f, 0.0f, -offset}, (Vector3){0, 1, 0}, 180, (Vector3){squeeze, 1.0f, stretch}, WHITE); 623 rlPopMatrix(); 624 625 guiState.isBlocked = 0; 626 627 EndMode3D(); 628
629 TowerDrawAllHealthBars(level->camera);
630 631 if (level->placementPhase == PLACEMENT_PHASE_PLACING) 632 { 633 if (level->placementTimer > placementDuration) 634 { 635 Tower *tower = TowerTryAdd(level->placementMode, mapX, mapY); 636 // testing repairing 637 tower->damage = 2.5f;
638 level->playerGold -= TowerTypeGetCosts(level->placementMode);
639 level->nextState = LEVEL_STATE_BUILDING; 640 level->placementMode = TOWER_TYPE_NONE; 641 } 642 } 643 else 644 { 645 if (Button("Cancel", 20, GetScreenHeight() - 40, 160, 30, 0)) 646 { 647 level->nextState = LEVEL_STATE_BUILDING; 648 level->placementMode = TOWER_TYPE_NONE; 649 TraceLog(LOG_INFO, "Cancel building"); 650 } 651 652 if (TowerGetAt(mapX, mapY) == 0 && Button("Build", GetScreenWidth() - 180, GetScreenHeight() - 40, 160, 30, 0)) 653 { 654 level->placementPhase = PLACEMENT_PHASE_PLACING; 655 level->placementTimer = 0.0f; 656 } 657 } 658 } 659 660 enum ContextMenuType 661 { 662 CONTEXT_MENU_TYPE_MAIN, 663 CONTEXT_MENU_TYPE_SELL_CONFIRM, 664 CONTEXT_MENU_TYPE_UPGRADE, 665 }; 666 667 enum UpgradeType 668 { 669 UPGRADE_TYPE_SPEED, 670 UPGRADE_TYPE_DAMAGE, 671 UPGRADE_TYPE_RANGE, 672 }; 673 674 typedef struct ContextMenuArgs 675 { 676 void *data; 677 uint8_t uint8; 678 int32_t int32; 679 Tower *tower; 680 } ContextMenuArgs; 681 682 int OnContextMenuBuild(Level *level, ContextMenuArgs *data) 683 { 684 uint8_t towerType = data->uint8; 685 level->placementMode = towerType; 686 level->nextState = LEVEL_STATE_BUILDING_PLACEMENT; 687 return 1; 688 } 689 690 int OnContextMenuSellConfirm(Level *level, ContextMenuArgs *data) 691 { 692 Tower *tower = data->tower; 693 int gold = data->int32; 694 level->playerGold += gold; 695 tower->towerType = TOWER_TYPE_NONE; 696 return 1; 697 } 698 699 int OnContextMenuSellCancel(Level *level, ContextMenuArgs *data) 700 { 701 return 1; 702 } 703 704 int OnContextMenuSell(Level *level, ContextMenuArgs *data) 705 { 706 level->placementContextMenuType = CONTEXT_MENU_TYPE_SELL_CONFIRM; 707 return 0; 708 } 709 710 int OnContextMenuDoUpgrade(Level *level, ContextMenuArgs *data) 711 { 712 Tower *tower = data->tower; 713 switch (data->uint8) 714 { 715 case UPGRADE_TYPE_SPEED: tower->upgradeState.speed++; break; 716 case UPGRADE_TYPE_DAMAGE: tower->upgradeState.damage++; break; 717 case UPGRADE_TYPE_RANGE: tower->upgradeState.range++; break; 718 } 719 level->playerGold -= data->int32; 720 return 0; 721 } 722 723 int OnContextMenuUpgrade(Level *level, ContextMenuArgs *data) 724 { 725 level->placementContextMenuType = CONTEXT_MENU_TYPE_UPGRADE; 726 return 0; 727 } 728 729 int OnContextMenuRepair(Level *level, ContextMenuArgs *data) 730 { 731 Tower *tower = data->tower; 732 if (level->playerGold >= 1) 733 { 734 level->playerGold -= 1; 735 tower->damage = fmaxf(0.0f, tower->damage - 1.0f); 736 } 737 return tower->damage == 0.0f; 738 } 739 740 typedef struct ContextMenuItem 741 { 742 uint8_t index; 743 char text[24]; 744 float alignX; 745 int (*action)(Level*, ContextMenuArgs*); 746 void *data; 747 ContextMenuArgs args; 748 ButtonState buttonState; 749 } ContextMenuItem; 750 751 ContextMenuItem ContextMenuItemText(uint8_t index, const char *text, float alignX) 752 { 753 ContextMenuItem item = {.index = index, .alignX = alignX}; 754 strncpy(item.text, text, 24); 755 return item; 756 } 757 758 ContextMenuItem ContextMenuItemButton(uint8_t index, const char *text, int (*action)(Level*, ContextMenuArgs*), ContextMenuArgs args) 759 { 760 ContextMenuItem item = {.index = index, .action = action, .args = args}; 761 strncpy(item.text, text, 24); 762 return item; 763 } 764 765 int DrawContextMenu(Level *level, Vector2 anchorLow, Vector2 anchorHigh, int width, ContextMenuItem *menus) 766 { 767 const int itemHeight = 28; 768 const int itemSpacing = 1; 769 const int padding = 8; 770 int itemCount = 0; 771 for (int i = 0; menus[i].text[0]; i++) 772 { 773 itemCount = itemCount > menus[i].index ? itemCount : menus[i].index; 774 } 775 776 Rectangle contextMenu = {0, 0, width, 777 (itemHeight + itemSpacing) * (itemCount + 1) + itemSpacing + padding * 2}; 778 779 Vector2 anchor = anchorHigh.y - 30 > contextMenu.height ? anchorHigh : anchorLow; 780 float anchorPivotY = anchorHigh.y - 30 > contextMenu.height ? 1.0f : 0.0f; 781 782 contextMenu.x = anchor.x - contextMenu.width * 0.5f; 783 contextMenu.y = anchor.y - contextMenu.height * anchorPivotY; 784 contextMenu.x = fmaxf(0, fminf(GetScreenWidth() - contextMenu.width, contextMenu.x)); 785 contextMenu.y = fmaxf(0, fminf(GetScreenHeight() - contextMenu.height, contextMenu.y)); 786 787 DrawTextureNPatch(spriteSheet, uiPanelPatch, contextMenu, Vector2Zero(), 0, WHITE); 788 DrawTextureRec(spriteSheet, uiDiamondMarker, (Vector2){anchor.x - uiDiamondMarker.width / 2, anchor.y - uiDiamondMarker.height / 2}, WHITE); 789 const int itemX = contextMenu.x + itemSpacing; 790 const int itemWidth = contextMenu.width - itemSpacing * 2; 791 #define ITEM_Y(idx) (contextMenu.y + itemSpacing + (itemHeight + itemSpacing) * idx + padding) 792 #define ITEM_RECT(idx, marginLR) itemX + (marginLR) + padding, ITEM_Y(idx), itemWidth - (marginLR) * 2 - padding * 2, itemHeight 793 int status = 0; 794 for (int i = 0; menus[i].text[0]; i++) 795 { 796 if (menus[i].action) 797 { 798 if (Button(menus[i].text, ITEM_RECT(menus[i].index, 0), &menus[i].buttonState)) 799 { 800 status = menus[i].action(level, &menus[i].args); 801 } 802 } 803 else 804 { 805 DrawBoxedText(menus[i].text, ITEM_RECT(menus[i].index, 0), menus[i].alignX, 0.5f, WHITE); 806 } 807 } 808 809 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && !CheckCollisionPointRec(GetMousePosition(), contextMenu)) 810 { 811 return 1; 812 } 813 814 return status; 815 } 816 817 void DrawLevelBuildingStateMainContextMenu(Level *level, Tower *tower, int32_t sellValue, float hp, float maxHitpoints, Vector2 anchorLow, Vector2 anchorHigh) 818 { 819 ContextMenuItem menu[12] = {0}; 820 int menuCount = 0; 821 int menuIndex = 0; 822 if (tower) 823 { 824 825 if (tower) {
826 menu[menuCount++] = ContextMenuItemText(menuIndex++, TowerTypeGetName(tower->towerType), 0.5f);
827 } 828 829 // two texts, same line 830 menu[menuCount++] = ContextMenuItemText(menuIndex, "HP:", 0.0f); 831 menu[menuCount++] = ContextMenuItemText(menuIndex++, TextFormat("%.1f / %.1f", hp, maxHitpoints), 1.0f); 832 833 if (tower->towerType != TOWER_TYPE_BASE) 834 { 835 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "Upgrade", OnContextMenuUpgrade, 836 (ContextMenuArgs){.tower = tower}); 837 } 838 839 if (tower->towerType != TOWER_TYPE_BASE) 840 { 841 842 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "Sell", OnContextMenuSell, 843 (ContextMenuArgs){.tower = tower, .int32 = sellValue}); 844 } 845 if (tower->towerType != TOWER_TYPE_BASE && tower->damage > 0 && level->playerGold >= 1) 846 { 847 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "Repair 1 (1G)", OnContextMenuRepair, 848 (ContextMenuArgs){.tower = tower}); 849 } 850 } 851 else 852 { 853 menu[menuCount] = ContextMenuItemButton(menuIndex++,
854 TextFormat("Wall: %dG", TowerTypeGetCosts(TOWER_TYPE_WALL)),
855 OnContextMenuBuild, (ContextMenuArgs){ .uint8 = (uint8_t) TOWER_TYPE_WALL});
856 menu[menuCount++].buttonState.isDisabled = level->playerGold < TowerTypeGetCosts(TOWER_TYPE_WALL);
857 858 menu[menuCount] = ContextMenuItemButton(menuIndex++,
859 TextFormat("Archer: %dG", TowerTypeGetCosts(TOWER_TYPE_ARCHER)),
860 OnContextMenuBuild, (ContextMenuArgs){ .uint8 = (uint8_t) TOWER_TYPE_ARCHER});
861 menu[menuCount++].buttonState.isDisabled = level->playerGold < TowerTypeGetCosts(TOWER_TYPE_ARCHER);
862 863 menu[menuCount] = ContextMenuItemButton(menuIndex++,
864 TextFormat("Ballista: %dG", TowerTypeGetCosts(TOWER_TYPE_BALLISTA)),
865 OnContextMenuBuild, (ContextMenuArgs){ .uint8 = (uint8_t) TOWER_TYPE_BALLISTA});
866 menu[menuCount++].buttonState.isDisabled = level->playerGold < TowerTypeGetCosts(TOWER_TYPE_BALLISTA);
867 868 menu[menuCount] = ContextMenuItemButton(menuIndex++,
869 TextFormat("Catapult: %dG", TowerTypeGetCosts(TOWER_TYPE_CATAPULT)),
870 OnContextMenuBuild, (ContextMenuArgs){ .uint8 = (uint8_t) TOWER_TYPE_CATAPULT});
871 menu[menuCount++].buttonState.isDisabled = level->playerGold < TowerTypeGetCosts(TOWER_TYPE_CATAPULT);
872 } 873 874 if (DrawContextMenu(level, anchorLow, anchorHigh, 150, menu)) 875 { 876 level->placementContextMenuStatus = -1; 877 } 878 } 879 880 void DrawLevelBuildingState(Level *level) 881 { 882 // when the context menu is not active, we update the placement position 883 if (level->placementContextMenuStatus == 0) 884 { 885 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera); 886 float hitDistance = ray.position.y / -ray.direction.y; 887 float hitX = ray.direction.x * hitDistance + ray.position.x; 888 float hitY = ray.direction.z * hitDistance + ray.position.z; 889 level->placementX = (int)floorf(hitX + 0.5f); 890 level->placementY = (int)floorf(hitY + 0.5f); 891 } 892 893 // the currently hovered/selected tower 894 Tower *tower = TowerGetAt(level->placementX, level->placementY); 895 // show the range of the tower when hovering/selecting it
896 TowerUpdateAllRangeFade(tower, 0.0f);
897 898 BeginMode3D(level->camera); 899 DrawLevelGround(level); 900 PathFindingMapUpdate(0, 0);
901 TowerDrawAll();
902 EnemyDraw(); 903 ProjectileDraw(); 904 ParticleDraw(); 905 DrawEnemyPaths(level); 906 907 guiState.isBlocked = 0; 908 909 // Hover rectangle, when the mouse is over the map 910 int isHovering = level->placementX >= -5 && level->placementX <= 5 && level->placementY >= -5 && level->placementY <= 5; 911 if (isHovering) 912 { 913 DrawCubeWires((Vector3){level->placementX, 0.75f, level->placementY}, 1.0f, 1.5f, 1.0f, GREEN); 914 } 915 916 EndMode3D(); 917
918 TowerDrawAllHealthBars(level->camera);
919 920 DrawTitle("Building phase"); 921 922 // Draw the context menu when the context menu is active 923 if (level->placementContextMenuStatus >= 1) 924 { 925 float maxHitpoints = 0.0f; 926 float hp = 0.0f; 927 float damageFactor = 0.0f; 928 int32_t sellValue = 0; 929 930 if (tower) 931 { 932 maxHitpoints = TowerGetMaxHealth(tower); 933 hp = maxHitpoints - tower->damage; 934 damageFactor = 1.0f - tower->damage / maxHitpoints;
935 sellValue = (int32_t) ceilf(TowerTypeGetCosts(tower->towerType) * 0.5f * damageFactor);
936 } 937 938 ContextMenuItem menu[12] = {0}; 939 int menuCount = 0; 940 int menuIndex = 0; 941 Vector2 anchorLow = GetWorldToScreen((Vector3){level->placementX, -0.25f, level->placementY}, level->camera); 942 Vector2 anchorHigh = GetWorldToScreen((Vector3){level->placementX, 2.0f, level->placementY}, level->camera); 943 944 if (level->placementContextMenuType == CONTEXT_MENU_TYPE_MAIN) 945 { 946 DrawLevelBuildingStateMainContextMenu(level, tower, sellValue, hp, maxHitpoints, anchorLow, anchorHigh); 947 } 948 else if (level->placementContextMenuType == CONTEXT_MENU_TYPE_UPGRADE) 949 { 950 int totalLevel = tower->upgradeState.damage + tower->upgradeState.speed + tower->upgradeState.range; 951 int costs = totalLevel * 4; 952 int isMaxLevel = totalLevel >= TOWER_MAX_STAGE; 953 menu[menuCount++] = ContextMenuItemText(menuIndex++, TextFormat("%s tower, stage %d%s",
954 TowerTypeGetName(tower->towerType), totalLevel, isMaxLevel ? " (max)" : ""), 0.5f);
955 int buttonMenuIndex = menuIndex; 956 menu[menuCount++] = ContextMenuItemButton(menuIndex++, TextFormat("Speed: %d (%dG)", tower->upgradeState.speed, costs), 957 OnContextMenuDoUpgrade, (ContextMenuArgs){.tower = tower, .uint8 = UPGRADE_TYPE_SPEED, .int32 = costs}); 958 menu[menuCount++] = ContextMenuItemButton(menuIndex++, TextFormat("Damage: %d (%dG)", tower->upgradeState.damage, costs), 959 OnContextMenuDoUpgrade, (ContextMenuArgs){.tower = tower, .uint8 = UPGRADE_TYPE_DAMAGE, .int32 = costs}); 960 menu[menuCount++] = ContextMenuItemButton(menuIndex++, TextFormat("Range: %d (%dG)", tower->upgradeState.range, costs), 961 OnContextMenuDoUpgrade, (ContextMenuArgs){.tower = tower, .uint8 = UPGRADE_TYPE_RANGE, .int32 = costs}); 962 963 // check if buttons should be disabled 964 if (isMaxLevel || level->playerGold < costs) 965 { 966 for (int i = buttonMenuIndex; i < menuCount; i++) 967 { 968 menu[i].buttonState.isDisabled = 1; 969 } 970 } 971 972 if (DrawContextMenu(level, anchorLow, anchorHigh, 220, menu)) 973 { 974 level->placementContextMenuType = CONTEXT_MENU_TYPE_MAIN; 975 } 976 } 977 else if (level->placementContextMenuType == CONTEXT_MENU_TYPE_SELL_CONFIRM) 978 {
979 menu[menuCount++] = ContextMenuItemText(menuIndex++, TextFormat("Sell %s?", TowerTypeGetName(tower->towerType)), 0.5f);
980 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "Yes", OnContextMenuSellConfirm, 981 (ContextMenuArgs){.tower = tower, .int32 = sellValue}); 982 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "No", OnContextMenuSellCancel, (ContextMenuArgs){0}); 983 Vector2 anchorLow = {GetScreenWidth() * 0.5f, GetScreenHeight() * 0.5f}; 984 if (DrawContextMenu(level, anchorLow, anchorLow, 150, menu)) 985 { 986 level->placementContextMenuStatus = -1; 987 level->placementContextMenuType = CONTEXT_MENU_TYPE_MAIN; 988 } 989 } 990 } 991 992 // Activate the context menu when the mouse is clicked and the context menu is not active 993 else if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON) && isHovering && level->placementContextMenuStatus <= 0) 994 { 995 level->placementContextMenuStatus += 1; 996 } 997 998 if (level->placementContextMenuStatus == 0) 999 { 1000 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 1001 { 1002 level->nextState = LEVEL_STATE_RESET; 1003 } 1004 1005 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0)) 1006 { 1007 level->nextState = LEVEL_STATE_BATTLE; 1008 } 1009 1010 } 1011 } 1012 1013 void InitBattleStateConditions(Level *level) 1014 { 1015 level->state = LEVEL_STATE_BATTLE; 1016 level->nextState = LEVEL_STATE_NONE; 1017 level->waveEndTimer = 0.0f; 1018 for (int i = 0; i < 10; i++) 1019 { 1020 EnemyWave *wave = &level->waves[i]; 1021 wave->spawned = 0; 1022 wave->timeToSpawnNext = wave->delay; 1023 } 1024 } 1025 1026 void DrawLevelBattleState(Level *level) 1027 { 1028 BeginMode3D(level->camera); 1029 DrawLevelGround(level);
1030 TowerDrawAll();
1031 EnemyDraw(); 1032 ProjectileDraw(); 1033 ParticleDraw(); 1034 guiState.isBlocked = 0; 1035 EndMode3D(); 1036 1037 EnemyDrawHealthbars(level->camera);
1038 TowerDrawAllHealthBars(level->camera);
1039 1040 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 1041 { 1042 level->nextState = LEVEL_STATE_RESET; 1043 } 1044 1045 int maxCount = 0; 1046 int remainingCount = 0; 1047 for (int i = 0; i < 10; i++) 1048 { 1049 EnemyWave *wave = &level->waves[i]; 1050 if (wave->wave != level->currentWave) 1051 { 1052 continue; 1053 } 1054 maxCount += wave->count; 1055 remainingCount += wave->count - wave->spawned; 1056 } 1057 int aliveCount = EnemyCount(); 1058 remainingCount += aliveCount; 1059 1060 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount); 1061 DrawTitle(text); 1062 } 1063 1064 void DrawLevel(Level *level) 1065 { 1066 switch (level->state) 1067 { 1068 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break; 1069 case LEVEL_STATE_BUILDING_PLACEMENT: DrawLevelBuildingPlacementState(level); break; 1070 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break; 1071 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break; 1072 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break; 1073 default: break; 1074 } 1075 1076 DrawLevelHud(level); 1077 } 1078 1079 void UpdateLevel(Level *level) 1080 { 1081 if (level->state == LEVEL_STATE_BATTLE) 1082 { 1083 int activeWaves = 0; 1084 for (int i = 0; i < 10; i++) 1085 { 1086 EnemyWave *wave = &level->waves[i]; 1087 if (wave->spawned >= wave->count || wave->wave != level->currentWave) 1088 { 1089 continue; 1090 } 1091 activeWaves++; 1092 wave->timeToSpawnNext -= gameTime.deltaTime; 1093 if (wave->timeToSpawnNext <= 0.0f) 1094 { 1095 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y); 1096 if (enemy) 1097 { 1098 wave->timeToSpawnNext = wave->interval; 1099 wave->spawned++; 1100 } 1101 } 1102 }
1103 if (TowerGetByType(TOWER_TYPE_BASE) == 0) {
1104 level->waveEndTimer += gameTime.deltaTime; 1105 if (level->waveEndTimer >= 2.0f) 1106 { 1107 level->nextState = LEVEL_STATE_LOST_WAVE; 1108 } 1109 } 1110 else if (activeWaves == 0 && EnemyCount() == 0) 1111 { 1112 level->waveEndTimer += gameTime.deltaTime; 1113 if (level->waveEndTimer >= 2.0f) 1114 { 1115 level->nextState = LEVEL_STATE_WON_WAVE; 1116 } 1117 } 1118 } 1119 1120 PathFindingMapUpdate(0, 0); 1121 EnemyUpdate(); 1122 TowerUpdate(); 1123 ProjectileUpdate(); 1124 ParticleUpdate(); 1125 1126 if (level->nextState == LEVEL_STATE_RESET) 1127 { 1128 InitLevel(level); 1129 } 1130 1131 if (level->nextState == LEVEL_STATE_BATTLE) 1132 { 1133 InitBattleStateConditions(level); 1134 } 1135 1136 if (level->nextState == LEVEL_STATE_WON_WAVE) 1137 { 1138 level->currentWave++; 1139 level->state = LEVEL_STATE_WON_WAVE; 1140 } 1141 1142 if (level->nextState == LEVEL_STATE_LOST_WAVE) 1143 { 1144 level->state = LEVEL_STATE_LOST_WAVE; 1145 } 1146 1147 if (level->nextState == LEVEL_STATE_BUILDING) 1148 { 1149 level->state = LEVEL_STATE_BUILDING; 1150 level->placementContextMenuStatus = 0; 1151 } 1152 1153 if (level->nextState == LEVEL_STATE_BUILDING_PLACEMENT) 1154 { 1155 level->state = LEVEL_STATE_BUILDING_PLACEMENT; 1156 level->placementTransitionPosition = (Vector2){ 1157 level->placementX, level->placementY}; 1158 // initialize the spring to the current position 1159 level->placementTowerSpring = (PhysicsPoint){ 1160 .position = (Vector3){level->placementX, 8.0f, level->placementY}, 1161 .velocity = (Vector3){0.0f, 0.0f, 0.0f}, 1162 }; 1163 level->placementPhase = PLACEMENT_PHASE_STARTING; 1164 level->placementTimer = 0.0f; 1165 } 1166 1167 if (level->nextState == LEVEL_STATE_WON_LEVEL) 1168 { 1169 // make something of this later 1170 InitLevel(level); 1171 } 1172 1173 level->nextState = LEVEL_STATE_NONE; 1174 } 1175 1176 float nextSpawnTime = 0.0f; 1177 1178 void ResetGame() 1179 { 1180 InitLevel(currentLevel); 1181 } 1182 1183 void InitGame() 1184 { 1185 TowerInit(); 1186 EnemyInit(); 1187 ProjectileInit(); 1188 ParticleInit(); 1189 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f); 1190 1191 currentLevel = levels; 1192 InitLevel(currentLevel); 1193 } 1194 1195 //# Immediate GUI functions 1196 1197 void DrawHealthBar(Camera3D camera, Vector3 position, Vector2 screenOffset, float healthRatio, Color barColor, float healthBarWidth) 1198 { 1199 const float healthBarHeight = 6.0f; 1200 const float healthBarOffset = 15.0f; 1201 const float inset = 2.0f; 1202 const float innerWidth = healthBarWidth - inset * 2; 1203 const float innerHeight = healthBarHeight - inset * 2; 1204 1205 Vector2 screenPos = GetWorldToScreen(position, camera); 1206 screenPos = Vector2Add(screenPos, screenOffset); 1207 float centerX = screenPos.x - healthBarWidth * 0.5f; 1208 float topY = screenPos.y - healthBarOffset; 1209 DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK); 1210 float healthWidth = innerWidth * healthRatio; 1211 DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor); 1212 } 1213 1214 void DrawBoxedText(const char *text, int x, int y, int width, int height, float alignX, float alignY, Color textColor) 1215 { 1216 Vector2 textSize = MeasureTextEx(gameFontNormal, text, gameFontNormal.baseSize, 1); 1217 1218 DrawTextEx(gameFontNormal, text, (Vector2){ 1219 x + (width - textSize.x) * alignX, 1220 y + (height - textSize.y) * alignY 1221 }, gameFontNormal.baseSize, 1, textColor); 1222 } 1223 1224 int Button(const char *text, int x, int y, int width, int height, ButtonState *state) 1225 { 1226 Rectangle bounds = {x, y, width, height}; 1227 int isPressed = 0; 1228 int isSelected = state && state->isSelected; 1229 int isDisabled = state && state->isDisabled; 1230 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !isDisabled) 1231 { 1232 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) 1233 { 1234 isPressed = 1; 1235 } 1236 guiState.isBlocked = 1; 1237 DrawTextureNPatch(spriteSheet, isPressed ? uiButtonPressed : uiButtonHovered, 1238 bounds, Vector2Zero(), 0, WHITE); 1239 } 1240 else 1241 { 1242 DrawTextureNPatch(spriteSheet, isSelected ? uiButtonHovered : (isDisabled ? uiButtonDisabled : uiButtonNormal), 1243 bounds, Vector2Zero(), 0, WHITE); 1244 } 1245 Vector2 textSize = MeasureTextEx(gameFontNormal, text, gameFontNormal.baseSize, 1); 1246 Color textColor = isDisabled ? LIGHTGRAY : BLACK; 1247 DrawTextEx(gameFontNormal, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, gameFontNormal.baseSize, 1, textColor); 1248 return isPressed; 1249 } 1250 1251 //# Main game loop 1252 1253 void GameUpdate() 1254 { 1255 UpdateLevel(currentLevel); 1256 } 1257 1258 int main(void) 1259 { 1260 int screenWidth, screenHeight; 1261 GetPreferredSize(&screenWidth, &screenHeight); 1262 InitWindow(screenWidth, screenHeight, "Tower defense"); 1263 float gamespeed = 1.0f; 1264 int frameRate = 30; 1265 SetTargetFPS(30); 1266 1267 LoadAssets(); 1268 InitGame(); 1269 1270 float pause = 1.0f; 1271 1272 while (!WindowShouldClose()) 1273 { 1274 if (IsPaused()) { 1275 // canvas is not visible in browser - do nothing 1276 continue; 1277 } 1278 1279 if (IsKeyPressed(KEY_F)) 1280 { 1281 frameRate = (frameRate + 5) % 30; 1282 frameRate = frameRate < 10 ? 10 : frameRate; 1283 SetTargetFPS(frameRate); 1284 } 1285 1286 if (IsKeyPressed(KEY_T)) 1287 { 1288 gamespeed += 0.1f; 1289 if (gamespeed > 1.05f) gamespeed = 0.1f; 1290 } 1291 1292 if (IsKeyPressed(KEY_P)) 1293 { 1294 pause = pause > 0.5f ? 0.0f : 1.0f; 1295 } 1296 1297 float dt = GetFrameTime() * gamespeed * pause; 1298 // cap maximum delta time to 0.1 seconds to prevent large time steps 1299 if (dt > 0.1f) dt = 0.1f; 1300 gameTime.time += dt; 1301 gameTime.deltaTime = dt; 1302 gameTime.frameCount += 1; 1303 1304 float fixedTimeDiff = gameTime.time - gameTime.fixedTimeStart; 1305 gameTime.fixedStepCount = (uint8_t)(fixedTimeDiff / gameTime.fixedDeltaTime); 1306 1307 BeginDrawing(); 1308 ClearBackground((Color){0x4E, 0x63, 0x26, 0xFF}); 1309 1310 GameUpdate(); 1311 DrawLevel(currentLevel); 1312 1313 if (gamespeed != 1.0f) 1314 DrawText(TextFormat("Speed: %.1f", gamespeed), GetScreenWidth() - 180, 60, 20, WHITE); 1315 EndDrawing(); 1316 1317 gameTime.fixedTimeStart += gameTime.fixedStepCount * gameTime.fixedDeltaTime; 1318 } 1319 1320 CloseWindow(); 1321 1322 return 0; 1323 }
  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 
 21 #define ENEMY_TYPE_MINION 1
 22 #define ENEMY_TYPE_RUNNER 2
 23 #define ENEMY_TYPE_SHIELD 3
 24 #define ENEMY_TYPE_BOSS 4
 25 
 26 #define PARTICLE_MAX_COUNT 400
 27 #define PARTICLE_TYPE_NONE 0
 28 #define PARTICLE_TYPE_EXPLOSION 1
 29 
 30 typedef struct Particle
 31 {
 32   uint8_t particleType;
 33   float spawnTime;
 34   float lifetime;
 35   Vector3 position;
 36   Vector3 velocity;
 37   Vector3 scale;
 38 } Particle;
 39 
 40 #define TOWER_MAX_COUNT 400
 41 enum TowerType
 42 {
 43   TOWER_TYPE_NONE,
 44   TOWER_TYPE_BASE,
 45   TOWER_TYPE_ARCHER,
 46   TOWER_TYPE_BALLISTA,
 47   TOWER_TYPE_CATAPULT,
 48   TOWER_TYPE_WALL,
 49   TOWER_TYPE_COUNT
 50 };
 51 
 52 typedef struct HitEffectConfig
 53 {
 54   float damage;
 55   float areaDamageRadius;
 56   float pushbackPowerDistance;
 57 } HitEffectConfig;
 58 
 59 typedef struct TowerTypeConfig
 60 {
 61   const char *name;
 62   float cooldown;
 63   float range;
 64   float maxUpgradeRange;
 65   float projectileSpeed;
 66   
 67   uint8_t cost;
 68   uint8_t projectileType;
 69   uint16_t maxHealth;
 70 
 71   HitEffectConfig hitEffect;
 72 } TowerTypeConfig;
 73 
 74 #define TOWER_MAX_STAGE 10
 75 
 76 typedef struct TowerUpgradeState
 77 {
 78   uint8_t range;
 79   uint8_t damage;
 80   uint8_t speed;
 81 } TowerUpgradeState;
 82 
 83 typedef struct Tower
 84 {
 85   int16_t x, y;
 86   uint8_t towerType;
 87   TowerUpgradeState upgradeState;
 88   Vector2 lastTargetPosition;
 89   float cooldown;
 90   float damage;
 91   // alpha value for the range circle drawing
 92   float drawRangeAlpha;
 93 } Tower;
 94 
 95 typedef struct GameTime
 96 {
 97   float time;
 98   float deltaTime;
 99   uint32_t frameCount;
100 
101   float fixedDeltaTime;
102   // leaving the fixed time stepping to the update functions,
103   // we need to know the fixed time at the start of the frame
104   float fixedTimeStart;
105   // and the number of fixed steps that we have to make this frame
106   // The fixedTime is fixedTimeStart + n * fixedStepCount
107   uint8_t fixedStepCount;
108 } GameTime;
109 
110 typedef struct ButtonState {
111   char isSelected;
112   char isDisabled;
113 } ButtonState;
114 
115 typedef struct GUIState {
116   int isBlocked;
117 } GUIState;
118 
119 typedef enum LevelState
120 {
121   LEVEL_STATE_NONE,
122   LEVEL_STATE_BUILDING,
123   LEVEL_STATE_BUILDING_PLACEMENT,
124   LEVEL_STATE_BATTLE,
125   LEVEL_STATE_WON_WAVE,
126   LEVEL_STATE_LOST_WAVE,
127   LEVEL_STATE_WON_LEVEL,
128   LEVEL_STATE_RESET,
129 } LevelState;
130 
131 typedef struct EnemyWave {
132   uint8_t enemyType;
133   uint8_t wave;
134   uint16_t count;
135   float interval;
136   float delay;
137   Vector2 spawnPosition;
138 
139   uint16_t spawned;
140   float timeToSpawnNext;
141 } EnemyWave;
142 
143 #define ENEMY_MAX_WAVE_COUNT 10
144 
145 typedef enum PlacementPhase
146 {
147   PLACEMENT_PHASE_STARTING,
148   PLACEMENT_PHASE_MOVING,
149   PLACEMENT_PHASE_PLACING,
150 } PlacementPhase;
151 
152 typedef struct Level
153 {
154   int seed;
155   LevelState state;
156   LevelState nextState;
157   Camera3D camera;
158   int placementMode;
159   PlacementPhase placementPhase;
160   float placementTimer;
161   
162   int16_t placementX;
163   int16_t placementY;
164   int8_t placementContextMenuStatus;
165   int8_t placementContextMenuType;
166 
167   Vector2 placementTransitionPosition;
168   PhysicsPoint placementTowerSpring;
169 
170   int initialGold;
171   int playerGold;
172 
173   EnemyWave waves[ENEMY_MAX_WAVE_COUNT];
174   int currentWave;
175   float waveEndTimer;
176 } Level;
177 
178 typedef struct DeltaSrc
179 {
180   char x, y;
181 } DeltaSrc;
182 
183 typedef struct PathfindingMap
184 {
185   int width, height;
186   float scale;
187   float *distances;
188   long *towerIndex; 
189   DeltaSrc *deltaSrc;
190   float maxDistance;
191   Matrix toMapSpace;
192   Matrix toWorldSpace;
193 } PathfindingMap;
194 
195 // when we execute the pathfinding algorithm, we need to store the active nodes
196 // in a queue. Each node has a position, a distance from the start, and the
197 // position of the node that we came from.
198 typedef struct PathfindingNode
199 {
200   int16_t x, y, fromX, fromY;
201   float distance;
202 } PathfindingNode;
203 
204 typedef struct EnemyId
205 {
206   uint16_t index;
207   uint16_t generation;
208 } EnemyId;
209 
210 typedef struct EnemyClassConfig
211 {
212   float speed;
213   float health;
214   float shieldHealth;
215   float shieldDamageAbsorption;
216   float radius;
217   float maxAcceleration;
218   float requiredContactTime;
219   float explosionDamage;
220   float explosionRange;
221   float explosionPushbackPower;
222   int goldValue;
223 } EnemyClassConfig;
224 
225 typedef struct Enemy
226 {
227   int16_t currentX, currentY;
228   int16_t nextX, nextY;
229   Vector2 simPosition;
230   Vector2 simVelocity;
231   uint16_t generation;
232   float walkedDistance;
233   float startMovingTime;
234   float damage, futureDamage;
235   float shieldDamage;
236   float contactTime;
237   uint8_t enemyType;
238   uint8_t movePathCount;
239   Vector2 movePath[ENEMY_MAX_PATH_COUNT];
240 } Enemy;
241 
242 // a unit that uses sprites to be drawn
243 #define SPRITE_UNIT_ANIMATION_COUNT 6
244 #define SPRITE_UNIT_PHASE_WEAPON_IDLE 1
245 #define SPRITE_UNIT_PHASE_WEAPON_COOLDOWN 2
246 #define SPRITE_UNIT_PHASE_WEAPON_SHIELD 3
247 
248 typedef struct SpriteAnimation
249 {
250   Rectangle srcRect;
251   Vector2 offset;
252   uint8_t animationId;
253   uint8_t frameCount;
254   uint8_t frameWidth;
255   float frameDuration;
256 } SpriteAnimation;
257 
258 typedef struct SpriteUnit
259 {
260   float scale;
261   SpriteAnimation animations[SPRITE_UNIT_ANIMATION_COUNT];
262 } SpriteUnit;
263 
264 #define PROJECTILE_MAX_COUNT 1200
265 #define PROJECTILE_TYPE_NONE 0
266 #define PROJECTILE_TYPE_ARROW 1
267 #define PROJECTILE_TYPE_CATAPULT 2
268 #define PROJECTILE_TYPE_BALLISTA 3
269 
270 typedef struct Projectile
271 {
272   uint8_t projectileType;
273   float shootTime;
274   float arrivalTime;
275   float distance;
276   Vector3 position;
277   Vector3 target;
278   Vector3 directionNormal;
279   EnemyId targetEnemy;
280   HitEffectConfig hitEffectConfig;
281 } Projectile;
282 
283 //# Function declarations
284 int Button(const char *text, int x, int y, int width, int height, ButtonState *state);
285 int EnemyAddDamageRange(Vector2 position, float range, float damage);
286 int EnemyAddDamage(Enemy *enemy, float damage);
287 
288 //# Enemy functions
289 void EnemyInit();
290 void EnemyDraw();
291 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource);
292 void EnemyUpdate();
293 float EnemyGetCurrentMaxSpeed(Enemy *enemy);
294 float EnemyGetMaxHealth(Enemy *enemy);
295 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY);
296 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount);
297 EnemyId EnemyGetId(Enemy *enemy);
298 Enemy *EnemyTryResolve(EnemyId enemyId);
299 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY);
300 int EnemyAddDamage(Enemy *enemy, float damage);
301 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range);
302 int EnemyCount();
303 void EnemyDrawHealthbars(Camera3D camera);
304 
305 //# Tower functions
306 const char *TowerTypeGetName(uint8_t towerType); 307 int TowerTypeGetCosts(uint8_t towerType); 308 void TowerInit(); 309 void TowerUpdate(); 310 void TowerUpdateAllRangeFade(Tower *selectedTower, float fadeoutTarget); 311 void TowerDrawAll(); 312 void TowerDrawAllHealthBars(Camera3D camera); 313 void TowerDrawModel(Tower *tower); 314 void TowerDrawRange(Tower *tower, float alpha); 315 Tower *TowerGetByIndex(int index); 316 Tower *TowerGetByType(uint8_t towerType); 317 Tower *TowerGetAt(int16_t x, int16_t y); 318 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y); 319 float TowerGetMaxHealth(Tower *tower); 320 float TowerGetRange(Tower *tower); 321
322 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase); 323 324 //# Particles 325 void ParticleInit(); 326 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime); 327 void ParticleUpdate(); 328 void ParticleDraw(); 329 330 //# Projectiles 331 void ProjectileInit(); 332 void ProjectileDraw(); 333 void ProjectileUpdate(); 334 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig); 335 336 //# Pathfinding map 337 void PathfindingMapInit(int width, int height, Vector3 translate, float scale); 338 float PathFindingGetDistance(int mapX, int mapY); 339 Vector2 PathFindingGetGradient(Vector3 world); 340 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY); 341 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells); 342 void PathFindingMapDraw(); 343 344 //# UI 345 void DrawHealthBar(Camera3D camera, Vector3 position, Vector2 screenOffset, float healthRatio, Color barColor, float healthBarWidth); 346 347 //# Level 348 void DrawLevelGround(Level *level); 349 void DrawEnemyPath(Level *level, Color arrowColor); 350 351 //# variables 352 extern Level *currentLevel; 353 extern Enemy enemies[ENEMY_MAX_COUNT]; 354 extern int enemyCount; 355 extern EnemyClassConfig enemyClassConfigs[]; 356 357 extern GUIState guiState; 358 extern GameTime gameTime; 359 extern Tower towers[TOWER_MAX_COUNT]; 360 extern int towerCount; 361 362 extern Texture2D palette, spriteSheet; 363 364 #endif
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 
  4 static TowerTypeConfig towerTypeConfigs[TOWER_TYPE_COUNT] = {
  5     [TOWER_TYPE_BASE] = {
  6         .name = "Castle",
  7         .maxHealth = 10,
  8     },
  9     [TOWER_TYPE_ARCHER] = {
 10         .name = "Archer",
 11         .cooldown = 0.5f,
 12         .range = 3.0f,
 13         .maxUpgradeRange = 5.0f,
 14         .cost = 6,
 15         .maxHealth = 10,
 16         .projectileSpeed = 4.0f,
 17         .projectileType = PROJECTILE_TYPE_ARROW,
 18         .hitEffect = {
 19           .damage = 3.0f,
 20         }
 21     },
 22     [TOWER_TYPE_BALLISTA] = {
 23         .name = "Ballista",
 24         .cooldown = 1.5f,
 25         .range = 6.0f,
 26         .maxUpgradeRange = 8.0f,
 27         .cost = 9,
 28         .maxHealth = 10,
 29         .projectileSpeed = 10.0f,
 30         .projectileType = PROJECTILE_TYPE_BALLISTA,
 31         .hitEffect = {
 32           .damage = 8.0f,
 33           .pushbackPowerDistance = 0.25f,
 34         }
 35     },
 36     [TOWER_TYPE_CATAPULT] = {
 37         .name = "Catapult",
 38         .cooldown = 1.7f,
 39         .range = 5.0f,
 40         .maxUpgradeRange = 7.0f,
 41         .cost = 10,
 42         .maxHealth = 10,
 43         .projectileSpeed = 3.0f,
 44         .projectileType = PROJECTILE_TYPE_CATAPULT,
 45         .hitEffect = {
 46           .damage = 2.0f,
 47           .areaDamageRadius = 1.75f,
 48         }
 49     },
 50     [TOWER_TYPE_WALL] = {
 51         .name = "Wall",
 52         .cost = 2,
 53         .maxHealth = 10,
 54     },
 55 };
 56 
 57 Tower towers[TOWER_MAX_COUNT];
 58 int towerCount = 0;
 59 
 60 Model towerModels[TOWER_TYPE_COUNT];
 61 
 62 // definition of our archer unit
 63 SpriteUnit archerUnit = {
 64   .animations[0] = {
 65     .srcRect = {0, 0, 16, 16},
 66     .offset = {7, 1},
 67     .frameCount = 1,
 68     .frameDuration = 0.0f,
 69   },
 70   .animations[1] = {
 71     .animationId = SPRITE_UNIT_PHASE_WEAPON_COOLDOWN,
 72     .srcRect = {16, 0, 6, 16},
 73     .offset = {8, 0},
 74   },
 75   .animations[2] = {
 76     .animationId = SPRITE_UNIT_PHASE_WEAPON_IDLE,
 77     .srcRect = {22, 0, 11, 16},
 78     .offset = {10, 0},
 79   },
 80 };
 81 
 82 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase)
 83 {
 84   float unitScale = unit.scale == 0 ? 1.0f : unit.scale;
 85   float xScale = flip ? -1.0f : 1.0f;
 86   Camera3D camera = currentLevel->camera;
 87   float size = 0.5f * unitScale;
 88   // we want the sprite to face the camera, so we need to calculate the up vector
 89   Vector3 forward = Vector3Subtract(camera.target, camera.position);
 90   Vector3 up = {0, 1, 0};
 91   Vector3 right = Vector3CrossProduct(forward, up);
 92   up = Vector3Normalize(Vector3CrossProduct(right, forward));
 93   
 94   for (int i=0;i<SPRITE_UNIT_ANIMATION_COUNT;i++)
 95   {
 96     SpriteAnimation anim = unit.animations[i];
 97     if (anim.animationId != phase && anim.animationId != 0)
 98     {
 99       continue;
100     }
101     Rectangle srcRect = anim.srcRect;
102     if (anim.frameCount > 1)
103     {
104       int w = anim.frameWidth > 0 ? anim.frameWidth : srcRect.width;
105       srcRect.x += (int)(t / anim.frameDuration) % anim.frameCount * w;
106     }
107     Vector2 offset = { anim.offset.x / 16.0f * size, anim.offset.y / 16.0f * size * xScale };
108     Vector2 scale = { srcRect.width / 16.0f * size, srcRect.height / 16.0f * size };
109     
110     if (flip)
111     {
112       srcRect.x += srcRect.width;
113       srcRect.width = -srcRect.width;
114       offset.x = scale.x - offset.x;
115     }
116     DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
117     // move the sprite slightly towards the camera to avoid z-fighting
118     position = Vector3Add(position, Vector3Scale(Vector3Normalize(forward), -0.01f));  
119   }
120 }
121 
122 void TowerInit()
123 {
124   for (int i = 0; i < TOWER_MAX_COUNT; i++)
125   {
126     towers[i] = (Tower){0};
127   }
128   towerCount = 0;
129 
130   towerModels[TOWER_TYPE_BASE] = LoadModel("data/keep.glb");
131   towerModels[TOWER_TYPE_WALL] = LoadModel("data/wall-0000.glb");
132 
133   for (int i = 0; i < TOWER_TYPE_COUNT; i++)
134   {
135     if (towerModels[i].materials)
136     {
137       // assign the palette texture to the material of the model (0 is not used afaik)
138       towerModels[i].materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
139     }
140   }
141 }
142 
143 static void TowerGunUpdate(Tower *tower)
144 {
145   TowerTypeConfig config = towerTypeConfigs[tower->towerType];
146   if (tower->cooldown <= 0.0f)
147   {
148 Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, TowerGetRange(tower));
149 if (enemy) 150 { 151 tower->cooldown = config.cooldown; 152 // shoot the enemy; determine future position of the enemy 153 float bulletSpeed = config.projectileSpeed; 154 Vector2 velocity = enemy->simVelocity; 155 Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0); 156 Vector2 towerPosition = {tower->x, tower->y}; 157 float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed; 158 for (int i = 0; i < 8; i++) { 159 velocity = enemy->simVelocity; 160 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0); 161 float distance = Vector2Distance(towerPosition, futurePosition); 162 float eta2 = distance / bulletSpeed; 163 if (fabs(eta - eta2) < 0.01f) { 164 break; 165 } 166 eta = (eta2 + eta) * 0.5f; 167 } 168 169 ProjectileTryAdd(config.projectileType, enemy, 170 (Vector3){towerPosition.x, 1.33f, towerPosition.y}, 171 (Vector3){futurePosition.x, 0.25f, futurePosition.y}, 172 bulletSpeed, config.hitEffect); 173 enemy->futureDamage += config.hitEffect.damage; 174 tower->lastTargetPosition = futurePosition; 175 } 176 } 177 else 178 { 179 tower->cooldown -= gameTime.deltaTime; 180 } 181 } 182 183 Tower *TowerGetAt(int16_t x, int16_t y) 184 { 185 for (int i = 0; i < towerCount; i++) 186 { 187 if (towers[i].x == x && towers[i].y == y && towers[i].towerType != TOWER_TYPE_NONE) 188 { 189 return &towers[i]; 190 } 191 } 192 return 0; 193 } 194
195 Tower *TowerGetByIndex(int index)
196 { 197 if (index < 0 || index >= towerCount) 198 { 199 return 0; 200 } 201 return &towers[index]; 202 } 203 204 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y) 205 { 206 if (towerCount >= TOWER_MAX_COUNT) 207 { 208 return 0; 209 } 210 211 Tower *tower = TowerGetAt(x, y); 212 if (tower) 213 { 214 return 0; 215 } 216 217 tower = &towers[towerCount++]; 218 *tower = (Tower){ 219 .x = x, 220 .y = y, 221 .towerType = towerType, 222 .cooldown = 0.0f, 223 .damage = 0.0f, 224 }; 225 return tower; 226 } 227
228 Tower *TowerGetByType(uint8_t towerType)
229 { 230 for (int i = 0; i < towerCount; i++) 231 { 232 if (towers[i].towerType == towerType) 233 { 234 return &towers[i]; 235 } 236 } 237 return 0; 238 } 239
240 const char *TowerTypeGetName(uint8_t towerType)
241 { 242 return towerTypeConfigs[towerType].name; 243 } 244
245 int TowerTypeGetCosts(uint8_t towerType)
246 { 247 return towerTypeConfigs[towerType].cost; 248 } 249 250 float TowerGetMaxHealth(Tower *tower) 251 { 252 return towerTypeConfigs[tower->towerType].maxHealth; 253 } 254
255 float TowerGetRange(Tower *tower)
256 {
257 float range = towerTypeConfigs[tower->towerType].range; 258 float maxUpgradeRange = towerTypeConfigs[tower->towerType].maxUpgradeRange; 259 if (tower->upgradeState.range > 0)
260 {
261 range = Lerp(range, maxUpgradeRange, tower->upgradeState.range / (float)TOWER_MAX_STAGE);
262 } 263 return range; 264 } 265
266 void TowerUpdateAllRangeFade(Tower *selectedTower, float fadeoutTarget)
267 { 268 // animate fade in and fade out of range drawing using framerate independent lerp 269 float fadeLerp = 1.0f - powf(0.005f, gameTime.deltaTime); 270 for (int i = 0; i < TOWER_MAX_COUNT; i++) 271 {
272 Tower *fadingTower = TowerGetByIndex(i);
273 if (!fadingTower) 274 { 275 break; 276 } 277 float drawRangeTarget = fadingTower == selectedTower ? 1.0f : fadeoutTarget; 278 fadingTower->drawRangeAlpha = Lerp(fadingTower->drawRangeAlpha, drawRangeTarget, fadeLerp); 279 } 280 } 281
282 void TowerDrawRange(Tower *tower, float alpha)
283 { 284 Color ringColor = (Color){255, 200, 100, 255}; 285 const int rings = 4; 286 const float radiusOffset = 0.5f; 287 const float animationSpeed = 2.0f; 288 float animation = 1.0f - fmodf(gameTime.time * animationSpeed, 1.0f);
289 float radius = TowerGetRange(tower);
290 // base circle
291 DrawCircle3D((Vector3){tower->x, 0.01f, tower->y}, radius, (Vector3){1, 0, 0}, 90,
292 Fade(ringColor, alpha)); 293 294 for (int i = 1; i < rings; i++) 295 { 296 float t = ((float)i + animation) / (float)rings; 297 float r = Lerp(radius, radius - radiusOffset, t * t); 298 float a = 1.0f - ((float)i + animation - 1) / (float) (rings - 1); 299 if (i == 1) 300 { 301 // fade out the outermost ring 302 a = animation; 303 } 304 a *= alpha; 305
306 DrawCircle3D((Vector3){tower->x, 0.01f, tower->y}, r, (Vector3){1, 0, 0}, 90,
307 Fade(ringColor, a)); 308 } 309 } 310
311 void TowerDrawModel(Tower *tower)
312 {
313 if (tower->towerType == TOWER_TYPE_NONE)
314 { 315 return; 316 } 317
318 if (tower->drawRangeAlpha > 2.0f/256.0f)
319 {
320 TowerDrawRange(tower, tower->drawRangeAlpha);
321 } 322
323 switch (tower->towerType)
324 { 325 case TOWER_TYPE_ARCHER: 326 {
327 Vector2 screenPosTower = GetWorldToScreen((Vector3){tower->x, 0.0f, tower->y}, currentLevel->camera); 328 Vector2 screenPosTarget = GetWorldToScreen((Vector3){tower->lastTargetPosition.x, 0.0f, tower->lastTargetPosition.y}, currentLevel->camera); 329 DrawModel(towerModels[TOWER_TYPE_WALL], (Vector3){tower->x, 0.0f, tower->y}, 1.0f, WHITE); 330 DrawSpriteUnit(archerUnit, (Vector3){tower->x, 1.0f, tower->y}, 0, screenPosTarget.x > screenPosTower.x, 331 tower->cooldown > 0.2f ? SPRITE_UNIT_PHASE_WEAPON_COOLDOWN : SPRITE_UNIT_PHASE_WEAPON_IDLE);
332 } 333 break; 334 case TOWER_TYPE_BALLISTA:
335 DrawCube((Vector3){tower->x, 0.5f, tower->y}, 1.0f, 1.0f, 1.0f, BROWN);
336 break; 337 case TOWER_TYPE_CATAPULT:
338 DrawCube((Vector3){tower->x, 0.5f, tower->y}, 1.0f, 1.0f, 1.0f, DARKGRAY);
339 break; 340 default:
341 if (towerModels[tower->towerType].materials)
342 {
343 DrawModel(towerModels[tower->towerType], (Vector3){tower->x, 0.0f, tower->y}, 1.0f, WHITE);
344 } else {
345 DrawCube((Vector3){tower->x, 0.5f, tower->y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY);
346 } 347 break; 348 } 349 } 350
351 void TowerDrawAll()
352 { 353 for (int i = 0; i < towerCount; i++) 354 {
355 TowerDrawModel(&towers[i]);
356 } 357 } 358 359 void TowerUpdate() 360 { 361 for (int i = 0; i < towerCount; i++) 362 { 363 Tower *tower = &towers[i]; 364 switch (tower->towerType) 365 { 366 case TOWER_TYPE_CATAPULT: 367 case TOWER_TYPE_BALLISTA: 368 case TOWER_TYPE_ARCHER: 369 TowerGunUpdate(tower); 370 break; 371 } 372 } 373 } 374
375 void TowerDrawAllHealthBars(Camera3D camera)
376 { 377 for (int i = 0; i < towerCount; i++) 378 { 379 Tower *tower = &towers[i]; 380 if (tower->towerType == TOWER_TYPE_NONE || tower->damage <= 0.0f) 381 { 382 continue; 383 } 384 385 Vector3 position = (Vector3){tower->x, 0.5f, tower->y}; 386 float maxHealth = TowerGetMaxHealth(tower); 387 float health = maxHealth - tower->damage; 388 float healthRatio = health / maxHealth; 389 390 DrawHealthBar(camera, position, Vector2Zero(), healthRatio, GREEN, 35.0f); 391 } 392 }
  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 #include <stdlib.h>
  4 #include <math.h>
  5 #include <rlgl.h>
  6 
  7 EnemyClassConfig enemyClassConfigs[] = {
  8     [ENEMY_TYPE_MINION] = {
  9       .health = 10.0f, 
 10       .speed = 0.6f, 
 11       .radius = 0.25f, 
 12       .maxAcceleration = 1.0f,
 13       .explosionDamage = 1.0f,
 14       .requiredContactTime = 0.5f,
 15       .explosionRange = 1.0f,
 16       .explosionPushbackPower = 0.25f,
 17       .goldValue = 1,
 18     },
 19     [ENEMY_TYPE_RUNNER] = {
 20       .health = 5.0f, 
 21       .speed = 1.0f, 
 22       .radius = 0.25f, 
 23       .maxAcceleration = 2.0f,
 24       .explosionDamage = 1.0f,
 25       .requiredContactTime = 0.5f,
 26       .explosionRange = 1.0f,
 27       .explosionPushbackPower = 0.25f,
 28       .goldValue = 2,
 29     },
 30     [ENEMY_TYPE_SHIELD] = {
 31       .health = 8.0f, 
 32       .speed = 0.5f, 
 33       .radius = 0.25f, 
 34       .maxAcceleration = 1.0f,
 35       .explosionDamage = 2.0f,
 36       .requiredContactTime = 0.5f,
 37       .explosionRange = 1.0f,
 38       .explosionPushbackPower = 0.25f,
 39       .goldValue = 3,
 40       .shieldDamageAbsorption = 4.0f,
 41       .shieldHealth = 25.0f,
 42     },
 43     [ENEMY_TYPE_BOSS] = {
 44       .health = 50.0f, 
 45       .speed = 0.4f, 
 46       .radius = 0.25f, 
 47       .maxAcceleration = 1.0f,
 48       .explosionDamage = 5.0f,
 49       .requiredContactTime = 0.5f,
 50       .explosionRange = 1.0f,
 51       .explosionPushbackPower = 0.25f,
 52       .goldValue = 10,
 53     },
 54 };
 55 
 56 Enemy enemies[ENEMY_MAX_COUNT];
 57 int enemyCount = 0;
 58 
 59 SpriteUnit enemySprites[] = {
 60     [ENEMY_TYPE_MINION] = {
 61       .animations[0] = {
 62         .srcRect = {0, 17, 16, 15},
 63         .offset = {8.0f, 0.0f},
 64         .frameCount = 6,
 65         .frameDuration = 0.1f,
 66       },
 67       .animations[1] = {
 68         .srcRect = {1, 33, 15, 14},
 69         .offset = {7.0f, 0.0f},
 70         .frameCount = 6,
 71         .frameWidth = 16,
 72         .frameDuration = 0.1f,
 73       },
 74     },
 75     [ENEMY_TYPE_RUNNER] = {
 76       .scale = 0.75f,
 77       .animations[0] = {
 78         .srcRect = {0, 17, 16, 15},
 79         .offset = {8.0f, 0.0f},
 80         .frameCount = 6,
 81         .frameDuration = 0.1f,
 82       },
 83     },
 84     [ENEMY_TYPE_SHIELD] = {
 85       .animations[0] = {
 86         .srcRect = {0, 17, 16, 15},
 87         .offset = {8.0f, 0.0f},
 88         .frameCount = 6,
 89         .frameDuration = 0.1f,
 90       },
 91       .animations[1] = {
 92         .animationId = SPRITE_UNIT_PHASE_WEAPON_SHIELD,
 93         .srcRect = {99, 17, 10, 11},
 94         .offset = {7.0f, 0.0f},
 95       },
 96     },
 97     [ENEMY_TYPE_BOSS] = {
 98       .scale = 1.5f,
 99       .animations[0] = {
100         .srcRect = {0, 17, 16, 15},
101         .offset = {8.0f, 0.0f},
102         .frameCount = 6,
103         .frameDuration = 0.1f,
104       },
105       .animations[1] = {
106         .srcRect = {97, 29, 14, 7},
107         .offset = {7.0f, -9.0f},
108       },
109     },
110 };
111 
112 void EnemyInit()
113 {
114   for (int i = 0; i < ENEMY_MAX_COUNT; i++)
115   {
116     enemies[i] = (Enemy){0};
117   }
118   enemyCount = 0;
119 }
120 
121 float EnemyGetCurrentMaxSpeed(Enemy *enemy)
122 {
123   return enemyClassConfigs[enemy->enemyType].speed;
124 }
125 
126 float EnemyGetMaxHealth(Enemy *enemy)
127 {
128   return enemyClassConfigs[enemy->enemyType].health;
129 }
130 
131 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY)
132 {
133   int16_t castleX = 0;
134   int16_t castleY = 0;
135   int16_t dx = castleX - currentX;
136   int16_t dy = castleY - currentY;
137   if (dx == 0 && dy == 0)
138   {
139     *nextX = currentX;
140     *nextY = currentY;
141     return 1;
142   }
143   Vector2 gradient = PathFindingGetGradient((Vector3){currentX, 0, currentY});
144 
145   if (gradient.x == 0 && gradient.y == 0)
146   {
147     *nextX = currentX;
148     *nextY = currentY;
149     return 1;
150   }
151 
152   if (fabsf(gradient.x) > fabsf(gradient.y))
153   {
154     *nextX = currentX + (int16_t)(gradient.x > 0.0f ? 1 : -1);
155     *nextY = currentY;
156     return 0;
157   }
158   *nextX = currentX;
159   *nextY = currentY + (int16_t)(gradient.y > 0.0f ? 1 : -1);
160   return 0;
161 }
162 
163 
164 // this function predicts the movement of the unit for the next deltaT seconds
165 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount)
166 {
167   const float pointReachedDistance = 0.25f;
168   const float pointReachedDistance2 = pointReachedDistance * pointReachedDistance;
169   const float maxSimStepTime = 0.015625f;
170   
171   float maxAcceleration = enemyClassConfigs[enemy->enemyType].maxAcceleration;
172   float maxSpeed = EnemyGetCurrentMaxSpeed(enemy);
173   int16_t nextX = enemy->nextX;
174   int16_t nextY = enemy->nextY;
175   Vector2 position = enemy->simPosition;
176   int passedCount = 0;
177   for (float t = 0.0f; t < deltaT; t += maxSimStepTime)
178   {
179     float stepTime = fminf(deltaT - t, maxSimStepTime);
180     Vector2 target = (Vector2){nextX, nextY};
181     float speed = Vector2Length(*velocity);
182     // draw the target position for debugging
183     //DrawCubeWires((Vector3){target.x, 0.2f, target.y}, 0.1f, 0.4f, 0.1f, RED);
184     Vector2 lookForwardPos = Vector2Add(position, Vector2Scale(*velocity, speed));
185     if (Vector2DistanceSqr(target, lookForwardPos) <= pointReachedDistance2)
186     {
187       // we reached the target position, let's move to the next waypoint
188       EnemyGetNextPosition(nextX, nextY, &nextX, &nextY);
189       target = (Vector2){nextX, nextY};
190       // track how many waypoints we passed
191       passedCount++;
192     }
193     
194     // acceleration towards the target
195     Vector2 unitDirection = Vector2Normalize(Vector2Subtract(target, lookForwardPos));
196     Vector2 acceleration = Vector2Scale(unitDirection, maxAcceleration * stepTime);
197     *velocity = Vector2Add(*velocity, acceleration);
198 
199     // limit the speed to the maximum speed
200     if (speed > maxSpeed)
201     {
202       *velocity = Vector2Scale(*velocity, maxSpeed / speed);
203     }
204 
205     // move the enemy
206     position = Vector2Add(position, Vector2Scale(*velocity, stepTime));
207   }
208 
209   if (waypointPassedCount)
210   {
211     (*waypointPassedCount) = passedCount;
212   }
213 
214   return position;
215 }
216 
217 void EnemyDraw()
218 {
219   rlDrawRenderBatchActive();
220   rlDisableDepthMask();
221   for (int i = 0; i < enemyCount; i++)
222   {
223     Enemy enemy = enemies[i];
224     if (enemy.enemyType == ENEMY_TYPE_NONE)
225     {
226       continue;
227     }
228 
229     Vector2 position = EnemyGetPosition(&enemy, gameTime.time - enemy.startMovingTime, &enemy.simVelocity, 0);
230     
231     // don't draw any trails for now; might replace this with footprints later
232     // if (enemy.movePathCount > 0)
233     // {
234     //   Vector3 p = {enemy.movePath[0].x, 0.2f, enemy.movePath[0].y};
235     //   DrawLine3D(p, (Vector3){position.x, 0.2f, position.y}, GREEN);
236     // }
237     // for (int j = 1; j < enemy.movePathCount; j++)
238     // {
239     //   Vector3 p = {enemy.movePath[j - 1].x, 0.2f, enemy.movePath[j - 1].y};
240     //   Vector3 q = {enemy.movePath[j].x, 0.2f, enemy.movePath[j].y};
241     //   DrawLine3D(p, q, GREEN);
242     // }
243 
244     float shieldHealth = enemyClassConfigs[enemy.enemyType].shieldHealth;
245     int phase = 0;
246     if (shieldHealth > 0 && enemy.shieldDamage < shieldHealth)
247     {
248       phase = SPRITE_UNIT_PHASE_WEAPON_SHIELD;
249     }
250 
251     switch (enemy.enemyType)
252     {
253     case ENEMY_TYPE_MINION:
254     case ENEMY_TYPE_RUNNER:
255     case ENEMY_TYPE_SHIELD:
256     case ENEMY_TYPE_BOSS:
257       DrawSpriteUnit(enemySprites[enemy.enemyType], (Vector3){position.x, 0.0f, position.y}, 
258         enemy.walkedDistance, 0, phase);
259       break;
260     }
261   }
262   rlDrawRenderBatchActive();
263   rlEnableDepthMask();
264 }
265 
266 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource)
267 {
268   // damage the tower
269   float explosionDamge = enemyClassConfigs[enemy->enemyType].explosionDamage;
270   float explosionRange = enemyClassConfigs[enemy->enemyType].explosionRange;
271   float explosionPushbackPower = enemyClassConfigs[enemy->enemyType].explosionPushbackPower;
272   float explosionRange2 = explosionRange * explosionRange;
273   tower->damage += enemyClassConfigs[enemy->enemyType].explosionDamage;
274   // explode the enemy
275   if (tower->damage >= TowerGetMaxHealth(tower))
276   {
277     tower->towerType = TOWER_TYPE_NONE;
278   }
279 
280   ParticleAdd(PARTICLE_TYPE_EXPLOSION, 
281     explosionSource, 
282     (Vector3){0, 0.1f, 0}, (Vector3){1.0f, 1.0f, 1.0f}, 1.0f);
283 
284   enemy->enemyType = ENEMY_TYPE_NONE;
285 
286   // push back enemies & dealing damage
287   for (int i = 0; i < enemyCount; i++)
288   {
289     Enemy *other = &enemies[i];
290     if (other->enemyType == ENEMY_TYPE_NONE)
291     {
292       continue;
293     }
294     float distanceSqr = Vector2DistanceSqr(enemy->simPosition, other->simPosition);
295     if (distanceSqr > 0 && distanceSqr < explosionRange2)
296     {
297       Vector2 direction = Vector2Normalize(Vector2Subtract(other->simPosition, enemy->simPosition));
298       other->simPosition = Vector2Add(other->simPosition, Vector2Scale(direction, explosionPushbackPower));
299       EnemyAddDamage(other, explosionDamge);
300     }
301   }
302 }
303 
304 void EnemyUpdate()
305 {
306   const float castleX = 0;
307   const float castleY = 0;
308   const float maxPathDistance2 = 0.25f * 0.25f;
309   
310   for (int i = 0; i < enemyCount; i++)
311   {
312     Enemy *enemy = &enemies[i];
313     if (enemy->enemyType == ENEMY_TYPE_NONE)
314     {
315       continue;
316     }
317 
318     int waypointPassedCount = 0;
319     Vector2 prevPosition = enemy->simPosition;
320     enemy->simPosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &enemy->simVelocity, &waypointPassedCount);
321     enemy->startMovingTime = gameTime.time;
322     enemy->walkedDistance += Vector2Distance(prevPosition, enemy->simPosition);
323     // track path of unit
324     if (enemy->movePathCount == 0 || Vector2DistanceSqr(enemy->simPosition, enemy->movePath[0]) > maxPathDistance2)
325     {
326       for (int j = ENEMY_MAX_PATH_COUNT - 1; j > 0; j--)
327       {
328         enemy->movePath[j] = enemy->movePath[j - 1];
329       }
330       enemy->movePath[0] = enemy->simPosition;
331       if (++enemy->movePathCount > ENEMY_MAX_PATH_COUNT)
332       {
333         enemy->movePathCount = ENEMY_MAX_PATH_COUNT;
334       }
335     }
336 
337     if (waypointPassedCount > 0)
338     {
339       enemy->currentX = enemy->nextX;
340       enemy->currentY = enemy->nextY;
341       if (EnemyGetNextPosition(enemy->currentX, enemy->currentY, &enemy->nextX, &enemy->nextY) &&
342         Vector2DistanceSqr(enemy->simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
343       {
344         // enemy reached the castle; remove it
345         enemy->enemyType = ENEMY_TYPE_NONE;
346         continue;
347       }
348     }
349   }
350 
351   // handle collisions between enemies
352   for (int i = 0; i < enemyCount - 1; i++)
353   {
354     Enemy *enemyA = &enemies[i];
355     if (enemyA->enemyType == ENEMY_TYPE_NONE)
356     {
357       continue;
358     }
359     for (int j = i + 1; j < enemyCount; j++)
360     {
361       Enemy *enemyB = &enemies[j];
362       if (enemyB->enemyType == ENEMY_TYPE_NONE)
363       {
364         continue;
365       }
366       float distanceSqr = Vector2DistanceSqr(enemyA->simPosition, enemyB->simPosition);
367       float radiusA = enemyClassConfigs[enemyA->enemyType].radius;
368       float radiusB = enemyClassConfigs[enemyB->enemyType].radius;
369       float radiusSum = radiusA + radiusB;
370       if (distanceSqr < radiusSum * radiusSum && distanceSqr > 0.001f)
371       {
372         // collision
373         float distance = sqrtf(distanceSqr);
374         float overlap = radiusSum - distance;
375         // move the enemies apart, but softly; if we have a clog of enemies,
376         // moving them perfectly apart can cause them to jitter
377         float positionCorrection = overlap / 5.0f;
378         Vector2 direction = (Vector2){
379             (enemyB->simPosition.x - enemyA->simPosition.x) / distance * positionCorrection,
380             (enemyB->simPosition.y - enemyA->simPosition.y) / distance * positionCorrection};
381         enemyA->simPosition = Vector2Subtract(enemyA->simPosition, direction);
382         enemyB->simPosition = Vector2Add(enemyB->simPosition, direction);
383       }
384     }
385   }
386 
387   // handle collisions between enemies and towers
388   for (int i = 0; i < enemyCount; i++)
389   {
390     Enemy *enemy = &enemies[i];
391     if (enemy->enemyType == ENEMY_TYPE_NONE)
392     {
393       continue;
394     }
395     enemy->contactTime -= gameTime.deltaTime;
396     if (enemy->contactTime < 0.0f)
397     {
398       enemy->contactTime = 0.0f;
399     }
400 
401     float enemyRadius = enemyClassConfigs[enemy->enemyType].radius;
402     // linear search over towers; could be optimized by using path finding tower map,
403     // but for now, we keep it simple
404     for (int j = 0; j < towerCount; j++)
405     {
406       Tower *tower = &towers[j];
407       if (tower->towerType == TOWER_TYPE_NONE)
408       {
409         continue;
410       }
411       float distanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){tower->x, tower->y});
412       float combinedRadius = enemyRadius + 0.708; // sqrt(0.5^2 + 0.5^2), corner-center distance of square with side length 1
413       if (distanceSqr > combinedRadius * combinedRadius)
414       {
415         continue;
416       }
417       // potential collision; square / circle intersection
418       float dx = tower->x - enemy->simPosition.x;
419       float dy = tower->y - enemy->simPosition.y;
420       float absDx = fabsf(dx);
421       float absDy = fabsf(dy);
422       Vector3 contactPoint = {0};
423       if (absDx <= 0.5f && absDx <= absDy) {
424         // vertical collision; push the enemy out horizontally
425         float overlap = enemyRadius + 0.5f - absDy;
426         if (overlap < 0.0f)
427         {
428           continue;
429         }
430         float direction = dy > 0.0f ? -1.0f : 1.0f;
431         enemy->simPosition.y += direction * overlap;
432         contactPoint = (Vector3){enemy->simPosition.x, 0.2f, tower->y + direction * 0.5f};
433       }
434       else if (absDy <= 0.5f && absDy <= absDx)
435       {
436         // horizontal collision; push the enemy out vertically
437         float overlap = enemyRadius + 0.5f - absDx;
438         if (overlap < 0.0f)
439         {
440           continue;
441         }
442         float direction = dx > 0.0f ? -1.0f : 1.0f;
443         enemy->simPosition.x += direction * overlap;
444         contactPoint = (Vector3){tower->x + direction * 0.5f, 0.2f, enemy->simPosition.y};
445       }
446       else
447       {
448         // possible collision with a corner
449         float cornerDX = dx > 0.0f ? -0.5f : 0.5f;
450         float cornerDY = dy > 0.0f ? -0.5f : 0.5f;
451         float cornerX = tower->x + cornerDX;
452         float cornerY = tower->y + cornerDY;
453         float cornerDistanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){cornerX, cornerY});
454         if (cornerDistanceSqr > enemyRadius * enemyRadius)
455         {
456           continue;
457         }
458         // push the enemy out along the diagonal
459         float cornerDistance = sqrtf(cornerDistanceSqr);
460         float overlap = enemyRadius - cornerDistance;
461         float directionX = cornerDistance > 0.0f ? (cornerX - enemy->simPosition.x) / cornerDistance : -cornerDX;
462         float directionY = cornerDistance > 0.0f ? (cornerY - enemy->simPosition.y) / cornerDistance : -cornerDY;
463         enemy->simPosition.x -= directionX * overlap;
464         enemy->simPosition.y -= directionY * overlap;
465         contactPoint = (Vector3){cornerX, 0.2f, cornerY};
466       }
467 
468       if (enemyClassConfigs[enemy->enemyType].explosionDamage > 0.0f)
469       {
470         enemy->contactTime += gameTime.deltaTime * 2.0f; // * 2 to undo the subtraction above
471         if (enemy->contactTime >= enemyClassConfigs[enemy->enemyType].requiredContactTime)
472         {
473           EnemyTriggerExplode(enemy, tower, contactPoint);
474         }
475       }
476     }
477   }
478 }
479 
480 EnemyId EnemyGetId(Enemy *enemy)
481 {
482   return (EnemyId){enemy - enemies, enemy->generation};
483 }
484 
485 Enemy *EnemyTryResolve(EnemyId enemyId)
486 {
487   if (enemyId.index >= ENEMY_MAX_COUNT)
488   {
489     return 0;
490   }
491   Enemy *enemy = &enemies[enemyId.index];
492   if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE)
493   {
494     return 0;
495   }
496   return enemy;
497 }
498 
499 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY)
500 {
501   Enemy *spawn = 0;
502   for (int i = 0; i < enemyCount; i++)
503   {
504     Enemy *enemy = &enemies[i];
505     if (enemy->enemyType == ENEMY_TYPE_NONE)
506     {
507       spawn = enemy;
508       break;
509     }
510   }
511 
512   if (enemyCount < ENEMY_MAX_COUNT && !spawn)
513   {
514     spawn = &enemies[enemyCount++];
515   }
516 
517   if (spawn)
518   {
519     *spawn = (Enemy){
520       .currentX = currentX,
521       .currentY = currentY,
522       .nextX = currentX,
523       .nextY = currentY,
524       .simPosition = (Vector2){currentX, currentY},
525       .simVelocity = (Vector2){0, 0},
526       .enemyType = enemyType,
527       .startMovingTime = gameTime.time,
528       .movePathCount = 0,
529       .walkedDistance = 0.0f,
530       .shieldDamage = 0.0f,
531       .damage = 0.0f,
532       .futureDamage = 0.0f,
533       .contactTime = 0.0f,
534       .generation = spawn->generation + 1,
535     };
536   }
537 
538   return spawn;
539 }
540 
541 int EnemyAddDamageRange(Vector2 position, float range, float damage)
542 {
543   int count = 0;
544   float range2 = range * range;
545   for (int i = 0; i < enemyCount; i++)
546   {
547     Enemy *enemy = &enemies[i];
548     if (enemy->enemyType == ENEMY_TYPE_NONE)
549     {
550       continue;
551     }
552     float distance2 = Vector2DistanceSqr(position, enemy->simPosition);
553     if (distance2 <= range2)
554     {
555       EnemyAddDamage(enemy, damage);
556       count++;
557     }
558   }
559   return count;
560 }
561 
562 int EnemyAddDamage(Enemy *enemy, float damage)
563 {
564   float shieldHealth = enemyClassConfigs[enemy->enemyType].shieldHealth;
565   if (shieldHealth > 0.0f && enemy->shieldDamage < shieldHealth)
566   {
567     float shieldDamageAbsorption = enemyClassConfigs[enemy->enemyType].shieldDamageAbsorption;
568     float shieldDamage = fminf(fminf(shieldDamageAbsorption, damage), shieldHealth - enemy->shieldDamage);
569     enemy->shieldDamage += shieldDamage;
570     damage -= shieldDamage;
571   }
572   enemy->damage += damage;
573   if (enemy->damage >= EnemyGetMaxHealth(enemy))
574   {
575     currentLevel->playerGold += enemyClassConfigs[enemy->enemyType].goldValue;
576     enemy->enemyType = ENEMY_TYPE_NONE;
577     return 1;
578   }
579 
580   return 0;
581 }
582 
583 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
584 {
585   int16_t castleX = 0;
586   int16_t castleY = 0;
587   Enemy* closest = 0;
588   int16_t closestDistance = 0;
589   float range2 = range * range;
590   for (int i = 0; i < enemyCount; i++)
591   {
592     Enemy* enemy = &enemies[i];
593     if (enemy->enemyType == ENEMY_TYPE_NONE)
594     {
595       continue;
596     }
597     float maxHealth = EnemyGetMaxHealth(enemy) + enemyClassConfigs[enemy->enemyType].shieldHealth;
598     if (enemy->futureDamage >= maxHealth)
599     {
600       // ignore enemies that will die soon
601       continue;
602     }
603     int16_t dx = castleX - enemy->currentX;
604     int16_t dy = castleY - enemy->currentY;
605     int16_t distance = abs(dx) + abs(dy);
606     if (!closest || distance < closestDistance)
607     {
608       float tdx = towerX - enemy->currentX;
609       float tdy = towerY - enemy->currentY;
610       float tdistance2 = tdx * tdx + tdy * tdy;
611       if (tdistance2 <= range2)
612       {
613         closest = enemy;
614         closestDistance = distance;
615       }
616     }
617   }
618   return closest;
619 }
620 
621 int EnemyCount()
622 {
623   int count = 0;
624   for (int i = 0; i < enemyCount; i++)
625   {
626     if (enemies[i].enemyType != ENEMY_TYPE_NONE)
627     {
628       count++;
629     }
630   }
631   return count;
632 }
633 
634 void EnemyDrawHealthbars(Camera3D camera)
635 {
636   for (int i = 0; i < enemyCount; i++)
637   {
638     Enemy *enemy = &enemies[i];
639     
640     float maxShieldHealth = enemyClassConfigs[enemy->enemyType].shieldHealth;
641     if (maxShieldHealth > 0.0f && enemy->shieldDamage < maxShieldHealth && enemy->shieldDamage > 0.0f)
642     {
643       float shieldHealth = maxShieldHealth - enemy->shieldDamage;
644       float shieldHealthRatio = shieldHealth / maxShieldHealth;
645       Vector3 position = (Vector3){enemy->simPosition.x, 0.5f, enemy->simPosition.y};
646       DrawHealthBar(camera, position, (Vector2) {.y = -4}, shieldHealthRatio, BLUE, 20.0f);
647     }
648 
649     if (enemy->enemyType == ENEMY_TYPE_NONE || enemy->damage == 0.0f)
650     {
651       continue;
652     }
653     Vector3 position = (Vector3){enemy->simPosition.x, 0.5f, enemy->simPosition.y};
654     float maxHealth = EnemyGetMaxHealth(enemy);
655     float health = maxHealth - enemy->damage;
656     float healthRatio = health / maxHealth;
657     
658     DrawHealthBar(camera, position, Vector2Zero(), healthRatio, GREEN, 15.0f);
659   }
660 }
  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

Now that the cleanups are done, we can add the speed and damage upgrades. To test the upgrades, we bump the amount of money the player has to 500. This way we can easily test the upgrades.

  • 💾
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 #include <rlgl.h>
  4 #include <stdlib.h>
  5 #include <math.h>
  6 #include <string.h>
  7 
  8 //# Variables
  9 Font gameFontNormal = {0};
 10 GUIState guiState = {0};
 11 GameTime gameTime = {
 12   .fixedDeltaTime = 1.0f / 60.0f,
 13 };
 14 
 15 Model floorTileAModel = {0};
 16 Model floorTileBModel = {0};
 17 Model treeModel[2] = {0};
 18 Model firTreeModel[2] = {0};
 19 Model rockModels[5] = {0};
 20 Model grassPatchModel[1] = {0};
 21 
 22 Model pathArrowModel = {0};
 23 Model greenArrowModel = {0};
 24 
 25 Texture2D palette, spriteSheet;
 26 
 27 NPatchInfo uiPanelPatch = {
 28   .layout = NPATCH_NINE_PATCH,
 29   .source = {145, 1, 46, 46},
 30   .top = 18, .bottom = 18,
 31   .left = 16, .right = 16
 32 };
 33 NPatchInfo uiButtonNormal = {
 34   .layout = NPATCH_NINE_PATCH,
 35   .source = {193, 1, 32, 20},
 36   .top = 7, .bottom = 7,
 37   .left = 10, .right = 10
 38 };
 39 NPatchInfo uiButtonDisabled = {
 40   .layout = NPATCH_NINE_PATCH,
 41   .source = {193, 22, 32, 20},
 42   .top = 7, .bottom = 7,
 43   .left = 10, .right = 10
 44 };
 45 NPatchInfo uiButtonHovered = {
 46   .layout = NPATCH_NINE_PATCH,
 47   .source = {193, 43, 32, 20},
 48   .top = 7, .bottom = 7,
 49   .left = 10, .right = 10
 50 };
 51 NPatchInfo uiButtonPressed = {
 52   .layout = NPATCH_NINE_PATCH,
 53   .source = {193, 64, 32, 20},
 54   .top = 7, .bottom = 7,
 55   .left = 10, .right = 10
 56 };
 57 Rectangle uiDiamondMarker = {145, 48, 15, 15};
 58 
 59 Level levels[] = {
 60   [0] = {
 61     .state = LEVEL_STATE_BUILDING,
62 .initialGold = 500,
63 .waves[0] = { 64 .enemyType = ENEMY_TYPE_SHIELD, 65 .wave = 0, 66 .count = 1, 67 .interval = 2.5f, 68 .delay = 1.0f, 69 .spawnPosition = {2, 6}, 70 }, 71 .waves[1] = { 72 .enemyType = ENEMY_TYPE_RUNNER, 73 .wave = 0, 74 .count = 5, 75 .interval = 0.5f, 76 .delay = 1.0f, 77 .spawnPosition = {-2, 6}, 78 }, 79 .waves[2] = { 80 .enemyType = ENEMY_TYPE_SHIELD, 81 .wave = 1, 82 .count = 20, 83 .interval = 1.5f, 84 .delay = 1.0f, 85 .spawnPosition = {0, 6}, 86 }, 87 .waves[3] = { 88 .enemyType = ENEMY_TYPE_MINION, 89 .wave = 2, 90 .count = 30, 91 .interval = 1.2f, 92 .delay = 1.0f, 93 .spawnPosition = {2, 6}, 94 }, 95 .waves[4] = { 96 .enemyType = ENEMY_TYPE_BOSS, 97 .wave = 2, 98 .count = 2, 99 .interval = 5.0f, 100 .delay = 2.0f, 101 .spawnPosition = {-2, 4}, 102 } 103 }, 104 }; 105 106 Level *currentLevel = levels; 107 108 void DrawBoxedText(const char *text, int x, int y, int width, int height, float alignX, float alignY, Color textColor); 109 110 void DrawTitleText(const char *text, int anchorX, float alignX, Color color) 111 { 112 int textWidth = MeasureTextEx(gameFontNormal, text, gameFontNormal.baseSize, 0).x; 113 int panelWidth = textWidth + 40; 114 int posX = anchorX - panelWidth * alignX; 115 int textOffset = 20; 116 DrawTextureNPatch(spriteSheet, uiPanelPatch, (Rectangle){ posX, -10, panelWidth, 40}, Vector2Zero(), 0, WHITE); 117 DrawTextEx(gameFontNormal, text, (Vector2){posX + textOffset + 1, 6 + 1}, gameFontNormal.baseSize, 0, BLACK); 118 DrawTextEx(gameFontNormal, text, (Vector2){posX + textOffset, 6}, gameFontNormal.baseSize, 0, color); 119 } 120 121 void DrawTitle(const char *text) 122 { 123 DrawTitleText(text, GetScreenWidth() / 2, 0.5f, WHITE); 124 } 125 126 //# Game 127 128 static Model LoadGLBModel(char *filename) 129 { 130 Model model = LoadModel(TextFormat("data/%s.glb",filename)); 131 for (int i = 0; i < model.materialCount; i++) 132 { 133 model.materials[i].maps[MATERIAL_MAP_DIFFUSE].texture = palette; 134 } 135 return model; 136 } 137 138 void LoadAssets() 139 { 140 // load a sprite sheet that contains all units 141 spriteSheet = LoadTexture("data/spritesheet.png"); 142 SetTextureFilter(spriteSheet, TEXTURE_FILTER_BILINEAR); 143 144 // we'll use a palette texture to colorize the all buildings and environment art 145 palette = LoadTexture("data/palette.png"); 146 // The texture uses gradients on very small space, so we'll enable bilinear filtering 147 SetTextureFilter(palette, TEXTURE_FILTER_BILINEAR); 148 149 gameFontNormal = LoadFont("data/alagard.png"); 150 151 floorTileAModel = LoadGLBModel("floor-tile-a"); 152 floorTileBModel = LoadGLBModel("floor-tile-b"); 153 treeModel[0] = LoadGLBModel("leaftree-large-1-a"); 154 treeModel[1] = LoadGLBModel("leaftree-large-1-b"); 155 firTreeModel[0] = LoadGLBModel("firtree-1-a"); 156 firTreeModel[1] = LoadGLBModel("firtree-1-b"); 157 rockModels[0] = LoadGLBModel("rock-1"); 158 rockModels[1] = LoadGLBModel("rock-2"); 159 rockModels[2] = LoadGLBModel("rock-3"); 160 rockModels[3] = LoadGLBModel("rock-4"); 161 rockModels[4] = LoadGLBModel("rock-5"); 162 grassPatchModel[0] = LoadGLBModel("grass-patch-1"); 163 164 pathArrowModel = LoadGLBModel("direction-arrow-x"); 165 greenArrowModel = LoadGLBModel("green-arrow"); 166 } 167 168 void InitLevel(Level *level) 169 { 170 level->seed = (int)(GetTime() * 100.0f); 171 172 TowerInit(); 173 EnemyInit(); 174 ProjectileInit(); 175 ParticleInit(); 176 TowerTryAdd(TOWER_TYPE_BASE, 0, 0); 177 178 level->placementMode = 0; 179 level->state = LEVEL_STATE_BUILDING; 180 level->nextState = LEVEL_STATE_NONE; 181 level->playerGold = level->initialGold; 182 level->currentWave = 0; 183 level->placementX = -1; 184 level->placementY = 0; 185 186 Camera *camera = &level->camera; 187 camera->position = (Vector3){4.0f, 8.0f, 8.0f}; 188 camera->target = (Vector3){0.0f, 0.0f, 0.0f}; 189 camera->up = (Vector3){0.0f, 1.0f, 0.0f}; 190 camera->fovy = 11.5f; 191 camera->projection = CAMERA_ORTHOGRAPHIC; 192 } 193 194 void DrawLevelHud(Level *level) 195 { 196 const char *text = TextFormat("Gold: %d", level->playerGold); 197 DrawTitleText(text, GetScreenWidth() - 10, 1.0f, YELLOW); 198 } 199 200 void DrawLevelReportLostWave(Level *level) 201 { 202 BeginMode3D(level->camera);
203 DrawLevelGround(level); 204 TowerUpdateAllRangeFade(0, 0.0f);
205 TowerDrawAll(); 206 EnemyDraw(); 207 ProjectileDraw(); 208 ParticleDraw(); 209 guiState.isBlocked = 0; 210 EndMode3D(); 211 212 TowerDrawAllHealthBars(level->camera); 213 214 DrawTitle("Wave lost"); 215 216 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 217 { 218 level->nextState = LEVEL_STATE_RESET; 219 } 220 } 221 222 int HasLevelNextWave(Level *level) 223 { 224 for (int i = 0; i < 10; i++) 225 { 226 EnemyWave *wave = &level->waves[i]; 227 if (wave->wave == level->currentWave) 228 { 229 return 1; 230 } 231 } 232 return 0; 233 } 234 235 void DrawLevelReportWonWave(Level *level) 236 { 237 BeginMode3D(level->camera);
238 DrawLevelGround(level); 239 TowerUpdateAllRangeFade(0, 0.0f);
240 TowerDrawAll(); 241 EnemyDraw(); 242 ProjectileDraw(); 243 ParticleDraw(); 244 guiState.isBlocked = 0; 245 EndMode3D(); 246 247 TowerDrawAllHealthBars(level->camera); 248 249 DrawTitle("Wave won"); 250 251 252 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 253 { 254 level->nextState = LEVEL_STATE_RESET; 255 } 256 257 if (HasLevelNextWave(level)) 258 { 259 if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0)) 260 { 261 level->nextState = LEVEL_STATE_BUILDING; 262 } 263 } 264 else { 265 if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0)) 266 { 267 level->nextState = LEVEL_STATE_WON_LEVEL; 268 } 269 } 270 } 271 272 int DrawBuildingBuildButton(Level *level, int x, int y, int width, int height, uint8_t towerType, const char *name) 273 { 274 static ButtonState buttonStates[8] = {0}; 275 int cost = TowerTypeGetCosts(towerType); 276 const char *text = TextFormat("%s: %d", name, cost); 277 buttonStates[towerType].isSelected = level->placementMode == towerType; 278 buttonStates[towerType].isDisabled = level->playerGold < cost; 279 if (Button(text, x, y, width, height, &buttonStates[towerType])) 280 { 281 level->placementMode = buttonStates[towerType].isSelected ? 0 : towerType; 282 level->nextState = LEVEL_STATE_BUILDING_PLACEMENT; 283 return 1; 284 } 285 return 0; 286 } 287 288 float GetRandomFloat(float min, float max) 289 { 290 int random = GetRandomValue(0, 0xfffffff); 291 return ((float)random / (float)0xfffffff) * (max - min) + min; 292 } 293 294 void DrawLevelGround(Level *level) 295 { 296 // draw checkerboard ground pattern 297 for (int x = -5; x <= 5; x += 1) 298 { 299 for (int y = -5; y <= 5; y += 1) 300 { 301 Model *model = (x + y) % 2 == 0 ? &floorTileAModel : &floorTileBModel; 302 DrawModel(*model, (Vector3){x, 0.0f, y}, 1.0f, WHITE); 303 } 304 } 305 306 int oldSeed = GetRandomValue(0, 0xfffffff); 307 SetRandomSeed(level->seed); 308 // increase probability for trees via duplicated entries 309 Model borderModels[64]; 310 int maxRockCount = GetRandomValue(2, 6); 311 int maxTreeCount = GetRandomValue(10, 20); 312 int maxFirTreeCount = GetRandomValue(5, 10); 313 int maxLeafTreeCount = maxTreeCount - maxFirTreeCount; 314 int grassPatchCount = GetRandomValue(5, 30); 315 316 int modelCount = 0; 317 for (int i = 0; i < maxRockCount && modelCount < 63; i++) 318 { 319 borderModels[modelCount++] = rockModels[GetRandomValue(0, 5)]; 320 } 321 for (int i = 0; i < maxLeafTreeCount && modelCount < 63; i++) 322 { 323 borderModels[modelCount++] = treeModel[GetRandomValue(0, 1)]; 324 } 325 for (int i = 0; i < maxFirTreeCount && modelCount < 63; i++) 326 { 327 borderModels[modelCount++] = firTreeModel[GetRandomValue(0, 1)]; 328 } 329 for (int i = 0; i < grassPatchCount && modelCount < 63; i++) 330 { 331 borderModels[modelCount++] = grassPatchModel[0]; 332 } 333 334 // draw some objects around the border of the map 335 Vector3 up = {0, 1, 0}; 336 // a pseudo random number generator to get the same result every time 337 const float wiggle = 0.75f; 338 const int layerCount = 3; 339 for (int layer = 0; layer <= layerCount; layer++) 340 { 341 int layerPos = 6 + layer; 342 Model *selectedModels = borderModels; 343 int selectedModelCount = modelCount; 344 if (layer == 0) 345 { 346 selectedModels = grassPatchModel; 347 selectedModelCount = 1; 348 } 349 for (int x = -6 - layer; x <= 6 + layer; x += 1) 350 { 351 DrawModelEx(selectedModels[GetRandomValue(0, selectedModelCount - 1)], 352 (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, -layerPos + GetRandomFloat(0.0f, wiggle)}, 353 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE); 354 DrawModelEx(selectedModels[GetRandomValue(0, selectedModelCount - 1)], 355 (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, layerPos + GetRandomFloat(0.0f, wiggle)}, 356 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE); 357 } 358 359 for (int z = -5 - layer; z <= 5 + layer; z += 1) 360 { 361 DrawModelEx(selectedModels[GetRandomValue(0, selectedModelCount - 1)], 362 (Vector3){-layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)}, 363 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE); 364 DrawModelEx(selectedModels[GetRandomValue(0, selectedModelCount - 1)], 365 (Vector3){layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)}, 366 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE); 367 } 368 } 369 370 SetRandomSeed(oldSeed); 371 } 372 373 void DrawEnemyPath(Level *level, Color arrowColor) 374 { 375 const int castleX = 0, castleY = 0; 376 const int maxWaypointCount = 200; 377 const float timeStep = 1.0f; 378 Vector3 arrowScale = {0.75f, 0.75f, 0.75f}; 379 380 // we start with a time offset to simulate the path, 381 // this way the arrows are animated in a forward moving direction 382 // The time is wrapped around the time step to get a smooth animation 383 float timeOffset = fmodf(GetTime(), timeStep); 384 385 for (int i = 0; i < ENEMY_MAX_WAVE_COUNT; i++) 386 { 387 EnemyWave *wave = &level->waves[i]; 388 if (wave->wave != level->currentWave) 389 { 390 continue; 391 } 392 393 // use this dummy enemy to simulate the path 394 Enemy dummy = { 395 .enemyType = ENEMY_TYPE_MINION, 396 .simPosition = (Vector2){wave->spawnPosition.x, wave->spawnPosition.y}, 397 .nextX = wave->spawnPosition.x, 398 .nextY = wave->spawnPosition.y, 399 .currentX = wave->spawnPosition.x, 400 .currentY = wave->spawnPosition.y, 401 }; 402 403 float deltaTime = timeOffset; 404 for (int j = 0; j < maxWaypointCount; j++) 405 { 406 int waypointPassedCount = 0; 407 Vector2 pos = EnemyGetPosition(&dummy, deltaTime, &dummy.simVelocity, &waypointPassedCount); 408 // after the initial variable starting offset, we use a fixed time step 409 deltaTime = timeStep; 410 dummy.simPosition = pos; 411 412 // Update the dummy's position just like we do in the regular enemy update loop 413 for (int k = 0; k < waypointPassedCount; k++) 414 { 415 dummy.currentX = dummy.nextX; 416 dummy.currentY = dummy.nextY; 417 if (EnemyGetNextPosition(dummy.currentX, dummy.currentY, &dummy.nextX, &dummy.nextY) && 418 Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f) 419 { 420 break; 421 } 422 } 423 if (Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f) 424 { 425 break; 426 } 427 428 // get the angle we need to rotate the arrow model. The velocity is just fine for this. 429 float angle = atan2f(dummy.simVelocity.x, dummy.simVelocity.y) * RAD2DEG - 90.0f; 430 DrawModelEx(pathArrowModel, (Vector3){pos.x, 0.15f, pos.y}, (Vector3){0, 1, 0}, angle, arrowScale, arrowColor); 431 } 432 } 433 } 434 435 void DrawEnemyPaths(Level *level) 436 { 437 // disable depth testing for the path arrows 438 // flush the 3D batch to draw the arrows on top of everything 439 rlDrawRenderBatchActive(); 440 rlDisableDepthTest(); 441 DrawEnemyPath(level, (Color){64, 64, 64, 160}); 442 443 rlDrawRenderBatchActive(); 444 rlEnableDepthTest(); 445 DrawEnemyPath(level, WHITE); 446 } 447 448 static void SimulateTowerPlacementBehavior(Level *level, float towerFloatHeight, float mapX, float mapY) 449 { 450 float dt = gameTime.fixedDeltaTime; 451 // smooth transition for the placement position using exponential decay 452 const float lambda = 15.0f; 453 float factor = 1.0f - expf(-lambda * dt); 454 455 float damping = 0.5f; 456 float springStiffness = 300.0f; 457 float springDecay = 95.0f; 458 float minHeight = 0.35f; 459 460 if (level->placementPhase == PLACEMENT_PHASE_STARTING) 461 { 462 damping = 1.0f; 463 springDecay = 90.0f; 464 springStiffness = 100.0f; 465 minHeight = 0.70f; 466 } 467 468 for (int i = 0; i < gameTime.fixedStepCount; i++) 469 { 470 level->placementTransitionPosition = 471 Vector2Lerp( 472 level->placementTransitionPosition, 473 (Vector2){mapX, mapY}, factor); 474 475 // draw the spring position for debugging the spring simulation 476 // first step: stiff spring, no simulation 477 Vector3 worldPlacementPosition = (Vector3){ 478 level->placementTransitionPosition.x, towerFloatHeight, level->placementTransitionPosition.y}; 479 Vector3 springTargetPosition = (Vector3){ 480 worldPlacementPosition.x, 1.0f + towerFloatHeight, worldPlacementPosition.z}; 481 // consider the current velocity to predict the future position in order to dampen 482 // the spring simulation. Longer prediction times will result in more damping 483 Vector3 predictedSpringPosition = Vector3Add(level->placementTowerSpring.position, 484 Vector3Scale(level->placementTowerSpring.velocity, dt * damping)); 485 Vector3 springPointDelta = Vector3Subtract(springTargetPosition, predictedSpringPosition); 486 Vector3 velocityChange = Vector3Scale(springPointDelta, dt * springStiffness); 487 // decay velocity of the upright forcing spring 488 // This force acts like a 2nd spring that pulls the tip upright into the air above the 489 // base position 490 level->placementTowerSpring.velocity = Vector3Scale(level->placementTowerSpring.velocity, 1.0f - expf(-springDecay * dt)); 491 level->placementTowerSpring.velocity = Vector3Add(level->placementTowerSpring.velocity, velocityChange); 492 493 // calculate length of the spring to calculate the force to apply to the placementTowerSpring position 494 // we use a simple spring model with a rest length of 1.0f 495 Vector3 springDelta = Vector3Subtract(worldPlacementPosition, level->placementTowerSpring.position); 496 float springLength = Vector3Length(springDelta); 497 float springForce = (springLength - 1.0f) * springStiffness; 498 Vector3 springForceVector = Vector3Normalize(springDelta); 499 springForceVector = Vector3Scale(springForceVector, springForce); 500 level->placementTowerSpring.velocity = Vector3Add(level->placementTowerSpring.velocity, 501 Vector3Scale(springForceVector, dt)); 502 503 level->placementTowerSpring.position = Vector3Add(level->placementTowerSpring.position, 504 Vector3Scale(level->placementTowerSpring.velocity, dt)); 505 if (level->placementTowerSpring.position.y < minHeight + towerFloatHeight) 506 { 507 level->placementTowerSpring.velocity.y *= -1.0f; 508 level->placementTowerSpring.position.y = fmaxf(level->placementTowerSpring.position.y, minHeight + towerFloatHeight); 509 } 510 } 511 } 512 513 void DrawLevelBuildingPlacementState(Level *level) 514 { 515 const float placementDuration = 0.5f; 516 517 level->placementTimer += gameTime.deltaTime; 518 if (level->placementTimer > 1.0f && level->placementPhase == PLACEMENT_PHASE_STARTING) 519 { 520 level->placementPhase = PLACEMENT_PHASE_MOVING; 521 level->placementTimer = 0.0f; 522 } 523 524 BeginMode3D(level->camera); 525 DrawLevelGround(level); 526 527 int blockedCellCount = 0; 528 Vector2 blockedCells[1]; 529 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera); 530 float planeDistance = ray.position.y / -ray.direction.y; 531 float planeX = ray.direction.x * planeDistance + ray.position.x; 532 float planeY = ray.direction.z * planeDistance + ray.position.z; 533 int16_t mapX = (int16_t)floorf(planeX + 0.5f); 534 int16_t mapY = (int16_t)floorf(planeY + 0.5f); 535 if (level->placementPhase == PLACEMENT_PHASE_MOVING && 536 level->placementMode && !guiState.isBlocked && 537 mapX >= -5 && mapX <= 5 && mapY >= -5 && mapY <= 5 && IsMouseButtonDown(MOUSE_LEFT_BUTTON)) 538 { 539 level->placementX = mapX; 540 level->placementY = mapY; 541 } 542 else 543 { 544 mapX = level->placementX; 545 mapY = level->placementY; 546 } 547 blockedCells[blockedCellCount++] = (Vector2){mapX, mapY}; 548 PathFindingMapUpdate(blockedCellCount, blockedCells);
549 550 TowerUpdateAllRangeFade(0, 0.0f);
551 TowerDrawAll(); 552 EnemyDraw(); 553 ProjectileDraw(); 554 ParticleDraw(); 555 DrawEnemyPaths(level); 556 557 // let the tower float up and down. Consider this height in the spring simulation as well 558 float towerFloatHeight = sinf(gameTime.time * 4.0f) * 0.2f + 0.3f; 559 560 if (level->placementPhase == PLACEMENT_PHASE_PLACING) 561 { 562 // The bouncing spring needs a bit of outro time to look nice and complete. 563 // So we scale the time so that the first 2/3rd of the placing phase handles the motion 564 // and the last 1/3rd is the outro physics (bouncing) 565 float t = fminf(1.0f, level->placementTimer / placementDuration * 1.5f); 566 // let towerFloatHeight describe parabola curve, starting at towerFloatHeight and ending at 0 567 float linearBlendHeight = (1.0f - t) * towerFloatHeight; 568 float parabola = (1.0f - ((t - 0.5f) * (t - 0.5f) * 4.0f)) * 2.0f; 569 towerFloatHeight = linearBlendHeight + parabola; 570 } 571 572 SimulateTowerPlacementBehavior(level, towerFloatHeight, mapX, mapY); 573 574 rlPushMatrix(); 575 rlTranslatef(level->placementTransitionPosition.x, 0, level->placementTransitionPosition.y); 576 577 // calculate x and z rotation to align the model with the spring 578 Vector3 position = {level->placementTransitionPosition.x, towerFloatHeight, level->placementTransitionPosition.y}; 579 Vector3 towerUp = Vector3Subtract(level->placementTowerSpring.position, position); 580 Vector3 rotationAxis = Vector3CrossProduct(towerUp, (Vector3){0, 1, 0}); 581 float angle = acosf(Vector3DotProduct(towerUp, (Vector3){0, 1, 0}) / Vector3Length(towerUp)) * RAD2DEG; 582 float springLength = Vector3Length(towerUp); 583 float towerStretch = fminf(fmaxf(springLength, 0.5f), 4.5f); 584 float towerSquash = 1.0f / towerStretch; 585 586 Tower dummy = { 587 .towerType = level->placementMode, 588 }; 589 590 float rangeAlpha = fminf(1.0f, level->placementTimer / placementDuration); 591 if (level->placementPhase == PLACEMENT_PHASE_PLACING) 592 { 593 rangeAlpha = 1.0f - rangeAlpha; 594 } 595 else if (level->placementPhase == PLACEMENT_PHASE_MOVING) 596 { 597 rangeAlpha = 1.0f; 598 } 599 600 TowerDrawRange(&dummy, rangeAlpha); 601 602 rlPushMatrix(); 603 rlTranslatef(0.0f, towerFloatHeight, 0.0f); 604 605 rlRotatef(-angle, rotationAxis.x, rotationAxis.y, rotationAxis.z); 606 rlScalef(towerSquash, towerStretch, towerSquash); 607 TowerDrawModel(&dummy); 608 rlPopMatrix(); 609 610 611 // draw a shadow for the tower 612 float umbrasize = 0.8 + sqrtf(towerFloatHeight); 613 DrawCube((Vector3){0.0f, 0.05f, 0.0f}, umbrasize, 0.0f, umbrasize, (Color){0, 0, 0, 32}); 614 DrawCube((Vector3){0.0f, 0.075f, 0.0f}, 0.85f, 0.0f, 0.85f, (Color){0, 0, 0, 64}); 615 616 617 float bounce = sinf(gameTime.time * 8.0f) * 0.5f + 0.5f; 618 float offset = fmaxf(0.0f, bounce - 0.4f) * 0.35f + 0.7f; 619 float squeeze = -fminf(0.0f, bounce - 0.4f) * 0.7f + 0.7f; 620 float stretch = fmaxf(0.0f, bounce - 0.3f) * 0.5f + 0.8f; 621 622 DrawModelEx(greenArrowModel, (Vector3){ offset, 0.0f, 0.0f}, (Vector3){0, 1, 0}, 90, (Vector3){squeeze, 1.0f, stretch}, WHITE); 623 DrawModelEx(greenArrowModel, (Vector3){-offset, 0.0f, 0.0f}, (Vector3){0, 1, 0}, 270, (Vector3){squeeze, 1.0f, stretch}, WHITE); 624 DrawModelEx(greenArrowModel, (Vector3){ 0.0f, 0.0f, offset}, (Vector3){0, 1, 0}, 0, (Vector3){squeeze, 1.0f, stretch}, WHITE); 625 DrawModelEx(greenArrowModel, (Vector3){ 0.0f, 0.0f, -offset}, (Vector3){0, 1, 0}, 180, (Vector3){squeeze, 1.0f, stretch}, WHITE); 626 rlPopMatrix(); 627 628 guiState.isBlocked = 0; 629 630 EndMode3D(); 631 632 TowerDrawAllHealthBars(level->camera); 633 634 if (level->placementPhase == PLACEMENT_PHASE_PLACING) 635 { 636 if (level->placementTimer > placementDuration) 637 { 638 Tower *tower = TowerTryAdd(level->placementMode, mapX, mapY); 639 // testing repairing 640 tower->damage = 2.5f; 641 level->playerGold -= TowerTypeGetCosts(level->placementMode); 642 level->nextState = LEVEL_STATE_BUILDING; 643 level->placementMode = TOWER_TYPE_NONE; 644 } 645 } 646 else 647 { 648 if (Button("Cancel", 20, GetScreenHeight() - 40, 160, 30, 0)) 649 { 650 level->nextState = LEVEL_STATE_BUILDING; 651 level->placementMode = TOWER_TYPE_NONE; 652 TraceLog(LOG_INFO, "Cancel building"); 653 } 654 655 if (TowerGetAt(mapX, mapY) == 0 && Button("Build", GetScreenWidth() - 180, GetScreenHeight() - 40, 160, 30, 0)) 656 { 657 level->placementPhase = PLACEMENT_PHASE_PLACING; 658 level->placementTimer = 0.0f; 659 } 660 } 661 } 662 663 enum ContextMenuType 664 { 665 CONTEXT_MENU_TYPE_MAIN, 666 CONTEXT_MENU_TYPE_SELL_CONFIRM, 667 CONTEXT_MENU_TYPE_UPGRADE, 668 }; 669 670 enum UpgradeType 671 { 672 UPGRADE_TYPE_SPEED, 673 UPGRADE_TYPE_DAMAGE, 674 UPGRADE_TYPE_RANGE, 675 }; 676 677 typedef struct ContextMenuArgs 678 { 679 void *data; 680 uint8_t uint8; 681 int32_t int32; 682 Tower *tower; 683 } ContextMenuArgs; 684 685 int OnContextMenuBuild(Level *level, ContextMenuArgs *data) 686 { 687 uint8_t towerType = data->uint8; 688 level->placementMode = towerType; 689 level->nextState = LEVEL_STATE_BUILDING_PLACEMENT; 690 return 1; 691 } 692 693 int OnContextMenuSellConfirm(Level *level, ContextMenuArgs *data) 694 { 695 Tower *tower = data->tower; 696 int gold = data->int32; 697 level->playerGold += gold; 698 tower->towerType = TOWER_TYPE_NONE; 699 return 1; 700 } 701 702 int OnContextMenuSellCancel(Level *level, ContextMenuArgs *data) 703 { 704 return 1; 705 } 706 707 int OnContextMenuSell(Level *level, ContextMenuArgs *data) 708 { 709 level->placementContextMenuType = CONTEXT_MENU_TYPE_SELL_CONFIRM; 710 return 0; 711 } 712 713 int OnContextMenuDoUpgrade(Level *level, ContextMenuArgs *data) 714 { 715 Tower *tower = data->tower; 716 switch (data->uint8) 717 { 718 case UPGRADE_TYPE_SPEED: tower->upgradeState.speed++; break; 719 case UPGRADE_TYPE_DAMAGE: tower->upgradeState.damage++; break; 720 case UPGRADE_TYPE_RANGE: tower->upgradeState.range++; break; 721 } 722 level->playerGold -= data->int32; 723 return 0; 724 } 725 726 int OnContextMenuUpgrade(Level *level, ContextMenuArgs *data) 727 { 728 level->placementContextMenuType = CONTEXT_MENU_TYPE_UPGRADE; 729 return 0; 730 } 731 732 int OnContextMenuRepair(Level *level, ContextMenuArgs *data) 733 { 734 Tower *tower = data->tower; 735 if (level->playerGold >= 1) 736 { 737 level->playerGold -= 1; 738 tower->damage = fmaxf(0.0f, tower->damage - 1.0f); 739 } 740 return tower->damage == 0.0f; 741 } 742 743 typedef struct ContextMenuItem 744 { 745 uint8_t index; 746 char text[24]; 747 float alignX; 748 int (*action)(Level*, ContextMenuArgs*); 749 void *data; 750 ContextMenuArgs args; 751 ButtonState buttonState; 752 } ContextMenuItem; 753 754 ContextMenuItem ContextMenuItemText(uint8_t index, const char *text, float alignX) 755 { 756 ContextMenuItem item = {.index = index, .alignX = alignX}; 757 strncpy(item.text, text, 24); 758 return item; 759 } 760 761 ContextMenuItem ContextMenuItemButton(uint8_t index, const char *text, int (*action)(Level*, ContextMenuArgs*), ContextMenuArgs args) 762 { 763 ContextMenuItem item = {.index = index, .action = action, .args = args}; 764 strncpy(item.text, text, 24); 765 return item; 766 } 767 768 int DrawContextMenu(Level *level, Vector2 anchorLow, Vector2 anchorHigh, int width, ContextMenuItem *menus) 769 { 770 const int itemHeight = 28; 771 const int itemSpacing = 1; 772 const int padding = 8; 773 int itemCount = 0; 774 for (int i = 0; menus[i].text[0]; i++) 775 { 776 itemCount = itemCount > menus[i].index ? itemCount : menus[i].index; 777 } 778 779 Rectangle contextMenu = {0, 0, width, 780 (itemHeight + itemSpacing) * (itemCount + 1) + itemSpacing + padding * 2}; 781 782 Vector2 anchor = anchorHigh.y - 30 > contextMenu.height ? anchorHigh : anchorLow; 783 float anchorPivotY = anchorHigh.y - 30 > contextMenu.height ? 1.0f : 0.0f; 784 785 contextMenu.x = anchor.x - contextMenu.width * 0.5f; 786 contextMenu.y = anchor.y - contextMenu.height * anchorPivotY; 787 contextMenu.x = fmaxf(0, fminf(GetScreenWidth() - contextMenu.width, contextMenu.x)); 788 contextMenu.y = fmaxf(0, fminf(GetScreenHeight() - contextMenu.height, contextMenu.y)); 789 790 DrawTextureNPatch(spriteSheet, uiPanelPatch, contextMenu, Vector2Zero(), 0, WHITE); 791 DrawTextureRec(spriteSheet, uiDiamondMarker, (Vector2){anchor.x - uiDiamondMarker.width / 2, anchor.y - uiDiamondMarker.height / 2}, WHITE); 792 const int itemX = contextMenu.x + itemSpacing; 793 const int itemWidth = contextMenu.width - itemSpacing * 2; 794 #define ITEM_Y(idx) (contextMenu.y + itemSpacing + (itemHeight + itemSpacing) * idx + padding) 795 #define ITEM_RECT(idx, marginLR) itemX + (marginLR) + padding, ITEM_Y(idx), itemWidth - (marginLR) * 2 - padding * 2, itemHeight 796 int status = 0; 797 for (int i = 0; menus[i].text[0]; i++) 798 { 799 if (menus[i].action) 800 { 801 if (Button(menus[i].text, ITEM_RECT(menus[i].index, 0), &menus[i].buttonState)) 802 { 803 status = menus[i].action(level, &menus[i].args); 804 } 805 } 806 else 807 { 808 DrawBoxedText(menus[i].text, ITEM_RECT(menus[i].index, 0), menus[i].alignX, 0.5f, WHITE); 809 } 810 } 811 812 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && !CheckCollisionPointRec(GetMousePosition(), contextMenu)) 813 { 814 return 1; 815 } 816 817 return status; 818 } 819 820 void DrawLevelBuildingStateMainContextMenu(Level *level, Tower *tower, int32_t sellValue, float hp, float maxHitpoints, Vector2 anchorLow, Vector2 anchorHigh) 821 { 822 ContextMenuItem menu[12] = {0}; 823 int menuCount = 0; 824 int menuIndex = 0; 825 if (tower) 826 { 827 828 if (tower) { 829 menu[menuCount++] = ContextMenuItemText(menuIndex++, TowerTypeGetName(tower->towerType), 0.5f); 830 } 831 832 // two texts, same line 833 menu[menuCount++] = ContextMenuItemText(menuIndex, "HP:", 0.0f); 834 menu[menuCount++] = ContextMenuItemText(menuIndex++, TextFormat("%.1f / %.1f", hp, maxHitpoints), 1.0f); 835 836 if (tower->towerType != TOWER_TYPE_BASE) 837 { 838 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "Upgrade", OnContextMenuUpgrade, 839 (ContextMenuArgs){.tower = tower}); 840 } 841 842 if (tower->towerType != TOWER_TYPE_BASE) 843 { 844 845 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "Sell", OnContextMenuSell, 846 (ContextMenuArgs){.tower = tower, .int32 = sellValue}); 847 } 848 if (tower->towerType != TOWER_TYPE_BASE && tower->damage > 0 && level->playerGold >= 1) 849 { 850 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "Repair 1 (1G)", OnContextMenuRepair, 851 (ContextMenuArgs){.tower = tower}); 852 } 853 } 854 else 855 { 856 menu[menuCount] = ContextMenuItemButton(menuIndex++, 857 TextFormat("Wall: %dG", TowerTypeGetCosts(TOWER_TYPE_WALL)), 858 OnContextMenuBuild, (ContextMenuArgs){ .uint8 = (uint8_t) TOWER_TYPE_WALL}); 859 menu[menuCount++].buttonState.isDisabled = level->playerGold < TowerTypeGetCosts(TOWER_TYPE_WALL); 860 861 menu[menuCount] = ContextMenuItemButton(menuIndex++, 862 TextFormat("Archer: %dG", TowerTypeGetCosts(TOWER_TYPE_ARCHER)), 863 OnContextMenuBuild, (ContextMenuArgs){ .uint8 = (uint8_t) TOWER_TYPE_ARCHER}); 864 menu[menuCount++].buttonState.isDisabled = level->playerGold < TowerTypeGetCosts(TOWER_TYPE_ARCHER); 865 866 menu[menuCount] = ContextMenuItemButton(menuIndex++, 867 TextFormat("Ballista: %dG", TowerTypeGetCosts(TOWER_TYPE_BALLISTA)), 868 OnContextMenuBuild, (ContextMenuArgs){ .uint8 = (uint8_t) TOWER_TYPE_BALLISTA}); 869 menu[menuCount++].buttonState.isDisabled = level->playerGold < TowerTypeGetCosts(TOWER_TYPE_BALLISTA); 870 871 menu[menuCount] = ContextMenuItemButton(menuIndex++, 872 TextFormat("Catapult: %dG", TowerTypeGetCosts(TOWER_TYPE_CATAPULT)), 873 OnContextMenuBuild, (ContextMenuArgs){ .uint8 = (uint8_t) TOWER_TYPE_CATAPULT}); 874 menu[menuCount++].buttonState.isDisabled = level->playerGold < TowerTypeGetCosts(TOWER_TYPE_CATAPULT); 875 } 876 877 if (DrawContextMenu(level, anchorLow, anchorHigh, 150, menu)) 878 { 879 level->placementContextMenuStatus = -1; 880 } 881 } 882 883 void DrawLevelBuildingState(Level *level) 884 { 885 // when the context menu is not active, we update the placement position 886 if (level->placementContextMenuStatus == 0) 887 { 888 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera); 889 float hitDistance = ray.position.y / -ray.direction.y; 890 float hitX = ray.direction.x * hitDistance + ray.position.x; 891 float hitY = ray.direction.z * hitDistance + ray.position.z; 892 level->placementX = (int)floorf(hitX + 0.5f); 893 level->placementY = (int)floorf(hitY + 0.5f); 894 } 895 896 // the currently hovered/selected tower 897 Tower *tower = TowerGetAt(level->placementX, level->placementY); 898 // show the range of the tower when hovering/selecting it 899 TowerUpdateAllRangeFade(tower, 0.0f); 900 901 BeginMode3D(level->camera); 902 DrawLevelGround(level); 903 PathFindingMapUpdate(0, 0); 904 TowerDrawAll(); 905 EnemyDraw(); 906 ProjectileDraw(); 907 ParticleDraw(); 908 DrawEnemyPaths(level); 909 910 guiState.isBlocked = 0; 911 912 // Hover rectangle, when the mouse is over the map 913 int isHovering = level->placementX >= -5 && level->placementX <= 5 && level->placementY >= -5 && level->placementY <= 5; 914 if (isHovering) 915 { 916 DrawCubeWires((Vector3){level->placementX, 0.75f, level->placementY}, 1.0f, 1.5f, 1.0f, GREEN); 917 } 918 919 EndMode3D(); 920 921 TowerDrawAllHealthBars(level->camera); 922 923 DrawTitle("Building phase"); 924 925 // Draw the context menu when the context menu is active 926 if (level->placementContextMenuStatus >= 1) 927 { 928 float maxHitpoints = 0.0f; 929 float hp = 0.0f; 930 float damageFactor = 0.0f; 931 int32_t sellValue = 0; 932 933 if (tower) 934 { 935 maxHitpoints = TowerGetMaxHealth(tower); 936 hp = maxHitpoints - tower->damage; 937 damageFactor = 1.0f - tower->damage / maxHitpoints; 938 sellValue = (int32_t) ceilf(TowerTypeGetCosts(tower->towerType) * 0.5f * damageFactor); 939 } 940 941 ContextMenuItem menu[12] = {0}; 942 int menuCount = 0; 943 int menuIndex = 0; 944 Vector2 anchorLow = GetWorldToScreen((Vector3){level->placementX, -0.25f, level->placementY}, level->camera); 945 Vector2 anchorHigh = GetWorldToScreen((Vector3){level->placementX, 2.0f, level->placementY}, level->camera); 946 947 if (level->placementContextMenuType == CONTEXT_MENU_TYPE_MAIN) 948 { 949 DrawLevelBuildingStateMainContextMenu(level, tower, sellValue, hp, maxHitpoints, anchorLow, anchorHigh); 950 } 951 else if (level->placementContextMenuType == CONTEXT_MENU_TYPE_UPGRADE) 952 { 953 int totalLevel = tower->upgradeState.damage + tower->upgradeState.speed + tower->upgradeState.range; 954 int costs = totalLevel * 4; 955 int isMaxLevel = totalLevel >= TOWER_MAX_STAGE; 956 menu[menuCount++] = ContextMenuItemText(menuIndex++, TextFormat("%s tower, stage %d%s", 957 TowerTypeGetName(tower->towerType), totalLevel, isMaxLevel ? " (max)" : ""), 0.5f); 958 int buttonMenuIndex = menuIndex; 959 menu[menuCount++] = ContextMenuItemButton(menuIndex++, TextFormat("Speed: %d (%dG)", tower->upgradeState.speed, costs), 960 OnContextMenuDoUpgrade, (ContextMenuArgs){.tower = tower, .uint8 = UPGRADE_TYPE_SPEED, .int32 = costs}); 961 menu[menuCount++] = ContextMenuItemButton(menuIndex++, TextFormat("Damage: %d (%dG)", tower->upgradeState.damage, costs), 962 OnContextMenuDoUpgrade, (ContextMenuArgs){.tower = tower, .uint8 = UPGRADE_TYPE_DAMAGE, .int32 = costs}); 963 menu[menuCount++] = ContextMenuItemButton(menuIndex++, TextFormat("Range: %d (%dG)", tower->upgradeState.range, costs), 964 OnContextMenuDoUpgrade, (ContextMenuArgs){.tower = tower, .uint8 = UPGRADE_TYPE_RANGE, .int32 = costs}); 965 966 // check if buttons should be disabled 967 if (isMaxLevel || level->playerGold < costs) 968 { 969 for (int i = buttonMenuIndex; i < menuCount; i++) 970 { 971 menu[i].buttonState.isDisabled = 1; 972 } 973 } 974 975 if (DrawContextMenu(level, anchorLow, anchorHigh, 220, menu)) 976 { 977 level->placementContextMenuType = CONTEXT_MENU_TYPE_MAIN; 978 } 979 } 980 else if (level->placementContextMenuType == CONTEXT_MENU_TYPE_SELL_CONFIRM) 981 { 982 menu[menuCount++] = ContextMenuItemText(menuIndex++, TextFormat("Sell %s?", TowerTypeGetName(tower->towerType)), 0.5f); 983 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "Yes", OnContextMenuSellConfirm, 984 (ContextMenuArgs){.tower = tower, .int32 = sellValue}); 985 menu[menuCount++] = ContextMenuItemButton(menuIndex++, "No", OnContextMenuSellCancel, (ContextMenuArgs){0}); 986 Vector2 anchorLow = {GetScreenWidth() * 0.5f, GetScreenHeight() * 0.5f}; 987 if (DrawContextMenu(level, anchorLow, anchorLow, 150, menu)) 988 { 989 level->placementContextMenuStatus = -1; 990 level->placementContextMenuType = CONTEXT_MENU_TYPE_MAIN; 991 } 992 } 993 } 994 995 // Activate the context menu when the mouse is clicked and the context menu is not active 996 else if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON) && isHovering && level->placementContextMenuStatus <= 0) 997 { 998 level->placementContextMenuStatus += 1; 999 } 1000 1001 if (level->placementContextMenuStatus == 0) 1002 { 1003 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 1004 { 1005 level->nextState = LEVEL_STATE_RESET; 1006 } 1007 1008 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0)) 1009 { 1010 level->nextState = LEVEL_STATE_BATTLE; 1011 } 1012 1013 } 1014 } 1015 1016 void InitBattleStateConditions(Level *level) 1017 { 1018 level->state = LEVEL_STATE_BATTLE; 1019 level->nextState = LEVEL_STATE_NONE; 1020 level->waveEndTimer = 0.0f; 1021 for (int i = 0; i < 10; i++) 1022 { 1023 EnemyWave *wave = &level->waves[i]; 1024 wave->spawned = 0; 1025 wave->timeToSpawnNext = wave->delay; 1026 } 1027 } 1028 1029 void DrawLevelBattleState(Level *level) 1030 { 1031 BeginMode3D(level->camera);
1032 DrawLevelGround(level); 1033 TowerUpdateAllRangeFade(0, 0.0f);
1034 TowerDrawAll(); 1035 EnemyDraw(); 1036 ProjectileDraw(); 1037 ParticleDraw(); 1038 guiState.isBlocked = 0; 1039 EndMode3D(); 1040 1041 EnemyDrawHealthbars(level->camera); 1042 TowerDrawAllHealthBars(level->camera); 1043 1044 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 1045 { 1046 level->nextState = LEVEL_STATE_RESET; 1047 } 1048 1049 int maxCount = 0; 1050 int remainingCount = 0; 1051 for (int i = 0; i < 10; i++) 1052 { 1053 EnemyWave *wave = &level->waves[i]; 1054 if (wave->wave != level->currentWave) 1055 { 1056 continue; 1057 } 1058 maxCount += wave->count; 1059 remainingCount += wave->count - wave->spawned; 1060 } 1061 int aliveCount = EnemyCount(); 1062 remainingCount += aliveCount; 1063 1064 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount); 1065 DrawTitle(text); 1066 } 1067 1068 void DrawLevel(Level *level) 1069 { 1070 switch (level->state) 1071 { 1072 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break; 1073 case LEVEL_STATE_BUILDING_PLACEMENT: DrawLevelBuildingPlacementState(level); break; 1074 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break; 1075 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break; 1076 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break; 1077 default: break; 1078 } 1079 1080 DrawLevelHud(level); 1081 } 1082 1083 void UpdateLevel(Level *level) 1084 { 1085 if (level->state == LEVEL_STATE_BATTLE) 1086 { 1087 int activeWaves = 0; 1088 for (int i = 0; i < 10; i++) 1089 { 1090 EnemyWave *wave = &level->waves[i]; 1091 if (wave->spawned >= wave->count || wave->wave != level->currentWave) 1092 { 1093 continue; 1094 } 1095 activeWaves++; 1096 wave->timeToSpawnNext -= gameTime.deltaTime; 1097 if (wave->timeToSpawnNext <= 0.0f) 1098 { 1099 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y); 1100 if (enemy) 1101 { 1102 wave->timeToSpawnNext = wave->interval; 1103 wave->spawned++; 1104 } 1105 } 1106 } 1107 if (TowerGetByType(TOWER_TYPE_BASE) == 0) { 1108 level->waveEndTimer += gameTime.deltaTime; 1109 if (level->waveEndTimer >= 2.0f) 1110 { 1111 level->nextState = LEVEL_STATE_LOST_WAVE; 1112 } 1113 } 1114 else if (activeWaves == 0 && EnemyCount() == 0) 1115 { 1116 level->waveEndTimer += gameTime.deltaTime; 1117 if (level->waveEndTimer >= 2.0f) 1118 { 1119 level->nextState = LEVEL_STATE_WON_WAVE; 1120 } 1121 } 1122 } 1123 1124 PathFindingMapUpdate(0, 0); 1125 EnemyUpdate(); 1126 TowerUpdate(); 1127 ProjectileUpdate(); 1128 ParticleUpdate(); 1129 1130 if (level->nextState == LEVEL_STATE_RESET) 1131 { 1132 InitLevel(level); 1133 } 1134 1135 if (level->nextState == LEVEL_STATE_BATTLE) 1136 { 1137 InitBattleStateConditions(level); 1138 } 1139 1140 if (level->nextState == LEVEL_STATE_WON_WAVE) 1141 { 1142 level->currentWave++; 1143 level->state = LEVEL_STATE_WON_WAVE; 1144 } 1145 1146 if (level->nextState == LEVEL_STATE_LOST_WAVE) 1147 { 1148 level->state = LEVEL_STATE_LOST_WAVE; 1149 } 1150 1151 if (level->nextState == LEVEL_STATE_BUILDING) 1152 { 1153 level->state = LEVEL_STATE_BUILDING; 1154 level->placementContextMenuStatus = 0; 1155 } 1156 1157 if (level->nextState == LEVEL_STATE_BUILDING_PLACEMENT) 1158 { 1159 level->state = LEVEL_STATE_BUILDING_PLACEMENT; 1160 level->placementTransitionPosition = (Vector2){ 1161 level->placementX, level->placementY}; 1162 // initialize the spring to the current position 1163 level->placementTowerSpring = (PhysicsPoint){ 1164 .position = (Vector3){level->placementX, 8.0f, level->placementY}, 1165 .velocity = (Vector3){0.0f, 0.0f, 0.0f}, 1166 }; 1167 level->placementPhase = PLACEMENT_PHASE_STARTING; 1168 level->placementTimer = 0.0f; 1169 } 1170 1171 if (level->nextState == LEVEL_STATE_WON_LEVEL) 1172 { 1173 // make something of this later 1174 InitLevel(level); 1175 } 1176 1177 level->nextState = LEVEL_STATE_NONE; 1178 } 1179 1180 float nextSpawnTime = 0.0f; 1181 1182 void ResetGame() 1183 { 1184 InitLevel(currentLevel); 1185 } 1186 1187 void InitGame() 1188 { 1189 TowerInit(); 1190 EnemyInit(); 1191 ProjectileInit(); 1192 ParticleInit(); 1193 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f); 1194 1195 currentLevel = levels; 1196 InitLevel(currentLevel); 1197 } 1198 1199 //# Immediate GUI functions 1200 1201 void DrawHealthBar(Camera3D camera, Vector3 position, Vector2 screenOffset, float healthRatio, Color barColor, float healthBarWidth) 1202 { 1203 const float healthBarHeight = 6.0f; 1204 const float healthBarOffset = 15.0f; 1205 const float inset = 2.0f; 1206 const float innerWidth = healthBarWidth - inset * 2; 1207 const float innerHeight = healthBarHeight - inset * 2; 1208 1209 Vector2 screenPos = GetWorldToScreen(position, camera); 1210 screenPos = Vector2Add(screenPos, screenOffset); 1211 float centerX = screenPos.x - healthBarWidth * 0.5f; 1212 float topY = screenPos.y - healthBarOffset; 1213 DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK); 1214 float healthWidth = innerWidth * healthRatio; 1215 DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor); 1216 } 1217 1218 void DrawBoxedText(const char *text, int x, int y, int width, int height, float alignX, float alignY, Color textColor) 1219 { 1220 Vector2 textSize = MeasureTextEx(gameFontNormal, text, gameFontNormal.baseSize, 1); 1221 1222 DrawTextEx(gameFontNormal, text, (Vector2){ 1223 x + (width - textSize.x) * alignX, 1224 y + (height - textSize.y) * alignY 1225 }, gameFontNormal.baseSize, 1, textColor); 1226 } 1227 1228 int Button(const char *text, int x, int y, int width, int height, ButtonState *state) 1229 { 1230 Rectangle bounds = {x, y, width, height}; 1231 int isPressed = 0; 1232 int isSelected = state && state->isSelected; 1233 int isDisabled = state && state->isDisabled; 1234 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !isDisabled) 1235 { 1236 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) 1237 { 1238 isPressed = 1; 1239 } 1240 guiState.isBlocked = 1; 1241 DrawTextureNPatch(spriteSheet, isPressed ? uiButtonPressed : uiButtonHovered, 1242 bounds, Vector2Zero(), 0, WHITE); 1243 } 1244 else 1245 { 1246 DrawTextureNPatch(spriteSheet, isSelected ? uiButtonHovered : (isDisabled ? uiButtonDisabled : uiButtonNormal), 1247 bounds, Vector2Zero(), 0, WHITE); 1248 } 1249 Vector2 textSize = MeasureTextEx(gameFontNormal, text, gameFontNormal.baseSize, 1); 1250 Color textColor = isDisabled ? LIGHTGRAY : BLACK; 1251 DrawTextEx(gameFontNormal, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, gameFontNormal.baseSize, 1, textColor); 1252 return isPressed; 1253 } 1254 1255 //# Main game loop 1256 1257 void GameUpdate() 1258 { 1259 UpdateLevel(currentLevel); 1260 } 1261 1262 int main(void) 1263 { 1264 int screenWidth, screenHeight; 1265 GetPreferredSize(&screenWidth, &screenHeight); 1266 InitWindow(screenWidth, screenHeight, "Tower defense"); 1267 float gamespeed = 1.0f; 1268 int frameRate = 30; 1269 SetTargetFPS(30); 1270 1271 LoadAssets(); 1272 InitGame(); 1273 1274 float pause = 1.0f; 1275 1276 while (!WindowShouldClose()) 1277 { 1278 if (IsPaused()) { 1279 // canvas is not visible in browser - do nothing 1280 continue; 1281 } 1282 1283 if (IsKeyPressed(KEY_F)) 1284 { 1285 frameRate = (frameRate + 5) % 30; 1286 frameRate = frameRate < 10 ? 10 : frameRate; 1287 SetTargetFPS(frameRate); 1288 } 1289 1290 if (IsKeyPressed(KEY_T)) 1291 { 1292 gamespeed += 0.1f; 1293 if (gamespeed > 1.05f) gamespeed = 0.1f; 1294 } 1295 1296 if (IsKeyPressed(KEY_P)) 1297 { 1298 pause = pause > 0.5f ? 0.0f : 1.0f; 1299 } 1300 1301 float dt = GetFrameTime() * gamespeed * pause; 1302 // cap maximum delta time to 0.1 seconds to prevent large time steps 1303 if (dt > 0.1f) dt = 0.1f; 1304 gameTime.time += dt; 1305 gameTime.deltaTime = dt; 1306 gameTime.frameCount += 1; 1307 1308 float fixedTimeDiff = gameTime.time - gameTime.fixedTimeStart; 1309 gameTime.fixedStepCount = (uint8_t)(fixedTimeDiff / gameTime.fixedDeltaTime); 1310 1311 BeginDrawing(); 1312 ClearBackground((Color){0x4E, 0x63, 0x26, 0xFF}); 1313 1314 GameUpdate(); 1315 DrawLevel(currentLevel); 1316 1317 if (gamespeed != 1.0f) 1318 DrawText(TextFormat("Speed: %.1f", gamespeed), GetScreenWidth() - 180, 60, 20, WHITE); 1319 EndDrawing(); 1320 1321 gameTime.fixedTimeStart += gameTime.fixedStepCount * gameTime.fixedDeltaTime; 1322 } 1323 1324 CloseWindow(); 1325 1326 return 0; 1327 }
  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 
 21 #define ENEMY_TYPE_MINION 1
 22 #define ENEMY_TYPE_RUNNER 2
 23 #define ENEMY_TYPE_SHIELD 3
 24 #define ENEMY_TYPE_BOSS 4
 25 
 26 #define PARTICLE_MAX_COUNT 400
 27 #define PARTICLE_TYPE_NONE 0
 28 #define PARTICLE_TYPE_EXPLOSION 1
 29 
 30 typedef struct Particle
 31 {
 32   uint8_t particleType;
 33   float spawnTime;
 34   float lifetime;
 35   Vector3 position;
 36   Vector3 velocity;
 37   Vector3 scale;
 38 } Particle;
 39 
 40 #define TOWER_MAX_COUNT 400
 41 enum TowerType
 42 {
 43   TOWER_TYPE_NONE,
 44   TOWER_TYPE_BASE,
 45   TOWER_TYPE_ARCHER,
 46   TOWER_TYPE_BALLISTA,
 47   TOWER_TYPE_CATAPULT,
 48   TOWER_TYPE_WALL,
 49   TOWER_TYPE_COUNT
 50 };
 51 
 52 typedef struct HitEffectConfig
 53 {
 54   float damage;
55 float maxUpgradeDamage;
56 float areaDamageRadius; 57 float pushbackPowerDistance; 58 } HitEffectConfig; 59 60 typedef struct TowerTypeConfig 61 { 62 const char *name;
63 float cooldown; 64 float maxUpgradeCooldown;
65 float range; 66 float maxUpgradeRange; 67 float projectileSpeed; 68 69 uint8_t cost; 70 uint8_t projectileType; 71 uint16_t maxHealth; 72 73 HitEffectConfig hitEffect; 74 } TowerTypeConfig; 75 76 #define TOWER_MAX_STAGE 10 77 78 typedef struct TowerUpgradeState 79 { 80 uint8_t range; 81 uint8_t damage; 82 uint8_t speed; 83 } TowerUpgradeState; 84 85 typedef struct Tower 86 { 87 int16_t x, y; 88 uint8_t towerType; 89 TowerUpgradeState upgradeState; 90 Vector2 lastTargetPosition; 91 float cooldown; 92 float damage; 93 // alpha value for the range circle drawing 94 float drawRangeAlpha; 95 } Tower; 96 97 typedef struct GameTime 98 { 99 float time; 100 float deltaTime; 101 uint32_t frameCount; 102 103 float fixedDeltaTime; 104 // leaving the fixed time stepping to the update functions, 105 // we need to know the fixed time at the start of the frame 106 float fixedTimeStart; 107 // and the number of fixed steps that we have to make this frame 108 // The fixedTime is fixedTimeStart + n * fixedStepCount 109 uint8_t fixedStepCount; 110 } GameTime; 111 112 typedef struct ButtonState { 113 char isSelected; 114 char isDisabled; 115 } ButtonState; 116 117 typedef struct GUIState { 118 int isBlocked; 119 } GUIState; 120 121 typedef enum LevelState 122 { 123 LEVEL_STATE_NONE, 124 LEVEL_STATE_BUILDING, 125 LEVEL_STATE_BUILDING_PLACEMENT, 126 LEVEL_STATE_BATTLE, 127 LEVEL_STATE_WON_WAVE, 128 LEVEL_STATE_LOST_WAVE, 129 LEVEL_STATE_WON_LEVEL, 130 LEVEL_STATE_RESET, 131 } LevelState; 132 133 typedef struct EnemyWave { 134 uint8_t enemyType; 135 uint8_t wave; 136 uint16_t count; 137 float interval; 138 float delay; 139 Vector2 spawnPosition; 140 141 uint16_t spawned; 142 float timeToSpawnNext; 143 } EnemyWave; 144 145 #define ENEMY_MAX_WAVE_COUNT 10 146 147 typedef enum PlacementPhase 148 { 149 PLACEMENT_PHASE_STARTING, 150 PLACEMENT_PHASE_MOVING, 151 PLACEMENT_PHASE_PLACING, 152 } PlacementPhase; 153 154 typedef struct Level 155 { 156 int seed; 157 LevelState state; 158 LevelState nextState; 159 Camera3D camera; 160 int placementMode; 161 PlacementPhase placementPhase; 162 float placementTimer; 163 164 int16_t placementX; 165 int16_t placementY; 166 int8_t placementContextMenuStatus; 167 int8_t placementContextMenuType; 168 169 Vector2 placementTransitionPosition; 170 PhysicsPoint placementTowerSpring; 171 172 int initialGold; 173 int playerGold; 174 175 EnemyWave waves[ENEMY_MAX_WAVE_COUNT]; 176 int currentWave; 177 float waveEndTimer; 178 } Level; 179 180 typedef struct DeltaSrc 181 { 182 char x, y; 183 } DeltaSrc; 184 185 typedef struct PathfindingMap 186 { 187 int width, height; 188 float scale; 189 float *distances; 190 long *towerIndex; 191 DeltaSrc *deltaSrc; 192 float maxDistance; 193 Matrix toMapSpace; 194 Matrix toWorldSpace; 195 } PathfindingMap; 196 197 // when we execute the pathfinding algorithm, we need to store the active nodes 198 // in a queue. Each node has a position, a distance from the start, and the 199 // position of the node that we came from. 200 typedef struct PathfindingNode 201 { 202 int16_t x, y, fromX, fromY; 203 float distance; 204 } PathfindingNode; 205 206 typedef struct EnemyId 207 { 208 uint16_t index; 209 uint16_t generation; 210 } EnemyId; 211 212 typedef struct EnemyClassConfig 213 { 214 float speed; 215 float health; 216 float shieldHealth; 217 float shieldDamageAbsorption; 218 float radius; 219 float maxAcceleration; 220 float requiredContactTime; 221 float explosionDamage; 222 float explosionRange; 223 float explosionPushbackPower; 224 int goldValue; 225 } EnemyClassConfig; 226 227 typedef struct Enemy 228 { 229 int16_t currentX, currentY; 230 int16_t nextX, nextY; 231 Vector2 simPosition; 232 Vector2 simVelocity; 233 uint16_t generation; 234 float walkedDistance; 235 float startMovingTime; 236 float damage, futureDamage; 237 float shieldDamage; 238 float contactTime; 239 uint8_t enemyType; 240 uint8_t movePathCount; 241 Vector2 movePath[ENEMY_MAX_PATH_COUNT]; 242 } Enemy; 243 244 // a unit that uses sprites to be drawn 245 #define SPRITE_UNIT_ANIMATION_COUNT 6 246 #define SPRITE_UNIT_PHASE_WEAPON_IDLE 1 247 #define SPRITE_UNIT_PHASE_WEAPON_COOLDOWN 2 248 #define SPRITE_UNIT_PHASE_WEAPON_SHIELD 3 249 250 typedef struct SpriteAnimation 251 { 252 Rectangle srcRect; 253 Vector2 offset; 254 uint8_t animationId; 255 uint8_t frameCount; 256 uint8_t frameWidth; 257 float frameDuration; 258 } SpriteAnimation; 259 260 typedef struct SpriteUnit 261 { 262 float scale; 263 SpriteAnimation animations[SPRITE_UNIT_ANIMATION_COUNT]; 264 } SpriteUnit; 265 266 #define PROJECTILE_MAX_COUNT 1200 267 #define PROJECTILE_TYPE_NONE 0 268 #define PROJECTILE_TYPE_ARROW 1 269 #define PROJECTILE_TYPE_CATAPULT 2 270 #define PROJECTILE_TYPE_BALLISTA 3 271 272 typedef struct Projectile 273 { 274 uint8_t projectileType; 275 float shootTime; 276 float arrivalTime; 277 float distance; 278 Vector3 position; 279 Vector3 target; 280 Vector3 directionNormal; 281 EnemyId targetEnemy; 282 HitEffectConfig hitEffectConfig; 283 } Projectile; 284 285 //# Function declarations 286 int Button(const char *text, int x, int y, int width, int height, ButtonState *state); 287 int EnemyAddDamageRange(Vector2 position, float range, float damage); 288 int EnemyAddDamage(Enemy *enemy, float damage); 289 290 //# Enemy functions 291 void EnemyInit(); 292 void EnemyDraw(); 293 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource); 294 void EnemyUpdate(); 295 float EnemyGetCurrentMaxSpeed(Enemy *enemy); 296 float EnemyGetMaxHealth(Enemy *enemy); 297 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY); 298 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount); 299 EnemyId EnemyGetId(Enemy *enemy); 300 Enemy *EnemyTryResolve(EnemyId enemyId); 301 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY); 302 int EnemyAddDamage(Enemy *enemy, float damage); 303 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range); 304 int EnemyCount(); 305 void EnemyDrawHealthbars(Camera3D camera); 306 307 //# Tower functions 308 const char *TowerTypeGetName(uint8_t towerType); 309 int TowerTypeGetCosts(uint8_t towerType); 310 void TowerInit(); 311 void TowerUpdate(); 312 void TowerUpdateAllRangeFade(Tower *selectedTower, float fadeoutTarget); 313 void TowerDrawAll(); 314 void TowerDrawAllHealthBars(Camera3D camera); 315 void TowerDrawModel(Tower *tower); 316 void TowerDrawRange(Tower *tower, float alpha); 317 Tower *TowerGetByIndex(int index); 318 Tower *TowerGetByType(uint8_t towerType); 319 Tower *TowerGetAt(int16_t x, int16_t y); 320 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y); 321 float TowerGetMaxHealth(Tower *tower); 322 float TowerGetRange(Tower *tower); 323 324 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase); 325 326 //# Particles 327 void ParticleInit(); 328 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime); 329 void ParticleUpdate(); 330 void ParticleDraw(); 331 332 //# Projectiles 333 void ProjectileInit(); 334 void ProjectileDraw(); 335 void ProjectileUpdate(); 336 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig); 337 338 //# Pathfinding map 339 void PathfindingMapInit(int width, int height, Vector3 translate, float scale); 340 float PathFindingGetDistance(int mapX, int mapY); 341 Vector2 PathFindingGetGradient(Vector3 world); 342 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY); 343 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells); 344 void PathFindingMapDraw(); 345 346 //# UI 347 void DrawHealthBar(Camera3D camera, Vector3 position, Vector2 screenOffset, float healthRatio, Color barColor, float healthBarWidth); 348 349 //# Level 350 void DrawLevelGround(Level *level); 351 void DrawEnemyPath(Level *level, Color arrowColor); 352 353 //# variables 354 extern Level *currentLevel; 355 extern Enemy enemies[ENEMY_MAX_COUNT]; 356 extern int enemyCount; 357 extern EnemyClassConfig enemyClassConfigs[]; 358 359 extern GUIState guiState; 360 extern GameTime gameTime; 361 extern Tower towers[TOWER_MAX_COUNT]; 362 extern int towerCount; 363 364 extern Texture2D palette, spriteSheet; 365 366 #endif
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 
  4 static TowerTypeConfig towerTypeConfigs[TOWER_TYPE_COUNT] = {
  5     [TOWER_TYPE_BASE] = {
  6         .name = "Castle",
  7         .maxHealth = 10,
  8     },
  9     [TOWER_TYPE_ARCHER] = {
 10         .name = "Archer",
 11         .cooldown = 0.5f,
12 .maxUpgradeCooldown = 0.25f,
13 .range = 3.0f, 14 .maxUpgradeRange = 5.0f, 15 .cost = 6, 16 .maxHealth = 10, 17 .projectileSpeed = 4.0f, 18 .projectileType = PROJECTILE_TYPE_ARROW, 19 .hitEffect = { 20 .damage = 3.0f,
21 .maxUpgradeDamage = 6.0f, 22 },
23 }, 24 [TOWER_TYPE_BALLISTA] = { 25 .name = "Ballista",
26 .cooldown = 1.5f, 27 .maxUpgradeCooldown = 1.0f,
28 .range = 6.0f, 29 .maxUpgradeRange = 8.0f, 30 .cost = 9, 31 .maxHealth = 10, 32 .projectileSpeed = 10.0f, 33 .projectileType = PROJECTILE_TYPE_BALLISTA, 34 .hitEffect = {
35 .damage = 8.0f, 36 .maxUpgradeDamage = 16.0f,
37 .pushbackPowerDistance = 0.25f, 38 } 39 }, 40 [TOWER_TYPE_CATAPULT] = { 41 .name = "Catapult",
42 .cooldown = 1.7f, 43 .maxUpgradeCooldown = 1.0f,
44 .range = 5.0f, 45 .maxUpgradeRange = 7.0f, 46 .cost = 10, 47 .maxHealth = 10, 48 .projectileSpeed = 3.0f, 49 .projectileType = PROJECTILE_TYPE_CATAPULT, 50 .hitEffect = {
51 .damage = 2.0f, 52 .maxUpgradeDamage = 4.0f,
53 .areaDamageRadius = 1.75f, 54 } 55 }, 56 [TOWER_TYPE_WALL] = { 57 .name = "Wall", 58 .cost = 2, 59 .maxHealth = 10, 60 }, 61 }; 62 63 Tower towers[TOWER_MAX_COUNT]; 64 int towerCount = 0; 65 66 Model towerModels[TOWER_TYPE_COUNT]; 67 68 // definition of our archer unit 69 SpriteUnit archerUnit = { 70 .animations[0] = { 71 .srcRect = {0, 0, 16, 16}, 72 .offset = {7, 1}, 73 .frameCount = 1, 74 .frameDuration = 0.0f, 75 }, 76 .animations[1] = { 77 .animationId = SPRITE_UNIT_PHASE_WEAPON_COOLDOWN, 78 .srcRect = {16, 0, 6, 16}, 79 .offset = {8, 0}, 80 }, 81 .animations[2] = { 82 .animationId = SPRITE_UNIT_PHASE_WEAPON_IDLE, 83 .srcRect = {22, 0, 11, 16}, 84 .offset = {10, 0}, 85 }, 86 }; 87 88 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase) 89 { 90 float unitScale = unit.scale == 0 ? 1.0f : unit.scale; 91 float xScale = flip ? -1.0f : 1.0f; 92 Camera3D camera = currentLevel->camera; 93 float size = 0.5f * unitScale; 94 // we want the sprite to face the camera, so we need to calculate the up vector 95 Vector3 forward = Vector3Subtract(camera.target, camera.position); 96 Vector3 up = {0, 1, 0}; 97 Vector3 right = Vector3CrossProduct(forward, up); 98 up = Vector3Normalize(Vector3CrossProduct(right, forward)); 99 100 for (int i=0;i<SPRITE_UNIT_ANIMATION_COUNT;i++) 101 { 102 SpriteAnimation anim = unit.animations[i]; 103 if (anim.animationId != phase && anim.animationId != 0) 104 { 105 continue; 106 } 107 Rectangle srcRect = anim.srcRect; 108 if (anim.frameCount > 1) 109 { 110 int w = anim.frameWidth > 0 ? anim.frameWidth : srcRect.width; 111 srcRect.x += (int)(t / anim.frameDuration) % anim.frameCount * w; 112 } 113 Vector2 offset = { anim.offset.x / 16.0f * size, anim.offset.y / 16.0f * size * xScale }; 114 Vector2 scale = { srcRect.width / 16.0f * size, srcRect.height / 16.0f * size }; 115 116 if (flip) 117 { 118 srcRect.x += srcRect.width; 119 srcRect.width = -srcRect.width; 120 offset.x = scale.x - offset.x; 121 } 122 DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE); 123 // move the sprite slightly towards the camera to avoid z-fighting 124 position = Vector3Add(position, Vector3Scale(Vector3Normalize(forward), -0.01f)); 125 } 126 } 127 128 void TowerInit() 129 { 130 for (int i = 0; i < TOWER_MAX_COUNT; i++) 131 { 132 towers[i] = (Tower){0}; 133 } 134 towerCount = 0; 135 136 towerModels[TOWER_TYPE_BASE] = LoadModel("data/keep.glb"); 137 towerModels[TOWER_TYPE_WALL] = LoadModel("data/wall-0000.glb"); 138 139 for (int i = 0; i < TOWER_TYPE_COUNT; i++) 140 { 141 if (towerModels[i].materials) 142 { 143 // assign the palette texture to the material of the model (0 is not used afaik)
144 towerModels[i].materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette; 145 } 146 } 147 } 148 149 static float TowerGetCooldown(Tower *tower) 150 { 151 float cooldown = towerTypeConfigs[tower->towerType].cooldown; 152 float maxUpgradeCooldown = towerTypeConfigs[tower->towerType].maxUpgradeCooldown; 153 if (tower->upgradeState.speed > 0) 154 { 155 cooldown = Lerp(cooldown, maxUpgradeCooldown, tower->upgradeState.speed / (float)TOWER_MAX_STAGE); 156 } 157 return cooldown;
158 } 159 160 static void TowerGunUpdate(Tower *tower) 161 { 162 TowerTypeConfig config = towerTypeConfigs[tower->towerType]; 163 if (tower->cooldown <= 0.0f) 164 { 165 Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, TowerGetRange(tower)); 166 if (enemy) 167 {
168 tower->cooldown = TowerGetCooldown(tower);
169 // shoot the enemy; determine future position of the enemy 170 float bulletSpeed = config.projectileSpeed; 171 Vector2 velocity = enemy->simVelocity; 172 Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0); 173 Vector2 towerPosition = {tower->x, tower->y}; 174 float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed; 175 for (int i = 0; i < 8; i++) { 176 velocity = enemy->simVelocity; 177 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0); 178 float distance = Vector2Distance(towerPosition, futurePosition); 179 float eta2 = distance / bulletSpeed; 180 if (fabs(eta - eta2) < 0.01f) { 181 break; 182 }
183 eta = (eta2 + eta) * 0.5f; 184 } 185 186 HitEffectConfig hitEffect = config.hitEffect; 187 // apply damage upgrade to hit effect 188 if (tower->upgradeState.damage > 0) 189 { 190 hitEffect.damage = Lerp(hitEffect.damage, hitEffect.maxUpgradeDamage, tower->upgradeState.damage / (float)TOWER_MAX_STAGE);
191 } 192 193 ProjectileTryAdd(config.projectileType, enemy, 194 (Vector3){towerPosition.x, 1.33f, towerPosition.y}, 195 (Vector3){futurePosition.x, 0.25f, futurePosition.y},
196 bulletSpeed, hitEffect); 197 enemy->futureDamage += hitEffect.damage;
198 tower->lastTargetPosition = futurePosition; 199 } 200 } 201 else 202 { 203 tower->cooldown -= gameTime.deltaTime; 204 } 205 } 206 207 Tower *TowerGetAt(int16_t x, int16_t y) 208 { 209 for (int i = 0; i < towerCount; i++) 210 { 211 if (towers[i].x == x && towers[i].y == y && towers[i].towerType != TOWER_TYPE_NONE) 212 { 213 return &towers[i]; 214 } 215 } 216 return 0; 217 } 218 219 Tower *TowerGetByIndex(int index) 220 { 221 if (index < 0 || index >= towerCount) 222 { 223 return 0; 224 } 225 return &towers[index]; 226 } 227 228 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y) 229 { 230 if (towerCount >= TOWER_MAX_COUNT) 231 { 232 return 0; 233 } 234 235 Tower *tower = TowerGetAt(x, y); 236 if (tower) 237 { 238 return 0; 239 } 240 241 tower = &towers[towerCount++]; 242 *tower = (Tower){ 243 .x = x, 244 .y = y, 245 .towerType = towerType, 246 .cooldown = 0.0f, 247 .damage = 0.0f, 248 }; 249 return tower; 250 } 251 252 Tower *TowerGetByType(uint8_t towerType) 253 { 254 for (int i = 0; i < towerCount; i++) 255 { 256 if (towers[i].towerType == towerType) 257 { 258 return &towers[i]; 259 } 260 } 261 return 0; 262 } 263 264 const char *TowerTypeGetName(uint8_t towerType) 265 { 266 return towerTypeConfigs[towerType].name; 267 } 268 269 int TowerTypeGetCosts(uint8_t towerType) 270 { 271 return towerTypeConfigs[towerType].cost; 272 } 273 274 float TowerGetMaxHealth(Tower *tower) 275 { 276 return towerTypeConfigs[tower->towerType].maxHealth; 277 } 278 279 float TowerGetRange(Tower *tower) 280 { 281 float range = towerTypeConfigs[tower->towerType].range; 282 float maxUpgradeRange = towerTypeConfigs[tower->towerType].maxUpgradeRange; 283 if (tower->upgradeState.range > 0) 284 { 285 range = Lerp(range, maxUpgradeRange, tower->upgradeState.range / (float)TOWER_MAX_STAGE); 286 } 287 return range; 288 } 289 290 void TowerUpdateAllRangeFade(Tower *selectedTower, float fadeoutTarget) 291 { 292 // animate fade in and fade out of range drawing using framerate independent lerp 293 float fadeLerp = 1.0f - powf(0.005f, gameTime.deltaTime); 294 for (int i = 0; i < TOWER_MAX_COUNT; i++) 295 { 296 Tower *fadingTower = TowerGetByIndex(i); 297 if (!fadingTower) 298 { 299 break; 300 } 301 float drawRangeTarget = fadingTower == selectedTower ? 1.0f : fadeoutTarget; 302 fadingTower->drawRangeAlpha = Lerp(fadingTower->drawRangeAlpha, drawRangeTarget, fadeLerp); 303 } 304 } 305 306 void TowerDrawRange(Tower *tower, float alpha) 307 { 308 Color ringColor = (Color){255, 200, 100, 255}; 309 const int rings = 4; 310 const float radiusOffset = 0.5f; 311 const float animationSpeed = 2.0f; 312 float animation = 1.0f - fmodf(gameTime.time * animationSpeed, 1.0f); 313 float radius = TowerGetRange(tower); 314 // base circle 315 DrawCircle3D((Vector3){tower->x, 0.01f, tower->y}, radius, (Vector3){1, 0, 0}, 90, 316 Fade(ringColor, alpha)); 317 318 for (int i = 1; i < rings; i++) 319 { 320 float t = ((float)i + animation) / (float)rings; 321 float r = Lerp(radius, radius - radiusOffset, t * t); 322 float a = 1.0f - ((float)i + animation - 1) / (float) (rings - 1); 323 if (i == 1) 324 { 325 // fade out the outermost ring 326 a = animation; 327 } 328 a *= alpha; 329 330 DrawCircle3D((Vector3){tower->x, 0.01f, tower->y}, r, (Vector3){1, 0, 0}, 90, 331 Fade(ringColor, a)); 332 } 333 } 334 335 void TowerDrawModel(Tower *tower) 336 { 337 if (tower->towerType == TOWER_TYPE_NONE) 338 { 339 return; 340 } 341 342 if (tower->drawRangeAlpha > 2.0f/256.0f) 343 { 344 TowerDrawRange(tower, tower->drawRangeAlpha); 345 } 346 347 switch (tower->towerType) 348 { 349 case TOWER_TYPE_ARCHER: 350 { 351 Vector2 screenPosTower = GetWorldToScreen((Vector3){tower->x, 0.0f, tower->y}, currentLevel->camera); 352 Vector2 screenPosTarget = GetWorldToScreen((Vector3){tower->lastTargetPosition.x, 0.0f, tower->lastTargetPosition.y}, currentLevel->camera); 353 DrawModel(towerModels[TOWER_TYPE_WALL], (Vector3){tower->x, 0.0f, tower->y}, 1.0f, WHITE); 354 DrawSpriteUnit(archerUnit, (Vector3){tower->x, 1.0f, tower->y}, 0, screenPosTarget.x > screenPosTower.x, 355 tower->cooldown > 0.2f ? SPRITE_UNIT_PHASE_WEAPON_COOLDOWN : SPRITE_UNIT_PHASE_WEAPON_IDLE); 356 } 357 break; 358 case TOWER_TYPE_BALLISTA: 359 DrawCube((Vector3){tower->x, 0.5f, tower->y}, 1.0f, 1.0f, 1.0f, BROWN); 360 break; 361 case TOWER_TYPE_CATAPULT: 362 DrawCube((Vector3){tower->x, 0.5f, tower->y}, 1.0f, 1.0f, 1.0f, DARKGRAY); 363 break; 364 default: 365 if (towerModels[tower->towerType].materials) 366 { 367 DrawModel(towerModels[tower->towerType], (Vector3){tower->x, 0.0f, tower->y}, 1.0f, WHITE); 368 } else { 369 DrawCube((Vector3){tower->x, 0.5f, tower->y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY); 370 } 371 break; 372 } 373 } 374 375 void TowerDrawAll() 376 { 377 for (int i = 0; i < towerCount; i++) 378 { 379 TowerDrawModel(&towers[i]); 380 } 381 } 382 383 void TowerUpdate() 384 { 385 for (int i = 0; i < towerCount; i++) 386 { 387 Tower *tower = &towers[i]; 388 switch (tower->towerType) 389 { 390 case TOWER_TYPE_CATAPULT: 391 case TOWER_TYPE_BALLISTA: 392 case TOWER_TYPE_ARCHER: 393 TowerGunUpdate(tower); 394 break; 395 } 396 } 397 } 398 399 void TowerDrawAllHealthBars(Camera3D camera) 400 { 401 for (int i = 0; i < towerCount; i++) 402 { 403 Tower *tower = &towers[i]; 404 if (tower->towerType == TOWER_TYPE_NONE || tower->damage <= 0.0f) 405 { 406 continue; 407 } 408 409 Vector3 position = (Vector3){tower->x, 0.5f, tower->y}; 410 float maxHealth = TowerGetMaxHealth(tower); 411 float health = maxHealth - tower->damage; 412 float healthRatio = health / maxHealth; 413 414 DrawHealthBar(camera, position, Vector2Zero(), healthRatio, GREEN, 35.0f); 415 } 416 }
  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 #include <stdlib.h>
  4 #include <math.h>
  5 #include <rlgl.h>
  6 
  7 EnemyClassConfig enemyClassConfigs[] = {
  8     [ENEMY_TYPE_MINION] = {
  9       .health = 10.0f, 
 10       .speed = 0.6f, 
 11       .radius = 0.25f, 
 12       .maxAcceleration = 1.0f,
 13       .explosionDamage = 1.0f,
 14       .requiredContactTime = 0.5f,
 15       .explosionRange = 1.0f,
 16       .explosionPushbackPower = 0.25f,
 17       .goldValue = 1,
 18     },
 19     [ENEMY_TYPE_RUNNER] = {
 20       .health = 5.0f, 
 21       .speed = 1.0f, 
 22       .radius = 0.25f, 
 23       .maxAcceleration = 2.0f,
 24       .explosionDamage = 1.0f,
 25       .requiredContactTime = 0.5f,
 26       .explosionRange = 1.0f,
 27       .explosionPushbackPower = 0.25f,
 28       .goldValue = 2,
 29     },
 30     [ENEMY_TYPE_SHIELD] = {
 31       .health = 8.0f, 
 32       .speed = 0.5f, 
 33       .radius = 0.25f, 
 34       .maxAcceleration = 1.0f,
 35       .explosionDamage = 2.0f,
 36       .requiredContactTime = 0.5f,
 37       .explosionRange = 1.0f,
 38       .explosionPushbackPower = 0.25f,
 39       .goldValue = 3,
 40       .shieldDamageAbsorption = 4.0f,
 41       .shieldHealth = 25.0f,
 42     },
 43     [ENEMY_TYPE_BOSS] = {
 44       .health = 50.0f, 
 45       .speed = 0.4f, 
 46       .radius = 0.25f, 
 47       .maxAcceleration = 1.0f,
 48       .explosionDamage = 5.0f,
 49       .requiredContactTime = 0.5f,
 50       .explosionRange = 1.0f,
 51       .explosionPushbackPower = 0.25f,
 52       .goldValue = 10,
 53     },
 54 };
 55 
 56 Enemy enemies[ENEMY_MAX_COUNT];
 57 int enemyCount = 0;
 58 
 59 SpriteUnit enemySprites[] = {
 60     [ENEMY_TYPE_MINION] = {
 61       .animations[0] = {
 62         .srcRect = {0, 17, 16, 15},
 63         .offset = {8.0f, 0.0f},
 64         .frameCount = 6,
 65         .frameDuration = 0.1f,
 66       },
 67       .animations[1] = {
 68         .srcRect = {1, 33, 15, 14},
 69         .offset = {7.0f, 0.0f},
 70         .frameCount = 6,
 71         .frameWidth = 16,
 72         .frameDuration = 0.1f,
 73       },
 74     },
 75     [ENEMY_TYPE_RUNNER] = {
 76       .scale = 0.75f,
 77       .animations[0] = {
 78         .srcRect = {0, 17, 16, 15},
 79         .offset = {8.0f, 0.0f},
 80         .frameCount = 6,
 81         .frameDuration = 0.1f,
 82       },
 83     },
 84     [ENEMY_TYPE_SHIELD] = {
 85       .animations[0] = {
 86         .srcRect = {0, 17, 16, 15},
 87         .offset = {8.0f, 0.0f},
 88         .frameCount = 6,
 89         .frameDuration = 0.1f,
 90       },
 91       .animations[1] = {
 92         .animationId = SPRITE_UNIT_PHASE_WEAPON_SHIELD,
 93         .srcRect = {99, 17, 10, 11},
 94         .offset = {7.0f, 0.0f},
 95       },
 96     },
 97     [ENEMY_TYPE_BOSS] = {
 98       .scale = 1.5f,
 99       .animations[0] = {
100         .srcRect = {0, 17, 16, 15},
101         .offset = {8.0f, 0.0f},
102         .frameCount = 6,
103         .frameDuration = 0.1f,
104       },
105       .animations[1] = {
106         .srcRect = {97, 29, 14, 7},
107         .offset = {7.0f, -9.0f},
108       },
109     },
110 };
111 
112 void EnemyInit()
113 {
114   for (int i = 0; i < ENEMY_MAX_COUNT; i++)
115   {
116     enemies[i] = (Enemy){0};
117   }
118   enemyCount = 0;
119 }
120 
121 float EnemyGetCurrentMaxSpeed(Enemy *enemy)
122 {
123   return enemyClassConfigs[enemy->enemyType].speed;
124 }
125 
126 float EnemyGetMaxHealth(Enemy *enemy)
127 {
128   return enemyClassConfigs[enemy->enemyType].health;
129 }
130 
131 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY)
132 {
133   int16_t castleX = 0;
134   int16_t castleY = 0;
135   int16_t dx = castleX - currentX;
136   int16_t dy = castleY - currentY;
137   if (dx == 0 && dy == 0)
138   {
139     *nextX = currentX;
140     *nextY = currentY;
141     return 1;
142   }
143   Vector2 gradient = PathFindingGetGradient((Vector3){currentX, 0, currentY});
144 
145   if (gradient.x == 0 && gradient.y == 0)
146   {
147     *nextX = currentX;
148     *nextY = currentY;
149     return 1;
150   }
151 
152   if (fabsf(gradient.x) > fabsf(gradient.y))
153   {
154     *nextX = currentX + (int16_t)(gradient.x > 0.0f ? 1 : -1);
155     *nextY = currentY;
156     return 0;
157   }
158   *nextX = currentX;
159   *nextY = currentY + (int16_t)(gradient.y > 0.0f ? 1 : -1);
160   return 0;
161 }
162 
163 
164 // this function predicts the movement of the unit for the next deltaT seconds
165 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount)
166 {
167   const float pointReachedDistance = 0.25f;
168   const float pointReachedDistance2 = pointReachedDistance * pointReachedDistance;
169   const float maxSimStepTime = 0.015625f;
170   
171   float maxAcceleration = enemyClassConfigs[enemy->enemyType].maxAcceleration;
172   float maxSpeed = EnemyGetCurrentMaxSpeed(enemy);
173   int16_t nextX = enemy->nextX;
174   int16_t nextY = enemy->nextY;
175   Vector2 position = enemy->simPosition;
176   int passedCount = 0;
177   for (float t = 0.0f; t < deltaT; t += maxSimStepTime)
178   {
179     float stepTime = fminf(deltaT - t, maxSimStepTime);
180     Vector2 target = (Vector2){nextX, nextY};
181     float speed = Vector2Length(*velocity);
182     // draw the target position for debugging
183     //DrawCubeWires((Vector3){target.x, 0.2f, target.y}, 0.1f, 0.4f, 0.1f, RED);
184     Vector2 lookForwardPos = Vector2Add(position, Vector2Scale(*velocity, speed));
185     if (Vector2DistanceSqr(target, lookForwardPos) <= pointReachedDistance2)
186     {
187       // we reached the target position, let's move to the next waypoint
188       EnemyGetNextPosition(nextX, nextY, &nextX, &nextY);
189       target = (Vector2){nextX, nextY};
190       // track how many waypoints we passed
191       passedCount++;
192     }
193     
194     // acceleration towards the target
195     Vector2 unitDirection = Vector2Normalize(Vector2Subtract(target, lookForwardPos));
196     Vector2 acceleration = Vector2Scale(unitDirection, maxAcceleration * stepTime);
197     *velocity = Vector2Add(*velocity, acceleration);
198 
199     // limit the speed to the maximum speed
200     if (speed > maxSpeed)
201     {
202       *velocity = Vector2Scale(*velocity, maxSpeed / speed);
203     }
204 
205     // move the enemy
206     position = Vector2Add(position, Vector2Scale(*velocity, stepTime));
207   }
208 
209   if (waypointPassedCount)
210   {
211     (*waypointPassedCount) = passedCount;
212   }
213 
214   return position;
215 }
216 
217 void EnemyDraw()
218 {
219   rlDrawRenderBatchActive();
220   rlDisableDepthMask();
221   for (int i = 0; i < enemyCount; i++)
222   {
223     Enemy enemy = enemies[i];
224     if (enemy.enemyType == ENEMY_TYPE_NONE)
225     {
226       continue;
227     }
228 
229     Vector2 position = EnemyGetPosition(&enemy, gameTime.time - enemy.startMovingTime, &enemy.simVelocity, 0);
230     
231     // don't draw any trails for now; might replace this with footprints later
232     // if (enemy.movePathCount > 0)
233     // {
234     //   Vector3 p = {enemy.movePath[0].x, 0.2f, enemy.movePath[0].y};
235     //   DrawLine3D(p, (Vector3){position.x, 0.2f, position.y}, GREEN);
236     // }
237     // for (int j = 1; j < enemy.movePathCount; j++)
238     // {
239     //   Vector3 p = {enemy.movePath[j - 1].x, 0.2f, enemy.movePath[j - 1].y};
240     //   Vector3 q = {enemy.movePath[j].x, 0.2f, enemy.movePath[j].y};
241     //   DrawLine3D(p, q, GREEN);
242     // }
243 
244     float shieldHealth = enemyClassConfigs[enemy.enemyType].shieldHealth;
245     int phase = 0;
246     if (shieldHealth > 0 && enemy.shieldDamage < shieldHealth)
247     {
248       phase = SPRITE_UNIT_PHASE_WEAPON_SHIELD;
249     }
250 
251     switch (enemy.enemyType)
252     {
253     case ENEMY_TYPE_MINION:
254     case ENEMY_TYPE_RUNNER:
255     case ENEMY_TYPE_SHIELD:
256     case ENEMY_TYPE_BOSS:
257       DrawSpriteUnit(enemySprites[enemy.enemyType], (Vector3){position.x, 0.0f, position.y}, 
258         enemy.walkedDistance, 0, phase);
259       break;
260     }
261   }
262   rlDrawRenderBatchActive();
263   rlEnableDepthMask();
264 }
265 
266 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource)
267 {
268   // damage the tower
269   float explosionDamge = enemyClassConfigs[enemy->enemyType].explosionDamage;
270   float explosionRange = enemyClassConfigs[enemy->enemyType].explosionRange;
271   float explosionPushbackPower = enemyClassConfigs[enemy->enemyType].explosionPushbackPower;
272   float explosionRange2 = explosionRange * explosionRange;
273   tower->damage += enemyClassConfigs[enemy->enemyType].explosionDamage;
274   // explode the enemy
275   if (tower->damage >= TowerGetMaxHealth(tower))
276   {
277     tower->towerType = TOWER_TYPE_NONE;
278   }
279 
280   ParticleAdd(PARTICLE_TYPE_EXPLOSION, 
281     explosionSource, 
282     (Vector3){0, 0.1f, 0}, (Vector3){1.0f, 1.0f, 1.0f}, 1.0f);
283 
284   enemy->enemyType = ENEMY_TYPE_NONE;
285 
286   // push back enemies & dealing damage
287   for (int i = 0; i < enemyCount; i++)
288   {
289     Enemy *other = &enemies[i];
290     if (other->enemyType == ENEMY_TYPE_NONE)
291     {
292       continue;
293     }
294     float distanceSqr = Vector2DistanceSqr(enemy->simPosition, other->simPosition);
295     if (distanceSqr > 0 && distanceSqr < explosionRange2)
296     {
297       Vector2 direction = Vector2Normalize(Vector2Subtract(other->simPosition, enemy->simPosition));
298       other->simPosition = Vector2Add(other->simPosition, Vector2Scale(direction, explosionPushbackPower));
299       EnemyAddDamage(other, explosionDamge);
300     }
301   }
302 }
303 
304 void EnemyUpdate()
305 {
306   const float castleX = 0;
307   const float castleY = 0;
308   const float maxPathDistance2 = 0.25f * 0.25f;
309   
310   for (int i = 0; i < enemyCount; i++)
311   {
312     Enemy *enemy = &enemies[i];
313     if (enemy->enemyType == ENEMY_TYPE_NONE)
314     {
315       continue;
316     }
317 
318     int waypointPassedCount = 0;
319     Vector2 prevPosition = enemy->simPosition;
320     enemy->simPosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &enemy->simVelocity, &waypointPassedCount);
321     enemy->startMovingTime = gameTime.time;
322     enemy->walkedDistance += Vector2Distance(prevPosition, enemy->simPosition);
323     // track path of unit
324     if (enemy->movePathCount == 0 || Vector2DistanceSqr(enemy->simPosition, enemy->movePath[0]) > maxPathDistance2)
325     {
326       for (int j = ENEMY_MAX_PATH_COUNT - 1; j > 0; j--)
327       {
328         enemy->movePath[j] = enemy->movePath[j - 1];
329       }
330       enemy->movePath[0] = enemy->simPosition;
331       if (++enemy->movePathCount > ENEMY_MAX_PATH_COUNT)
332       {
333         enemy->movePathCount = ENEMY_MAX_PATH_COUNT;
334       }
335     }
336 
337     if (waypointPassedCount > 0)
338     {
339       enemy->currentX = enemy->nextX;
340       enemy->currentY = enemy->nextY;
341       if (EnemyGetNextPosition(enemy->currentX, enemy->currentY, &enemy->nextX, &enemy->nextY) &&
342         Vector2DistanceSqr(enemy->simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
343       {
344         // enemy reached the castle; remove it
345         enemy->enemyType = ENEMY_TYPE_NONE;
346         continue;
347       }
348     }
349   }
350 
351   // handle collisions between enemies
352   for (int i = 0; i < enemyCount - 1; i++)
353   {
354     Enemy *enemyA = &enemies[i];
355     if (enemyA->enemyType == ENEMY_TYPE_NONE)
356     {
357       continue;
358     }
359     for (int j = i + 1; j < enemyCount; j++)
360     {
361       Enemy *enemyB = &enemies[j];
362       if (enemyB->enemyType == ENEMY_TYPE_NONE)
363       {
364         continue;
365       }
366       float distanceSqr = Vector2DistanceSqr(enemyA->simPosition, enemyB->simPosition);
367       float radiusA = enemyClassConfigs[enemyA->enemyType].radius;
368       float radiusB = enemyClassConfigs[enemyB->enemyType].radius;
369       float radiusSum = radiusA + radiusB;
370       if (distanceSqr < radiusSum * radiusSum && distanceSqr > 0.001f)
371       {
372         // collision
373         float distance = sqrtf(distanceSqr);
374         float overlap = radiusSum - distance;
375         // move the enemies apart, but softly; if we have a clog of enemies,
376         // moving them perfectly apart can cause them to jitter
377         float positionCorrection = overlap / 5.0f;
378         Vector2 direction = (Vector2){
379             (enemyB->simPosition.x - enemyA->simPosition.x) / distance * positionCorrection,
380             (enemyB->simPosition.y - enemyA->simPosition.y) / distance * positionCorrection};
381         enemyA->simPosition = Vector2Subtract(enemyA->simPosition, direction);
382         enemyB->simPosition = Vector2Add(enemyB->simPosition, direction);
383       }
384     }
385   }
386 
387   // handle collisions between enemies and towers
388   for (int i = 0; i < enemyCount; i++)
389   {
390     Enemy *enemy = &enemies[i];
391     if (enemy->enemyType == ENEMY_TYPE_NONE)
392     {
393       continue;
394     }
395     enemy->contactTime -= gameTime.deltaTime;
396     if (enemy->contactTime < 0.0f)
397     {
398       enemy->contactTime = 0.0f;
399     }
400 
401     float enemyRadius = enemyClassConfigs[enemy->enemyType].radius;
402     // linear search over towers; could be optimized by using path finding tower map,
403     // but for now, we keep it simple
404     for (int j = 0; j < towerCount; j++)
405     {
406       Tower *tower = &towers[j];
407       if (tower->towerType == TOWER_TYPE_NONE)
408       {
409         continue;
410       }
411       float distanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){tower->x, tower->y});
412       float combinedRadius = enemyRadius + 0.708; // sqrt(0.5^2 + 0.5^2), corner-center distance of square with side length 1
413       if (distanceSqr > combinedRadius * combinedRadius)
414       {
415         continue;
416       }
417       // potential collision; square / circle intersection
418       float dx = tower->x - enemy->simPosition.x;
419       float dy = tower->y - enemy->simPosition.y;
420       float absDx = fabsf(dx);
421       float absDy = fabsf(dy);
422       Vector3 contactPoint = {0};
423       if (absDx <= 0.5f && absDx <= absDy) {
424         // vertical collision; push the enemy out horizontally
425         float overlap = enemyRadius + 0.5f - absDy;
426         if (overlap < 0.0f)
427         {
428           continue;
429         }
430         float direction = dy > 0.0f ? -1.0f : 1.0f;
431         enemy->simPosition.y += direction * overlap;
432         contactPoint = (Vector3){enemy->simPosition.x, 0.2f, tower->y + direction * 0.5f};
433       }
434       else if (absDy <= 0.5f && absDy <= absDx)
435       {
436         // horizontal collision; push the enemy out vertically
437         float overlap = enemyRadius + 0.5f - absDx;
438         if (overlap < 0.0f)
439         {
440           continue;
441         }
442         float direction = dx > 0.0f ? -1.0f : 1.0f;
443         enemy->simPosition.x += direction * overlap;
444         contactPoint = (Vector3){tower->x + direction * 0.5f, 0.2f, enemy->simPosition.y};
445       }
446       else
447       {
448         // possible collision with a corner
449         float cornerDX = dx > 0.0f ? -0.5f : 0.5f;
450         float cornerDY = dy > 0.0f ? -0.5f : 0.5f;
451         float cornerX = tower->x + cornerDX;
452         float cornerY = tower->y + cornerDY;
453         float cornerDistanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){cornerX, cornerY});
454         if (cornerDistanceSqr > enemyRadius * enemyRadius)
455         {
456           continue;
457         }
458         // push the enemy out along the diagonal
459         float cornerDistance = sqrtf(cornerDistanceSqr);
460         float overlap = enemyRadius - cornerDistance;
461         float directionX = cornerDistance > 0.0f ? (cornerX - enemy->simPosition.x) / cornerDistance : -cornerDX;
462         float directionY = cornerDistance > 0.0f ? (cornerY - enemy->simPosition.y) / cornerDistance : -cornerDY;
463         enemy->simPosition.x -= directionX * overlap;
464         enemy->simPosition.y -= directionY * overlap;
465         contactPoint = (Vector3){cornerX, 0.2f, cornerY};
466       }
467 
468       if (enemyClassConfigs[enemy->enemyType].explosionDamage > 0.0f)
469       {
470         enemy->contactTime += gameTime.deltaTime * 2.0f; // * 2 to undo the subtraction above
471         if (enemy->contactTime >= enemyClassConfigs[enemy->enemyType].requiredContactTime)
472         {
473           EnemyTriggerExplode(enemy, tower, contactPoint);
474         }
475       }
476     }
477   }
478 }
479 
480 EnemyId EnemyGetId(Enemy *enemy)
481 {
482   return (EnemyId){enemy - enemies, enemy->generation};
483 }
484 
485 Enemy *EnemyTryResolve(EnemyId enemyId)
486 {
487   if (enemyId.index >= ENEMY_MAX_COUNT)
488   {
489     return 0;
490   }
491   Enemy *enemy = &enemies[enemyId.index];
492   if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE)
493   {
494     return 0;
495   }
496   return enemy;
497 }
498 
499 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY)
500 {
501   Enemy *spawn = 0;
502   for (int i = 0; i < enemyCount; i++)
503   {
504     Enemy *enemy = &enemies[i];
505     if (enemy->enemyType == ENEMY_TYPE_NONE)
506     {
507       spawn = enemy;
508       break;
509     }
510   }
511 
512   if (enemyCount < ENEMY_MAX_COUNT && !spawn)
513   {
514     spawn = &enemies[enemyCount++];
515   }
516 
517   if (spawn)
518   {
519     *spawn = (Enemy){
520       .currentX = currentX,
521       .currentY = currentY,
522       .nextX = currentX,
523       .nextY = currentY,
524       .simPosition = (Vector2){currentX, currentY},
525       .simVelocity = (Vector2){0, 0},
526       .enemyType = enemyType,
527       .startMovingTime = gameTime.time,
528       .movePathCount = 0,
529       .walkedDistance = 0.0f,
530       .shieldDamage = 0.0f,
531       .damage = 0.0f,
532       .futureDamage = 0.0f,
533       .contactTime = 0.0f,
534       .generation = spawn->generation + 1,
535     };
536   }
537 
538   return spawn;
539 }
540 
541 int EnemyAddDamageRange(Vector2 position, float range, float damage)
542 {
543   int count = 0;
544   float range2 = range * range;
545   for (int i = 0; i < enemyCount; i++)
546   {
547     Enemy *enemy = &enemies[i];
548     if (enemy->enemyType == ENEMY_TYPE_NONE)
549     {
550       continue;
551     }
552     float distance2 = Vector2DistanceSqr(position, enemy->simPosition);
553     if (distance2 <= range2)
554     {
555       EnemyAddDamage(enemy, damage);
556       count++;
557     }
558   }
559   return count;
560 }
561 
562 int EnemyAddDamage(Enemy *enemy, float damage)
563 {
564   float shieldHealth = enemyClassConfigs[enemy->enemyType].shieldHealth;
565   if (shieldHealth > 0.0f && enemy->shieldDamage < shieldHealth)
566   {
567     float shieldDamageAbsorption = enemyClassConfigs[enemy->enemyType].shieldDamageAbsorption;
568     float shieldDamage = fminf(fminf(shieldDamageAbsorption, damage), shieldHealth - enemy->shieldDamage);
569     enemy->shieldDamage += shieldDamage;
570     damage -= shieldDamage;
571   }
572   enemy->damage += damage;
573   if (enemy->damage >= EnemyGetMaxHealth(enemy))
574   {
575     currentLevel->playerGold += enemyClassConfigs[enemy->enemyType].goldValue;
576     enemy->enemyType = ENEMY_TYPE_NONE;
577     return 1;
578   }
579 
580   return 0;
581 }
582 
583 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
584 {
585   int16_t castleX = 0;
586   int16_t castleY = 0;
587   Enemy* closest = 0;
588   int16_t closestDistance = 0;
589   float range2 = range * range;
590   for (int i = 0; i < enemyCount; i++)
591   {
592     Enemy* enemy = &enemies[i];
593     if (enemy->enemyType == ENEMY_TYPE_NONE)
594     {
595       continue;
596     }
597     float maxHealth = EnemyGetMaxHealth(enemy) + enemyClassConfigs[enemy->enemyType].shieldHealth;
598     if (enemy->futureDamage >= maxHealth)
599     {
600       // ignore enemies that will die soon
601       continue;
602     }
603     int16_t dx = castleX - enemy->currentX;
604     int16_t dy = castleY - enemy->currentY;
605     int16_t distance = abs(dx) + abs(dy);
606     if (!closest || distance < closestDistance)
607     {
608       float tdx = towerX - enemy->currentX;
609       float tdy = towerY - enemy->currentY;
610       float tdistance2 = tdx * tdx + tdy * tdy;
611       if (tdistance2 <= range2)
612       {
613         closest = enemy;
614         closestDistance = distance;
615       }
616     }
617   }
618   return closest;
619 }
620 
621 int EnemyCount()
622 {
623   int count = 0;
624   for (int i = 0; i < enemyCount; i++)
625   {
626     if (enemies[i].enemyType != ENEMY_TYPE_NONE)
627     {
628       count++;
629     }
630   }
631   return count;
632 }
633 
634 void EnemyDrawHealthbars(Camera3D camera)
635 {
636   for (int i = 0; i < enemyCount; i++)
637   {
638     Enemy *enemy = &enemies[i];
639     
640     float maxShieldHealth = enemyClassConfigs[enemy->enemyType].shieldHealth;
641     if (maxShieldHealth > 0.0f && enemy->shieldDamage < maxShieldHealth && enemy->shieldDamage > 0.0f)
642     {
643       float shieldHealth = maxShieldHealth - enemy->shieldDamage;
644       float shieldHealthRatio = shieldHealth / maxShieldHealth;
645       Vector3 position = (Vector3){enemy->simPosition.x, 0.5f, enemy->simPosition.y};
646       DrawHealthBar(camera, position, (Vector2) {.y = -4}, shieldHealthRatio, BLUE, 20.0f);
647     }
648 
649     if (enemy->enemyType == ENEMY_TYPE_NONE || enemy->damage == 0.0f)
650     {
651       continue;
652     }
653     Vector3 position = (Vector3){enemy->simPosition.x, 0.5f, enemy->simPosition.y};
654     float maxHealth = EnemyGetMaxHealth(enemy);
655     float health = maxHealth - enemy->damage;
656     float healthRatio = health / maxHealth;
657     
658     DrawHealthBar(camera, position, Vector2Zero(), healthRatio, GREEN, 15.0f);
659   }
660 }
  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 upgrade system is now working as intended. One can argue if the upgrade mechanic makes sense, but it certainly was a simple system to implement. One way or another, the changes done here are potentially useful for other approaches as well, e.g. if we chose a different upgrade system like a skill tree, improving the range or damage of a tower would require similar changes. Now that we have the upgrade system in place, we can look into how we could balance the game.

Conclusion

In this part, we implemented the tower upgrade system. While simple, it offers potentially some room for playing strategically.

While we display now also the range of the tower, we currently do not display the damage or rate of fire. This is something we could add in the future.

In the next part, the focus will look into parsing the game configuration from a file.

🍪