Simple tower defense tutorial, part 16: Adding new enemey types.

Originally I wanted to make this a post about balancing the game, but there is still a lack of enemies and rules to provide a good challenge. So, let's add some enemies first that have some special rules.

The first change we do is to add new ids to the game as well as defining a weapon for the minion.

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

Just adding the weapon to the minion to make it more unique reveals two rendering issues:

Render order of the game objects.

The weapon disappears behind another enemy sprite: This happens because the sprite is drawn in whatever order the enemies are in the list and the sprites draw to the z-buffer. When now another sprite is rendered behind, it will be drawn over the weapon.

There are multiple ways to fix this:

Since we focus here on simplicity, I will first try the last option and see if it works. Sorting the sprites would be my second choice, but that would require a few more changes in the code.

The second problem is that there are lines on the edges of the sprites. This is due to the texture filtering and is called "bleeding" (see here).

Here is a picture of the sprite sheet and how the sprites are drawn:

Pixel color bleeding.

The source rectangles from our atlas texture have not enough space between the sprites (padding), so the texture filtering is interpolating the colors of the adjacent pixels belonging to other sprites. Texture bleeding can be observed in may different situations. Light mapping is often suffers from the same problem.

To solve this probleme here, we only have to add a small margin to the sprites in the atlas texture. In this specific case, I can simply select a smaller rectangle from the sprite, since the sprites we use here are not using the entire space.

But in the future when we're adding new sprites, we have to keep this in mind.

Since we are not using mipmaps, a distance of 1 pixel is enough. As always, the topic can be quite complex; there are various different solutions to this problem each with different trade-offs. Luckily, we can ignore this for now.

In case you don't know what mipmaps are, you can read it up here.

Anyway, let's try out how the enemies look without depth writing and with a margin around the sprites.

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

Zooming in, we can see the difference:

Render order and bleeding fixed.

The bleeding is gone and the render order looks good - moving the sword from the left to the right hand helps to reduce the overlapping. Since the enemies have a radius and push each other away, overlapping is therefore only happen in rare cases, so this should be good enough for now.

One important bit: When changing the rendering state (calling rlDisableDepthMask()), we have to first flush the batch to make sure the sprites are drawn in the correct order. This is what the rlDrawRenderBatchActive(); call does. If we don't do this, the render state change may not be applied until the batch is flushed (raylib is internally batching the draw calls).

One more missing thing is, that the weapon has also an animation, so let's add that as well.

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

Animated swords.

We can now use the minion without the weapon as the runner enemy. But maybe, we should refine the approach to describe sprite animations for the units. Just look at how the struct has grown:

  1 typedef struct SpriteUnit
  2 {
  3   Rectangle srcRect;
  4   Vector2 offset;
  5   int frameCount;
  6   float frameDuration;
  7   Rectangle srcWeaponIdleRect;
  8   Vector2 srcWeaponIdleOffset;
  9   int srcWeaponIdleFrameCount;
 10   int srcWeaponIdleFrameWidth;
 11   float srcWeaponIdleFrameDuration;
 12   Rectangle srcWeaponCooldownRect;
 13   Vector2 srcWeaponCooldownOffset;
 14 } SpriteUnit;

It isn't difficult to spot that there is a lot of repetition in the struct. It is basically a bunch of rectangles and offsets. Adding more and more such custom variables will make this struct and the code using it needlessly complex. So let's refactor this into a more generic struct that can handle multiple overlays and animations. A fixed number of overlays is good enough for our game:

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

There is now a new SpriteAnimation struct that describes a single sprite animation source:

  1 typedef struct SpriteAnimation
  2 {
  3   Rectangle srcRect;
  4   Vector2 offset;
  5   uint8_t animationId;
  6   uint8_t frameCount;
  7   uint8_t frameWidth;
  8   float frameDuration;
  9 } SpriteAnimation;

This used to be a part of the SpriteUnit - which looks now very simple:

  1 typedef struct SpriteUnit
  2 {
  3   SpriteAnimation animations[SPRITE_UNIT_ANIMATION_COUNT];
  4 } SpriteUnit;

It is now greatly simplified and is just a number of animations. Creating animations for a unit is now a description of the different animations that are drawn on top of each other:

  1 SpriteUnit enemySprites[] = {
  2   [ENEMY_TYPE_MINION] = {
  3     .animations[0] = { // the minion body
  4       .srcRect = {0, 17, 16, 15},
  5       .offset = {8.0f, 0.0f},
  6       .frameCount = 6,
  7       .frameDuration = 0.1f,
  8     },
  9     .animations[1] = { // the sword
 10       .srcRect = {1, 33, 15, 14},
 11       .offset = {7.0f, 0.0f},
 12       .frameCount = 6,
 13       .frameWidth = 16,
 14       .frameDuration = 0.1f,
 15     },
 16   },
 17   ...
 18 }

