Simple tower defense tutorial, part 17: Tower upgrading, 1/2

Upgrading towers is an important aspect in most tower defense games. There are plenty of mechanics like merge mechanics, science trees, items, etc.

For this part, I want to establish the foundation to allow upgrading towers by clicking on towers and selecting options from a context menu. We thus need to implement a context menu first.

When tapping on a free cell, we offer options to build a tower. The tower can still be moved around by dragging it, but the initial position is the cell where the context menu was opened. This way, we can simplify the UI.

When tapping on a tower, we offer options to upgrade, repair or sell the tower.

Let's start with the context menu for building towers:

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

That was not too hard. The most complex part is to handle the mouse inputs: When changing the state of the context menu ( level->placementContextMenuStatus ), we have to do that in the right order. This would not be necessary if we had a nextStatus field that is used to transition - a reminder that mutability makes things more complicated. I might change that in the future in case this becomes more complicated.

Context menu for building towers

So far, the implementation is still quite simple though, considering that there is no UI framework at play, just a few rectangles and text. I would for now continue this way. At a later point, we want to improve the design, maybe it makes then sense to formalize a few things.

The next step is to trigger a different menu when clicking on a tower. For the castle at 0,0, we currently have nothing to offer, so let's just show some stats.

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

For the purpose of testing, the towers spawn here with some damage already. The context menu looks like this:

Context menus for spawning, repairing and selling action demo
Demonstration how spawning and repairing and selling towers works

The code is a bit messy here due to the integration of drawing and event handling in one go. Abstracting this could help to make this more sane. But before doing that, it is not the worst idea to try out the most straightforward way to be able to judge what is needed and if it is worth it. I once made the experiment of comparing an abstraction I wrote with a simple implementation and it turned out that the simple implementation was shorter and easier to understand. It's a healthy exercise to do this from time to time. It can be quite sobering.

One thing you might find confusing is the usage of macros here to shorten some function arguments, so let's have a look at this:

  1 const int itemX = contextMenu.x + itemSpacing;
  2 const int itemWidth = contextMenu.width - itemSpacing * 2;
  3 #define ITEM_Y(idx) (contextMenu.y + itemSpacing + (itemHeight + itemSpacing) * idx)
  4 #define ITEM_RECT(idx, marginLR) itemX + (marginLR), ITEM_Y(idx), itemWidth - (marginLR) * 2, itemHeight
  5 if (tower == 0)
  6 {
  7   if (
  8     DrawBuildingBuildButton(level, ITEM_RECT(0, 0), TOWER_TYPE_WALL, "Wall") ||
  9     DrawBuildingBuildButton(level, ITEM_RECT(1, 0), TOWER_TYPE_ARCHER, "Archer") ||
 10     DrawBuildingBuildButton(level, ITEM_RECT(2, 0), TOWER_TYPE_BALLISTA, "Ballista") ||
 11     DrawBuildingBuildButton(level, ITEM_RECT(3, 0), TOWER_TYPE_CATAPULT, "Catapult"))
 12   {
 13     level->placementContextMenuStatus = 0;
 14   }
 15 }
A macro (or define) in C is a mechanism to instruct the preprocessor to replace a certain text with another text. This is a simple text replacement and can be used to generate code that would be tedious to write by hand. Since it happens at compile time, it does not have a direct impact on the performance of the program other than the possible increase in the size of the binary due to generating code instead of wrapping it in a function that is called.

Macros are pure text replacements. There is no syntax checking or type checking in the macro body. Only the final result is checked by the compiler.

Using macros is considered to be a double-edged sword. It can make the code harder to debug and understand, but it can also make the code more readable and maintainable.

Also, let's compare it with the previous code we had for the context menu:

  1 if (tower == 0)
  2 {
  3   if (
  4     DrawBuildingBuildButton(level, contextMenu.x + 5, contextMenu.y + 5, contextMenu.width - 10, 30, TOWER_TYPE_WALL, "Wall") ||
  5     DrawBuildingBuildButton(level, contextMenu.x + 5, contextMenu.y + 40, contextMenu.width - 10, 30, TOWER_TYPE_ARCHER, "Archer") ||
  6     DrawBuildingBuildButton(level, contextMenu.x + 5, contextMenu.y + 75, contextMenu.width - 10, 30, TOWER_TYPE_BALLISTA, "Ballista") ||
  7     DrawBuildingBuildButton(level, contextMenu.x + 5, contextMenu.y + 110, contextMenu.width - 10, 30, TOWER_TYPE_CATAPULT, "Catapult"))
  8   {
  9     level->placementContextMenuStatus = 0;
 10   }
 11 }

The first obvious difference is, that the macro version is shorter. The second difference is, that the original version used hand calculated values for the y position, while the macro version uses a formula. This makes it easier to adjust the spacing between the items when needed or inserting new items. The brevitity of the macro version also makes it easier to read.

What is a bit special is, that the ITEM_RECT inserts all 4 arguments into the function call. This is a bit unusual, but it is a useful trick to know.

Tower upgrading

Now that we have the context menu for towers, we can think about how to implement the upgrading. As the game designer noob I am, I will simply pick a few rules out of the blue:

Since the height of the context menu is limited to half the screen size and we have a lot of items to display, we need to organize the items in submenus. Some options would also benefit from a confirmation dialog, like when selling a tower. This is a point where reworking the code a little is necessary to avoid making this a total mess. So let's first come up with a structure and a function that can handle the drawing and event handling of the context menu without changing the behavior:

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

It mostly looks and feels the same now, but there is now a small instruction in place to draw the context menu and handle its various options:

The next step is to implement different submenus for the tower upgrades. Let's start with the sell confirmation dialog:

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

The sell confirmation dialog is a simple yes/no dialog:

Sell confirmation dialog

It appears that the generic context menu handling function can also be used for simple dialogue boxes. We could even improve the function to allow multiple buttons in one line, essentially creating a simple layouting system. But for now, let's keep it simple.

What would be nice to have is to make the UI a little more appealing.

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

This involved a couple of changes here and there, but nothing too big in general.

The first update was the sprite atlas to include some UI elements:

Sprite atlas with UI elements
Sprite atlas with UI elements on the right

On the right, there is a panel that can be used for panel UI elements and furthermore there are 4 button variants that represent the different states of a button:

The pressed state is only active for a single frame, so it does not fit in so well in the current way how buttons work here, but it might be useful in the future.

In order to scale the buttons and the panel, we use the 9-slice scaling technique. In raylib, this can be configured via the NPatchInfo struct and drawn with DrawTextureNPatch. When drawing a texture with 9-slice scaling, the texture is divided into 9 parts: 4 corners, 4 edges and 1 center. The corners are drawn as they are, the edges are stretched and the center is stretched in both directions. It's a simple way to create scalable UI elements that has been around for a long time.

Here is a raylib example that demonstrates the 9-slice scaling feature.

While it could be fun to continue working out the visual design of the UI, this is still too early and there are many more important things we want to implement more sooner than later.

Conclusion

We have now a generic context menu that can be utilized for different purposes. The tower selection for building new towers is now cleaner. Using the context menu system for a confirmation dialog works well. The next step is to implement the tower upgrade mechanic and its UI. This will be the topic of the next part.

🍪