Simple tower defense tutorial, part 21: Config loader

In the previous part we implemented a simple parser for the game configuration.

The plan for this part is to integrate the parser into the game and load the configuration from a file.

The last sample in this post should allow modifying the configuration file and reloading the game without recompiling it in the web browser.

Loading the configuration

The first step is to load the configuration file when the game starts. The parser needs a few modifications to cooperate with the game, but in general, it is almost the same as in the previous post.

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

The next step is to allow reloading the configuration file without restarting the game in the web version - the desktop version allows this by default since it loads the config when resetting the level.

The way I want this to work is that the when the game starts in the web version, it creates a text area with the content of the configuration file. The player can modify the configuration and press a button to reload the game with the new configuration.

I will hide from now on the files that are not important for the part, so the files shown here are just a selection.

A new file is the html-edit.js file, which handles the editing part in the browser:

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

So now we have a way to modify the game configuration in the browser version!

There were just a few... workarounds necessary to make this work:

The GLFW emscripten implementation wants to prevent site navigation events getting triggered by pressing TAB or BACKSPACE. This is was pretty nasty to find a workaround for since this is deeply integrated into the GLFW emscripten implementation. I ended up overloading the window.addEventListener function to wrap functions that for keydown events and replacing the event.preventDefault function with a function that does nothing. This is now a script that is included in the HTML file of this page, so it is not very portable... but at least it works for this setup.

Now that we have a simple text editor in the browser, we can make a few improvements. Highlighting the line that contains an error is for example something pretty important to ease editing. There are quite a few JavaScript/HTML powered editors that could make this editor looks and feel like a full editor, but I want to keep things as minimal as possible.

So lets add some error logging and line highlighting.

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

After a few changes in the html-edit.js file, the web editor looks fairly good. The logging panel shows all the logs coming from the game, and when there is an error in the configuration file, the line number gutter will highlight the line with the error.

So technically, we can now create a level with all the waves and enemies we want without recompiling the game. What's next?

Next steps

The goal is still "balancing the game". For that purpose, I want to extend our config file with some more options: Adding a test section that allows setting up a test scenario - so specifying which buildings are built and upgraded between the waves. Simulating each wave takes only a few milliseconds, so the idea here is that we can specify a number range for certain parameters and let the game simulate all the combinations, logging each outcome in a way so we can visualize the results in the browser.

So in the next part, we will add this test section and implement the simulation part.

🍪