It is simple to see that with this approach that we can make up enemies with multiple overlays and animations. But the best part is how the code looks like that draws the unit sprite - let's compare it - first the new version:

  1 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase)
  2 {
  3   float xScale = flip ? -1.0f : 1.0f;
  4   Camera3D camera = currentLevel->camera;
  5   float size = 0.5f;
  6   // we want the sprite to face the camera, so we need to calculate the up vector
  7   Vector3 forward = Vector3Subtract(camera.target, camera.position);
  8   Vector3 up = {0, 1, 0};
  9   Vector3 right = Vector3CrossProduct(forward, up);
 10   up = Vector3Normalize(Vector3CrossProduct(right, forward));
 11   
 12   for (int i=0;i<SPRITE_UNIT_ANIMATION_COUNT;i++)
 13   {
 14     SpriteAnimation anim = unit.animations[i];
 15     if (anim.animationId != phase && anim.animationId != 0)
 16     {
 17       continue;
 18     }
 19     Rectangle srcRect = anim.srcRect;
 20     if (anim.frameCount > 1)
 21     {
 22       int w = anim.frameWidth > 0 ? anim.frameWidth : srcRect.width;
 23       srcRect.x += (int)(t / anim.frameDuration) % anim.frameCount * w;
 24     }
 25     Vector2 offset = { anim.offset.x / 16.0f * size, anim.offset.y / 16.0f * size * xScale };
 26     Vector2 scale = { srcRect.width / 16.0f * size, srcRect.height / 16.0f * size };
 27     
 28     if (flip)
 29     {
 30       srcRect.x += srcRect.width;
 31       srcRect.width = -srcRect.width;
 32       offset.x = scale.x - offset.x;
 33     }
 34     DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
 35     // move the sprite slightly towards the camera to avoid z-fighting
 36     position = Vector3Add(position, Vector3Scale(Vector3Normalize(forward), -0.01f));  
 37   }
 38 }

Noteworthy is this part:

if (anim.animationId != phase && anim.animationId != 0) { continue; }

... which is ignoring SpriteAnimation elements that have an animationId set and that does not match the current phase. This is how we can have units that draw different animations depending on the phase of the unit - e.g. for the archers. Not the most elegant approach, but it works for now!

And the old version:

  1 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase)
  2 {
  3   float xScale = flip ? -1.0f : 1.0f;
  4   Camera3D camera = currentLevel->camera;
  5   float size = 0.5f;
  6   Vector2 offset = (Vector2){ unit.offset.x / 16.0f * size, unit.offset.y / 16.0f * size * xScale };
  7   Vector2 scale = (Vector2){ unit.srcRect.width / 16.0f * size, unit.srcRect.height / 16.0f * size };
  8   // we want the sprite to face the camera, so we need to calculate the up vector
  9   Vector3 forward = Vector3Subtract(camera.target, camera.position);
 10   Vector3 up = {0, 1, 0};
 11   Vector3 right = Vector3CrossProduct(forward, up);
 12   up = Vector3Normalize(Vector3CrossProduct(right, forward));
 13 
 14   Rectangle srcRect = unit.srcRect;
 15   if (unit.frameCount > 1)
 16   {
 17     srcRect.x += (int)(t / unit.frameDuration) % unit.frameCount * srcRect.width;
 18   }
 19   if (flip)
 20   {
 21     srcRect.x += srcRect.width;
 22     srcRect.width = -srcRect.width;
 23   }
 24   DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
 25   // move the sprite slightly towards the camera to avoid z-fighting
 26   position = Vector3Add(position, Vector3Scale(Vector3Normalize(forward), -0.01f));
 27 
 28   if (phase == SPRITE_UNIT_PHASE_WEAPON_COOLDOWN && unit.srcWeaponCooldownRect.width > 0)
 29   {
 30     offset = (Vector2){ unit.srcWeaponCooldownOffset.x / 16.0f * size, unit.srcWeaponCooldownOffset.y / 16.0f * size };
 31     scale = (Vector2){ unit.srcWeaponCooldownRect.width / 16.0f * size, unit.srcWeaponCooldownRect.height / 16.0f * size };
 32     srcRect = unit.srcWeaponCooldownRect;
 33     if (flip)
 34     {
 35       // position.x = flip * scale.x * 0.5f;
 36       srcRect.x += srcRect.width;
 37       srcRect.width = -srcRect.width;
 38       offset.x = scale.x - offset.x;
 39     }
 40     DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
 41   }
 42   else if (phase == SPRITE_UNIT_PHASE_WEAPON_IDLE && unit.srcWeaponIdleRect.width > 0)
 43   {
 44     offset = (Vector2){ unit.srcWeaponIdleOffset.x / 16.0f * size, unit.srcWeaponIdleOffset.y / 16.0f * size };
 45     scale = (Vector2){ unit.srcWeaponIdleRect.width / 16.0f * size, unit.srcWeaponIdleRect.height / 16.0f * size };
 46     srcRect = unit.srcWeaponIdleRect;
 47     if (flip)
 48     {
 49       // position.x = flip * scale.x * 0.5f;
 50       srcRect.x += srcRect.width;
 51       srcRect.width = -srcRect.width;
 52       offset.x = scale.x - offset.x;
 53     }
 54     if (unit.srcWeaponIdleFrameCount > 1)
 55     {
 56       int w = unit.srcWeaponIdleFrameWidth > 0 ? unit.srcWeaponIdleFrameWidth : srcRect.width;
 57       srcRect.x += (int)(t / unit.srcWeaponIdleFrameDuration) % unit.srcWeaponIdleFrameCount * w;
 58     }
 59     DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
 60   }

Comparing the changes, we can see that setting up a unit sprite is more structured now and the code to draw it has become significantly shorter and simpler.

Both should be useful when adding more units and features to the game. We might later want to add more effects to the sprite animations, like scaling, rotation, or color changes and with this approach, this should be easier to do. Later.

Now, let's add the new enemies and let them spawn to see how they look like.

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

Different enemies in the game.

The graphics are now good enough for current state. The next rule to implement is to give the shield its own hitpoints and apply a damage reduction. The idea is that the the ballista's high damage is piercing the shield and the minion behind it while arrow attacks only damage the shield. When the shield is destroyed, the shield should now longer be drawn.

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

Shield with health bar.
The shield has a health bar that appears when the shield is hit.

Conclusion

We added 3 different new types of enemies in this part of the tutorial. The shield enemy adds a first new flavor that tastes like rock-paper-scissors.

The catapult tower could play out its area damage against groups of enemies, but the current wave configuration does not allow for this.

In the next part, we'll add a tower upgrade system and refine the UI a bit.

🍪