Simple tower defense tutorial, part 11: Enemy path visualizations
In the last part, we added two new towers to the game. At the moment these are, visually speaking, just static boxes, but are at least functional. But there is some fundamental issue with the gameplay: While things work on a mechanical level, the player currently can't anticipate how the enemies will move through the level as tower placement can change this a lot, making planning impossible.
So in this part, we should add a visualization of the path the enemies will take and allowing to preview how the path will change before placing new towers.
Path visualization
Our pathfinding system provides only the next cell the enemy will move to. But this way we can draw any path by simply starting with a point.
Let's just start drawing some cubes along the way the enemies will move, beginning from the spawn point, before thinking much about how to visualize the path:
1 #include "td_main.h"
2 #include <raymath.h>
3 #include <stdlib.h>
4 #include <math.h>
5
6 //# Variables
7 GUIState guiState = {0};
8 GameTime gameTime = {0};
9
10 Model floorTileAModel = {0};
11 Model floorTileBModel = {0};
12 Model treeModel[2] = {0};
13 Model firTreeModel[2] = {0};
14 Model rockModels[5] = {0};
15 Model grassPatchModel[1] = {0};
16
17 Texture2D palette, spriteSheet;
18
19 Level levels[] = {
20 [0] = {
21 .state = LEVEL_STATE_BUILDING,
22 .initialGold = 20,
23 .waves[0] = {
24 .enemyType = ENEMY_TYPE_MINION,
25 .wave = 0,
26 .count = 10,
27 .interval = 2.5f,
28 .delay = 1.0f,
29 .spawnPosition = {0, 6},
30 },
31 .waves[1] = {
32 .enemyType = ENEMY_TYPE_MINION,
33 .wave = 1,
34 .count = 20,
35 .interval = 1.5f,
36 .delay = 1.0f,
37 .spawnPosition = {0, 6},
38 },
39 .waves[2] = {
40 .enemyType = ENEMY_TYPE_MINION,
41 .wave = 2,
42 .count = 30,
43 .interval = 1.2f,
44 .delay = 1.0f,
45 .spawnPosition = {0, 6},
46 }
47 },
48 };
49
50 Level *currentLevel = levels;
51
52 //# Game
53
54 static Model LoadGLBModel(char *filename)
55 {
56 Model model = LoadModel(TextFormat("data/%s.glb",filename));
57 if (model.materialCount > 1)
58 {
59 model.materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
60 }
61 return model;
62 }
63
64 void LoadAssets()
65 {
66 // load a sprite sheet that contains all units
67 spriteSheet = LoadTexture("data/spritesheet.png");
68 SetTextureFilter(spriteSheet, TEXTURE_FILTER_BILINEAR);
69
70 // we'll use a palette texture to colorize the all buildings and environment art
71 palette = LoadTexture("data/palette.png");
72 // The texture uses gradients on very small space, so we'll enable bilinear filtering
73 SetTextureFilter(palette, TEXTURE_FILTER_BILINEAR);
74
75 floorTileAModel = LoadGLBModel("floor-tile-a");
76 floorTileBModel = LoadGLBModel("floor-tile-b");
77 treeModel[0] = LoadGLBModel("leaftree-large-1-a");
78 treeModel[1] = LoadGLBModel("leaftree-large-1-b");
79 firTreeModel[0] = LoadGLBModel("firtree-1-a");
80 firTreeModel[1] = LoadGLBModel("firtree-1-b");
81 rockModels[0] = LoadGLBModel("rock-1");
82 rockModels[1] = LoadGLBModel("rock-2");
83 rockModels[2] = LoadGLBModel("rock-3");
84 rockModels[3] = LoadGLBModel("rock-4");
85 rockModels[4] = LoadGLBModel("rock-5");
86 grassPatchModel[0] = LoadGLBModel("grass-patch-1");
87 }
88
89 void InitLevel(Level *level)
90 {
91 level->seed = (int)(GetTime() * 100.0f);
92
93 TowerInit();
94 EnemyInit();
95 ProjectileInit();
96 ParticleInit();
97 TowerTryAdd(TOWER_TYPE_BASE, 0, 0);
98
99 level->placementMode = 0;
100 level->state = LEVEL_STATE_BUILDING;
101 level->nextState = LEVEL_STATE_NONE;
102 level->playerGold = level->initialGold;
103 level->currentWave = 0;
104
105 Camera *camera = &level->camera;
106 camera->position = (Vector3){4.0f, 8.0f, 8.0f};
107 camera->target = (Vector3){0.0f, 0.0f, 0.0f};
108 camera->up = (Vector3){0.0f, 1.0f, 0.0f};
109 camera->fovy = 10.0f;
110 camera->projection = CAMERA_ORTHOGRAPHIC;
111 }
112
113 void DrawLevelHud(Level *level)
114 {
115 const char *text = TextFormat("Gold: %d", level->playerGold);
116 Font font = GetFontDefault();
117 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 120, 10}, font.baseSize * 2.0f, 2.0f, BLACK);
118 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 122, 8}, font.baseSize * 2.0f, 2.0f, YELLOW);
119 }
120
121 void DrawLevelReportLostWave(Level *level)
122 {
123 BeginMode3D(level->camera);
124 DrawLevelGround(level);
125 TowerDraw();
126 EnemyDraw();
127 ProjectileDraw();
128 ParticleDraw();
129 guiState.isBlocked = 0;
130 EndMode3D();
131
132 TowerDrawHealthBars(level->camera);
133
134 const char *text = "Wave lost";
135 int textWidth = MeasureText(text, 20);
136 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
137
138 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
139 {
140 level->nextState = LEVEL_STATE_RESET;
141 }
142 }
143
144 int HasLevelNextWave(Level *level)
145 {
146 for (int i = 0; i < 10; i++)
147 {
148 EnemyWave *wave = &level->waves[i];
149 if (wave->wave == level->currentWave)
150 {
151 return 1;
152 }
153 }
154 return 0;
155 }
156
157 void DrawLevelReportWonWave(Level *level)
158 {
159 BeginMode3D(level->camera);
160 DrawLevelGround(level);
161 TowerDraw();
162 EnemyDraw();
163 ProjectileDraw();
164 ParticleDraw();
165 guiState.isBlocked = 0;
166 EndMode3D();
167
168 TowerDrawHealthBars(level->camera);
169
170 const char *text = "Wave won";
171 int textWidth = MeasureText(text, 20);
172 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
173
174
175 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
176 {
177 level->nextState = LEVEL_STATE_RESET;
178 }
179
180 if (HasLevelNextWave(level))
181 {
182 if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
183 {
184 level->nextState = LEVEL_STATE_BUILDING;
185 }
186 }
187 else {
188 if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
189 {
190 level->nextState = LEVEL_STATE_WON_LEVEL;
191 }
192 }
193 }
194
195 void DrawBuildingBuildButton(Level *level, int x, int y, int width, int height, uint8_t towerType, const char *name)
196 {
197 static ButtonState buttonStates[8] = {0};
198 int cost = GetTowerCosts(towerType);
199 const char *text = TextFormat("%s: %d", name, cost);
200 buttonStates[towerType].isSelected = level->placementMode == towerType;
201 buttonStates[towerType].isDisabled = level->playerGold < cost;
202 if (Button(text, x, y, width, height, &buttonStates[towerType]))
203 {
204 level->placementMode = buttonStates[towerType].isSelected ? 0 : towerType;
205 }
206 }
207
208 float GetRandomFloat(float min, float max)
209 {
210 int random = GetRandomValue(0, 0xfffffff);
211 return ((float)random / (float)0xfffffff) * (max - min) + min;
212 }
213
214 void DrawLevelGround(Level *level)
215 {
216 // draw checkerboard ground pattern
217 for (int x = -5; x <= 5; x += 1)
218 {
219 for (int y = -5; y <= 5; y += 1)
220 {
221 Model *model = (x + y) % 2 == 0 ? &floorTileAModel : &floorTileBModel;
222 DrawModel(*model, (Vector3){x, 0.0f, y}, 1.0f, WHITE);
223 }
224 }
225
226 int oldSeed = GetRandomValue(0, 0xfffffff);
227 SetRandomSeed(level->seed);
228 // increase probability for trees via duplicated entries
229 Model borderModels[64];
230 int maxRockCount = GetRandomValue(2, 6);
231 int maxTreeCount = GetRandomValue(10, 20);
232 int maxFirTreeCount = GetRandomValue(5, 10);
233 int maxLeafTreeCount = maxTreeCount - maxFirTreeCount;
234 int grassPatchCount = GetRandomValue(5, 30);
235
236 int modelCount = 0;
237 for (int i = 0; i < maxRockCount && modelCount < 63; i++)
238 {
239 borderModels[modelCount++] = rockModels[GetRandomValue(0, 5)];
240 }
241 for (int i = 0; i < maxLeafTreeCount && modelCount < 63; i++)
242 {
243 borderModels[modelCount++] = treeModel[GetRandomValue(0, 1)];
244 }
245 for (int i = 0; i < maxFirTreeCount && modelCount < 63; i++)
246 {
247 borderModels[modelCount++] = firTreeModel[GetRandomValue(0, 1)];
248 }
249 for (int i = 0; i < grassPatchCount && modelCount < 63; i++)
250 {
251 borderModels[modelCount++] = grassPatchModel[0];
252 }
253
254 // draw some objects around the border of the map
255 Vector3 up = {0, 1, 0};
256 // a pseudo random number generator to get the same result every time
257 const float wiggle = 0.75f;
258 const int layerCount = 3;
259 for (int layer = 0; layer < layerCount; layer++)
260 {
261 int layerPos = 6 + layer;
262 for (int x = -6 + layer; x <= 6 + layer; x += 1)
263 {
264 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)],
265 (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, -layerPos + GetRandomFloat(0.0f, wiggle)},
266 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
267 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)],
268 (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, layerPos + GetRandomFloat(0.0f, wiggle)},
269 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
270 }
271
272 for (int z = -5 + layer; z <= 5 + layer; z += 1)
273 {
274 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)],
275 (Vector3){-layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)},
276 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
277 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)],
278 (Vector3){layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)},
279 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
280 }
281 }
282
283 SetRandomSeed(oldSeed);
284 }
285
286 void DrawEnemyPath(Level *level)
287 {
288 for (int i = 0; i < ENEMY_MAX_WAVE_COUNT; i++)
289 {
290 EnemyWave *wave = &level->waves[i];
291 if (wave->wave != level->currentWave)
292 {
293 continue;
294 }
295 Vector3 position = (Vector3){wave->spawnPosition.x, 0.0f, wave->spawnPosition.y};
296 DrawCube(position, 0.25f, 2.0f, 0.25f, RED);
297 for (int j = 0; j < 100; j++)
298 {
299 Vector2 gradient = PathFindingGetGradient(position);
300 if (gradient.x == 0 && gradient.y == 0)
301 {
302 break;
303 }
304 position.x += gradient.x;
305 position.z += gradient.y;
306 DrawCube(position, 0.2f, 1.25f, 0.2f, ORANGE);
307 }
308 }
309 }
310
311 void DrawLevelBuildingState(Level *level)
312 {
313 BeginMode3D(level->camera);
314 DrawLevelGround(level);
315 DrawEnemyPath(level);
316 TowerDraw();
317 EnemyDraw();
318 ProjectileDraw();
319 ParticleDraw();
320
321 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera);
322 float planeDistance = ray.position.y / -ray.direction.y;
323 float planeX = ray.direction.x * planeDistance + ray.position.x;
324 float planeY = ray.direction.z * planeDistance + ray.position.z;
325 int16_t mapX = (int16_t)floorf(planeX + 0.5f);
326 int16_t mapY = (int16_t)floorf(planeY + 0.5f);
327 if (level->placementMode && !guiState.isBlocked && mapX >= -5 && mapX <= 5 && mapY >= -5 && mapY <= 5)
328 {
329 DrawCubeWires((Vector3){mapX, 0.2f, mapY}, 1.0f, 0.4f, 1.0f, RED);
330 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
331 {
332 if (TowerTryAdd(level->placementMode, mapX, mapY))
333 {
334 level->playerGold -= GetTowerCosts(level->placementMode);
335 level->placementMode = TOWER_TYPE_NONE;
336 }
337 }
338 }
339
340 guiState.isBlocked = 0;
341
342 EndMode3D();
343
344 TowerDrawHealthBars(level->camera);
345
346 static ButtonState buildWallButtonState = {0};
347 static ButtonState buildGunButtonState = {0};
348 buildWallButtonState.isSelected = level->placementMode == TOWER_TYPE_WALL;
349 buildGunButtonState.isSelected = level->placementMode == TOWER_TYPE_ARCHER;
350
351 DrawBuildingBuildButton(level, 10, 10, 110, 30, TOWER_TYPE_WALL, "Wall");
352 DrawBuildingBuildButton(level, 10, 50, 110, 30, TOWER_TYPE_ARCHER, "Archer");
353 DrawBuildingBuildButton(level, 10, 90, 110, 30, TOWER_TYPE_BALLISTA, "Ballista");
354 DrawBuildingBuildButton(level, 10, 130, 110, 30, TOWER_TYPE_CATAPULT, "Catapult");
355
356 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
357 {
358 level->nextState = LEVEL_STATE_RESET;
359 }
360
361 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0))
362 {
363 level->nextState = LEVEL_STATE_BATTLE;
364 }
365
366 const char *text = "Building phase";
367 int textWidth = MeasureText(text, 20);
368 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
369 }
370
371 void InitBattleStateConditions(Level *level)
372 {
373 level->state = LEVEL_STATE_BATTLE;
374 level->nextState = LEVEL_STATE_NONE;
375 level->waveEndTimer = 0.0f;
376 for (int i = 0; i < 10; i++)
377 {
378 EnemyWave *wave = &level->waves[i];
379 wave->spawned = 0;
380 wave->timeToSpawnNext = wave->delay;
381 }
382 }
383
384 void DrawLevelBattleState(Level *level)
385 {
386 BeginMode3D(level->camera);
387 DrawLevelGround(level);
388 TowerDraw();
389 EnemyDraw();
390 ProjectileDraw();
391 ParticleDraw();
392 guiState.isBlocked = 0;
393 EndMode3D();
394
395 EnemyDrawHealthbars(level->camera);
396 TowerDrawHealthBars(level->camera);
397
398 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
399 {
400 level->nextState = LEVEL_STATE_RESET;
401 }
402
403 int maxCount = 0;
404 int remainingCount = 0;
405 for (int i = 0; i < 10; i++)
406 {
407 EnemyWave *wave = &level->waves[i];
408 if (wave->wave != level->currentWave)
409 {
410 continue;
411 }
412 maxCount += wave->count;
413 remainingCount += wave->count - wave->spawned;
414 }
415 int aliveCount = EnemyCount();
416 remainingCount += aliveCount;
417
418 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount);
419 int textWidth = MeasureText(text, 20);
420 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
421 }
422
423 void DrawLevel(Level *level)
424 {
425 switch (level->state)
426 {
427 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break;
428 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break;
429 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break;
430 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break;
431 default: break;
432 }
433
434 DrawLevelHud(level);
435 }
436
437 void UpdateLevel(Level *level)
438 {
439 if (level->state == LEVEL_STATE_BATTLE)
440 {
441 int activeWaves = 0;
442 for (int i = 0; i < 10; i++)
443 {
444 EnemyWave *wave = &level->waves[i];
445 if (wave->spawned >= wave->count || wave->wave != level->currentWave)
446 {
447 continue;
448 }
449 activeWaves++;
450 wave->timeToSpawnNext -= gameTime.deltaTime;
451 if (wave->timeToSpawnNext <= 0.0f)
452 {
453 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y);
454 if (enemy)
455 {
456 wave->timeToSpawnNext = wave->interval;
457 wave->spawned++;
458 }
459 }
460 }
461 if (GetTowerByType(TOWER_TYPE_BASE) == 0) {
462 level->waveEndTimer += gameTime.deltaTime;
463 if (level->waveEndTimer >= 2.0f)
464 {
465 level->nextState = LEVEL_STATE_LOST_WAVE;
466 }
467 }
468 else if (activeWaves == 0 && EnemyCount() == 0)
469 {
470 level->waveEndTimer += gameTime.deltaTime;
471 if (level->waveEndTimer >= 2.0f)
472 {
473 level->nextState = LEVEL_STATE_WON_WAVE;
474 }
475 }
476 }
477
478 PathFindingMapUpdate();
479 EnemyUpdate();
480 TowerUpdate();
481 ProjectileUpdate();
482 ParticleUpdate();
483
484 if (level->nextState == LEVEL_STATE_RESET)
485 {
486 InitLevel(level);
487 }
488
489 if (level->nextState == LEVEL_STATE_BATTLE)
490 {
491 InitBattleStateConditions(level);
492 }
493
494 if (level->nextState == LEVEL_STATE_WON_WAVE)
495 {
496 level->currentWave++;
497 level->state = LEVEL_STATE_WON_WAVE;
498 }
499
500 if (level->nextState == LEVEL_STATE_LOST_WAVE)
501 {
502 level->state = LEVEL_STATE_LOST_WAVE;
503 }
504
505 if (level->nextState == LEVEL_STATE_BUILDING)
506 {
507 level->state = LEVEL_STATE_BUILDING;
508 }
509
510 if (level->nextState == LEVEL_STATE_WON_LEVEL)
511 {
512 // make something of this later
513 InitLevel(level);
514 }
515
516 level->nextState = LEVEL_STATE_NONE;
517 }
518
519 float nextSpawnTime = 0.0f;
520
521 void ResetGame()
522 {
523 InitLevel(currentLevel);
524 }
525
526 void InitGame()
527 {
528 TowerInit();
529 EnemyInit();
530 ProjectileInit();
531 ParticleInit();
532 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f);
533
534 currentLevel = levels;
535 InitLevel(currentLevel);
536 }
537
538 //# Immediate GUI functions
539
540 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth)
541 {
542 const float healthBarHeight = 6.0f;
543 const float healthBarOffset = 15.0f;
544 const float inset = 2.0f;
545 const float innerWidth = healthBarWidth - inset * 2;
546 const float innerHeight = healthBarHeight - inset * 2;
547
548 Vector2 screenPos = GetWorldToScreen(position, camera);
549 float centerX = screenPos.x - healthBarWidth * 0.5f;
550 float topY = screenPos.y - healthBarOffset;
551 DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK);
552 float healthWidth = innerWidth * healthRatio;
553 DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor);
554 }
555
556 int Button(const char *text, int x, int y, int width, int height, ButtonState *state)
557 {
558 Rectangle bounds = {x, y, width, height};
559 int isPressed = 0;
560 int isSelected = state && state->isSelected;
561 int isDisabled = state && state->isDisabled;
562 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !isDisabled)
563 {
564 Color color = isSelected ? DARKGRAY : GRAY;
565 DrawRectangle(x, y, width, height, color);
566 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
567 {
568 isPressed = 1;
569 }
570 guiState.isBlocked = 1;
571 }
572 else
573 {
574 Color color = isSelected ? WHITE : LIGHTGRAY;
575 DrawRectangle(x, y, width, height, color);
576 }
577 Font font = GetFontDefault();
578 Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1);
579 Color textColor = isDisabled ? GRAY : BLACK;
580 DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, textColor);
581 return isPressed;
582 }
583
584 //# Main game loop
585
586 void GameUpdate()
587 {
588 float dt = GetFrameTime();
589 // cap maximum delta time to 0.1 seconds to prevent large time steps
590 if (dt > 0.1f) dt = 0.1f;
591 gameTime.time += dt;
592 gameTime.deltaTime = dt;
593
594 UpdateLevel(currentLevel);
595 }
596
597 int main(void)
598 {
599 int screenWidth, screenHeight;
600 GetPreferredSize(&screenWidth, &screenHeight);
601 InitWindow(screenWidth, screenHeight, "Tower defense");
602 SetTargetFPS(30);
603
604 LoadAssets();
605 InitGame();
606
607 while (!WindowShouldClose())
608 {
609 if (IsPaused()) {
610 // canvas is not visible in browser - do nothing
611 continue;
612 }
613
614 BeginDrawing();
615 ClearBackground((Color){0x4E, 0x63, 0x26, 0xFF});
616
617 GameUpdate();
618 DrawLevel(currentLevel);
619
620 EndDrawing();
621 }
622
623 CloseWindow();
624
625 return 0;
626 }
1 #include "td_main.h"
2 #include <raymath.h>
3
4 static TowerTypeConfig towerTypeConfigs[TOWER_TYPE_COUNT] = {
5 [TOWER_TYPE_BASE] = {
6 .maxHealth = 10,
7 },
8 [TOWER_TYPE_ARCHER] = {
9 .cooldown = 0.5f,
10 .range = 3.0f,
11 .cost = 6,
12 .maxHealth = 10,
13 .projectileSpeed = 4.0f,
14 .projectileType = PROJECTILE_TYPE_ARROW,
15 .hitEffect = {
16 .damage = 3.0f,
17 }
18 },
19 [TOWER_TYPE_BALLISTA] = {
20 .cooldown = 1.5f,
21 .range = 6.0f,
22 .cost = 9,
23 .maxHealth = 10,
24 .projectileSpeed = 10.0f,
25 .projectileType = PROJECTILE_TYPE_BALLISTA,
26 .hitEffect = {
27 .damage = 6.0f,
28 .pushbackPowerDistance = 0.25f,
29 }
30 },
31 [TOWER_TYPE_CATAPULT] = {
32 .cooldown = 1.7f,
33 .range = 5.0f,
34 .cost = 10,
35 .maxHealth = 10,
36 .projectileSpeed = 3.0f,
37 .projectileType = PROJECTILE_TYPE_CATAPULT,
38 .hitEffect = {
39 .damage = 2.0f,
40 .areaDamageRadius = 1.75f,
41 }
42 },
43 [TOWER_TYPE_WALL] = {
44 .cost = 2,
45 .maxHealth = 10,
46 },
47 };
48
49 Tower towers[TOWER_MAX_COUNT];
50 int towerCount = 0;
51
52 Model towerModels[TOWER_TYPE_COUNT];
53
54 // definition of our archer unit
55 SpriteUnit archerUnit = {
56 .srcRect = {0, 0, 16, 16},
57 .offset = {7, 1},
58 .frameCount = 1,
59 .frameDuration = 0.0f,
60 .srcWeaponIdleRect = {16, 0, 6, 16},
61 .srcWeaponIdleOffset = {8, 0},
62 .srcWeaponCooldownRect = {22, 0, 11, 16},
63 .srcWeaponCooldownOffset = {10, 0},
64 };
65
66 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase)
67 {
68 float xScale = flip ? -1.0f : 1.0f;
69 Camera3D camera = currentLevel->camera;
70 float size = 0.5f;
71 Vector2 offset = (Vector2){ unit.offset.x / 16.0f * size, unit.offset.y / 16.0f * size * xScale };
72 Vector2 scale = (Vector2){ unit.srcRect.width / 16.0f * size, unit.srcRect.height / 16.0f * size };
73 // we want the sprite to face the camera, so we need to calculate the up vector
74 Vector3 forward = Vector3Subtract(camera.target, camera.position);
75 Vector3 up = {0, 1, 0};
76 Vector3 right = Vector3CrossProduct(forward, up);
77 up = Vector3Normalize(Vector3CrossProduct(right, forward));
78
79 Rectangle srcRect = unit.srcRect;
80 if (unit.frameCount > 1)
81 {
82 srcRect.x += (int)(t / unit.frameDuration) % unit.frameCount * srcRect.width;
83 }
84 if (flip)
85 {
86 srcRect.x += srcRect.width;
87 srcRect.width = -srcRect.width;
88 }
89 DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
90
91 if (phase == SPRITE_UNIT_PHASE_WEAPON_COOLDOWN && unit.srcWeaponCooldownRect.width > 0)
92 {
93 offset = (Vector2){ unit.srcWeaponCooldownOffset.x / 16.0f * size, unit.srcWeaponCooldownOffset.y / 16.0f * size };
94 scale = (Vector2){ unit.srcWeaponCooldownRect.width / 16.0f * size, unit.srcWeaponCooldownRect.height / 16.0f * size };
95 srcRect = unit.srcWeaponCooldownRect;
96 if (flip)
97 {
98 // position.x = flip * scale.x * 0.5f;
99 srcRect.x += srcRect.width;
100 srcRect.width = -srcRect.width;
101 offset.x = scale.x - offset.x;
102 }
103 DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
104 }
105 else if (phase == SPRITE_UNIT_PHASE_WEAPON_IDLE && unit.srcWeaponIdleRect.width > 0)
106 {
107 offset = (Vector2){ unit.srcWeaponIdleOffset.x / 16.0f * size, unit.srcWeaponIdleOffset.y / 16.0f * size };
108 scale = (Vector2){ unit.srcWeaponIdleRect.width / 16.0f * size, unit.srcWeaponIdleRect.height / 16.0f * size };
109 srcRect = unit.srcWeaponIdleRect;
110 if (flip)
111 {
112 // position.x = flip * scale.x * 0.5f;
113 srcRect.x += srcRect.width;
114 srcRect.width = -srcRect.width;
115 offset.x = scale.x - offset.x;
116 }
117 DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
118 }
119 }
120
121 void TowerInit()
122 {
123 for (int i = 0; i < TOWER_MAX_COUNT; i++)
124 {
125 towers[i] = (Tower){0};
126 }
127 towerCount = 0;
128
129 towerModels[TOWER_TYPE_BASE] = LoadModel("data/keep.glb");
130 towerModels[TOWER_TYPE_WALL] = LoadModel("data/wall-0000.glb");
131
132 for (int i = 0; i < TOWER_TYPE_COUNT; i++)
133 {
134 if (towerModels[i].materials)
135 {
136 // assign the palette texture to the material of the model (0 is not used afaik)
137 towerModels[i].materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
138 }
139 }
140 }
141
142 static void TowerGunUpdate(Tower *tower)
143 {
144 TowerTypeConfig config = towerTypeConfigs[tower->towerType];
145 if (tower->cooldown <= 0.0f)
146 {
147 Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, config.range);
148 if (enemy)
149 {
150 tower->cooldown = config.cooldown;
151 // shoot the enemy; determine future position of the enemy
152 float bulletSpeed = config.projectileSpeed;
153 Vector2 velocity = enemy->simVelocity;
154 Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0);
155 Vector2 towerPosition = {tower->x, tower->y};
156 float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed;
157 for (int i = 0; i < 8; i++) {
158 velocity = enemy->simVelocity;
159 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0);
160 float distance = Vector2Distance(towerPosition, futurePosition);
161 float eta2 = distance / bulletSpeed;
162 if (fabs(eta - eta2) < 0.01f) {
163 break;
164 }
165 eta = (eta2 + eta) * 0.5f;
166 }
167
168 ProjectileTryAdd(config.projectileType, enemy,
169 (Vector3){towerPosition.x, 1.33f, towerPosition.y},
170 (Vector3){futurePosition.x, 0.25f, futurePosition.y},
171 bulletSpeed, config.hitEffect);
172 enemy->futureDamage += config.hitEffect.damage;
173 tower->lastTargetPosition = futurePosition;
174 }
175 }
176 else
177 {
178 tower->cooldown -= gameTime.deltaTime;
179 }
180 }
181
182 Tower *TowerGetAt(int16_t x, int16_t y)
183 {
184 for (int i = 0; i < towerCount; i++)
185 {
186 if (towers[i].x == x && towers[i].y == y && towers[i].towerType != TOWER_TYPE_NONE)
187 {
188 return &towers[i];
189 }
190 }
191 return 0;
192 }
193
194 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y)
195 {
196 if (towerCount >= TOWER_MAX_COUNT)
197 {
198 return 0;
199 }
200
201 Tower *tower = TowerGetAt(x, y);
202 if (tower)
203 {
204 return 0;
205 }
206
207 tower = &towers[towerCount++];
208 tower->x = x;
209 tower->y = y;
210 tower->towerType = towerType;
211 tower->cooldown = 0.0f;
212 tower->damage = 0.0f;
213 return tower;
214 }
215
216 Tower *GetTowerByType(uint8_t towerType)
217 {
218 for (int i = 0; i < towerCount; i++)
219 {
220 if (towers[i].towerType == towerType)
221 {
222 return &towers[i];
223 }
224 }
225 return 0;
226 }
227
228 int GetTowerCosts(uint8_t towerType)
229 {
230 return towerTypeConfigs[towerType].cost;
231 }
232
233 float TowerGetMaxHealth(Tower *tower)
234 {
235 return towerTypeConfigs[tower->towerType].maxHealth;
236 }
237
238 void TowerDraw()
239 {
240 for (int i = 0; i < towerCount; i++)
241 {
242 Tower tower = towers[i];
243 if (tower.towerType == TOWER_TYPE_NONE)
244 {
245 continue;
246 }
247
248 switch (tower.towerType)
249 {
250 case TOWER_TYPE_ARCHER:
251 {
252 Vector2 screenPosTower = GetWorldToScreen((Vector3){tower.x, 0.0f, tower.y}, currentLevel->camera);
253 Vector2 screenPosTarget = GetWorldToScreen((Vector3){tower.lastTargetPosition.x, 0.0f, tower.lastTargetPosition.y}, currentLevel->camera);
254 DrawModel(towerModels[TOWER_TYPE_WALL], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
255 DrawSpriteUnit(archerUnit, (Vector3){tower.x, 1.0f, tower.y}, 0, screenPosTarget.x > screenPosTower.x,
256 tower.cooldown > 0.2f ? SPRITE_UNIT_PHASE_WEAPON_COOLDOWN : SPRITE_UNIT_PHASE_WEAPON_IDLE);
257 }
258 break;
259 case TOWER_TYPE_BALLISTA:
260 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, BROWN);
261 break;
262 case TOWER_TYPE_CATAPULT:
263 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, DARKGRAY);
264 break;
265 default:
266 if (towerModels[tower.towerType].materials)
267 {
268 DrawModel(towerModels[tower.towerType], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
269 } else {
270 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY);
271 }
272 break;
273 }
274 }
275 }
276
277 void TowerUpdate()
278 {
279 for (int i = 0; i < towerCount; i++)
280 {
281 Tower *tower = &towers[i];
282 switch (tower->towerType)
283 {
284 case TOWER_TYPE_CATAPULT:
285 case TOWER_TYPE_BALLISTA:
286 case TOWER_TYPE_ARCHER:
287 TowerGunUpdate(tower);
288 break;
289 }
290 }
291 }
292
293 void TowerDrawHealthBars(Camera3D camera)
294 {
295 for (int i = 0; i < towerCount; i++)
296 {
297 Tower *tower = &towers[i];
298 if (tower->towerType == TOWER_TYPE_NONE || tower->damage <= 0.0f)
299 {
300 continue;
301 }
302
303 Vector3 position = (Vector3){tower->x, 0.5f, tower->y};
304 float maxHealth = TowerGetMaxHealth(tower);
305 float health = maxHealth - tower->damage;
306 float healthRatio = health / maxHealth;
307
308 DrawHealthBar(camera, position, healthRatio, GREEN, 35.0f);
309 }
310 }
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 #define ENEMY_MAX_PATH_COUNT 8
12 #define ENEMY_MAX_COUNT 400
13 #define ENEMY_TYPE_NONE 0
14 #define ENEMY_TYPE_MINION 1
15
16 #define PARTICLE_MAX_COUNT 400
17 #define PARTICLE_TYPE_NONE 0
18 #define PARTICLE_TYPE_EXPLOSION 1
19
20 typedef struct Particle
21 {
22 uint8_t particleType;
23 float spawnTime;
24 float lifetime;
25 Vector3 position;
26 Vector3 velocity;
27 Vector3 scale;
28 } Particle;
29
30 #define TOWER_MAX_COUNT 400
31 enum TowerType
32 {
33 TOWER_TYPE_NONE,
34 TOWER_TYPE_BASE,
35 TOWER_TYPE_ARCHER,
36 TOWER_TYPE_BALLISTA,
37 TOWER_TYPE_CATAPULT,
38 TOWER_TYPE_WALL,
39 TOWER_TYPE_COUNT
40 };
41
42 typedef struct HitEffectConfig
43 {
44 float damage;
45 float areaDamageRadius;
46 float pushbackPowerDistance;
47 } HitEffectConfig;
48
49 typedef struct TowerTypeConfig
50 {
51 float cooldown;
52 float range;
53 float projectileSpeed;
54
55 uint8_t cost;
56 uint8_t projectileType;
57 uint16_t maxHealth;
58
59 HitEffectConfig hitEffect;
60 } TowerTypeConfig;
61
62 typedef struct Tower
63 {
64 int16_t x, y;
65 uint8_t towerType;
66 Vector2 lastTargetPosition;
67 float cooldown;
68 float damage;
69 } Tower;
70
71 typedef struct GameTime
72 {
73 float time;
74 float deltaTime;
75 } GameTime;
76
77 typedef struct ButtonState {
78 char isSelected;
79 char isDisabled;
80 } ButtonState;
81
82 typedef struct GUIState {
83 int isBlocked;
84 } GUIState;
85
86 typedef enum LevelState
87 {
88 LEVEL_STATE_NONE,
89 LEVEL_STATE_BUILDING,
90 LEVEL_STATE_BATTLE,
91 LEVEL_STATE_WON_WAVE,
92 LEVEL_STATE_LOST_WAVE,
93 LEVEL_STATE_WON_LEVEL,
94 LEVEL_STATE_RESET,
95 } LevelState;
96
97 typedef struct EnemyWave {
98 uint8_t enemyType;
99 uint8_t wave;
100 uint16_t count;
101 float interval;
102 float delay;
103 Vector2 spawnPosition;
104
105 uint16_t spawned;
106 float timeToSpawnNext;
107 } EnemyWave;
108
109 #define ENEMY_MAX_WAVE_COUNT 10
110
111 typedef struct Level
112 {
113 int seed;
114 LevelState state;
115 LevelState nextState;
116 Camera3D camera;
117 int placementMode;
118
119 int initialGold;
120 int playerGold;
121
122 EnemyWave waves[ENEMY_MAX_WAVE_COUNT];
123 int currentWave;
124 float waveEndTimer;
125 } Level;
126
127 typedef struct DeltaSrc
128 {
129 char x, y;
130 } DeltaSrc;
131
132 typedef struct PathfindingMap
133 {
134 int width, height;
135 float scale;
136 float *distances;
137 long *towerIndex;
138 DeltaSrc *deltaSrc;
139 float maxDistance;
140 Matrix toMapSpace;
141 Matrix toWorldSpace;
142 } PathfindingMap;
143
144 // when we execute the pathfinding algorithm, we need to store the active nodes
145 // in a queue. Each node has a position, a distance from the start, and the
146 // position of the node that we came from.
147 typedef struct PathfindingNode
148 {
149 int16_t x, y, fromX, fromY;
150 float distance;
151 } PathfindingNode;
152
153 typedef struct EnemyId
154 {
155 uint16_t index;
156 uint16_t generation;
157 } EnemyId;
158
159 typedef struct EnemyClassConfig
160 {
161 float speed;
162 float health;
163 float radius;
164 float maxAcceleration;
165 float requiredContactTime;
166 float explosionDamage;
167 float explosionRange;
168 float explosionPushbackPower;
169 int goldValue;
170 } EnemyClassConfig;
171
172 typedef struct Enemy
173 {
174 int16_t currentX, currentY;
175 int16_t nextX, nextY;
176 Vector2 simPosition;
177 Vector2 simVelocity;
178 uint16_t generation;
179 float walkedDistance;
180 float startMovingTime;
181 float damage, futureDamage;
182 float contactTime;
183 uint8_t enemyType;
184 uint8_t movePathCount;
185 Vector2 movePath[ENEMY_MAX_PATH_COUNT];
186 } Enemy;
187
188 // a unit that uses sprites to be drawn
189 #define SPRITE_UNIT_PHASE_WEAPON_IDLE 0
190 #define SPRITE_UNIT_PHASE_WEAPON_COOLDOWN 1
191 typedef struct SpriteUnit
192 {
193 Rectangle srcRect;
194 Vector2 offset;
195 int frameCount;
196 float frameDuration;
197 Rectangle srcWeaponIdleRect;
198 Vector2 srcWeaponIdleOffset;
199 Rectangle srcWeaponCooldownRect;
200 Vector2 srcWeaponCooldownOffset;
201 } SpriteUnit;
202
203 #define PROJECTILE_MAX_COUNT 1200
204 #define PROJECTILE_TYPE_NONE 0
205 #define PROJECTILE_TYPE_ARROW 1
206 #define PROJECTILE_TYPE_CATAPULT 2
207 #define PROJECTILE_TYPE_BALLISTA 3
208
209 typedef struct Projectile
210 {
211 uint8_t projectileType;
212 float shootTime;
213 float arrivalTime;
214 float distance;
215 Vector3 position;
216 Vector3 target;
217 Vector3 directionNormal;
218 EnemyId targetEnemy;
219 HitEffectConfig hitEffectConfig;
220 } Projectile;
221
222 //# Function declarations
223 float TowerGetMaxHealth(Tower *tower);
224 int Button(const char *text, int x, int y, int width, int height, ButtonState *state);
225 int EnemyAddDamageRange(Vector2 position, float range, float damage);
226 int EnemyAddDamage(Enemy *enemy, float damage);
227
228 //# Enemy functions
229 void EnemyInit();
230 void EnemyDraw();
231 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource);
232 void EnemyUpdate();
233 float EnemyGetCurrentMaxSpeed(Enemy *enemy);
234 float EnemyGetMaxHealth(Enemy *enemy);
235 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY);
236 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount);
237 EnemyId EnemyGetId(Enemy *enemy);
238 Enemy *EnemyTryResolve(EnemyId enemyId);
239 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY);
240 int EnemyAddDamage(Enemy *enemy, float damage);
241 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range);
242 int EnemyCount();
243 void EnemyDrawHealthbars(Camera3D camera);
244
245 //# Tower functions
246 void TowerInit();
247 Tower *TowerGetAt(int16_t x, int16_t y);
248 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y);
249 Tower *GetTowerByType(uint8_t towerType);
250 int GetTowerCosts(uint8_t towerType);
251 float TowerGetMaxHealth(Tower *tower);
252 void TowerDraw();
253 void TowerUpdate();
254 void TowerDrawHealthBars(Camera3D camera);
255 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase);
256
257 //# Particles
258 void ParticleInit();
259 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime);
260 void ParticleUpdate();
261 void ParticleDraw();
262
263 //# Projectiles
264 void ProjectileInit();
265 void ProjectileDraw();
266 void ProjectileUpdate();
267 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig);
268
269 //# Pathfinding map
270 void PathfindingMapInit(int width, int height, Vector3 translate, float scale);
271 float PathFindingGetDistance(int mapX, int mapY);
272 Vector2 PathFindingGetGradient(Vector3 world);
273 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY);
274 void PathFindingMapUpdate();
275 void PathFindingMapDraw();
276
277 //# UI
278 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth);
279
280 //# Level
281 void DrawLevelGround(Level *level);
282 void DrawEnemyPath(Level *level);
283
284 //# variables
285 extern Level *currentLevel;
286 extern Enemy enemies[ENEMY_MAX_COUNT];
287 extern int enemyCount;
288 extern EnemyClassConfig enemyClassConfigs[];
289
290 extern GUIState guiState;
291 extern GameTime gameTime;
292 extern Tower towers[TOWER_MAX_COUNT];
293 extern int towerCount;
294
295 extern Texture2D palette, spriteSheet;
296
297 #endif
1 #include "td_main.h"
2 #include <raymath.h>
3 #include <stdlib.h>
4 #include <math.h>
5
6 EnemyClassConfig enemyClassConfigs[] = {
7 [ENEMY_TYPE_MINION] = {
8 .health = 10.0f,
9 .speed = 0.6f,
10 .radius = 0.25f,
11 .maxAcceleration = 1.0f,
12 .explosionDamage = 1.0f,
13 .requiredContactTime = 0.5f,
14 .explosionRange = 1.0f,
15 .explosionPushbackPower = 0.25f,
16 .goldValue = 1,
17 },
18 };
19
20 Enemy enemies[ENEMY_MAX_COUNT];
21 int enemyCount = 0;
22
23 SpriteUnit enemySprites[] = {
24 [ENEMY_TYPE_MINION] = {
25 .srcRect = {0, 16, 16, 16},
26 .offset = {8.0f, 0.0f},
27 .frameCount = 6,
28 .frameDuration = 0.1f,
29 },
30 };
31
32 void EnemyInit()
33 {
34 for (int i = 0; i < ENEMY_MAX_COUNT; i++)
35 {
36 enemies[i] = (Enemy){0};
37 }
38 enemyCount = 0;
39 }
40
41 float EnemyGetCurrentMaxSpeed(Enemy *enemy)
42 {
43 return enemyClassConfigs[enemy->enemyType].speed;
44 }
45
46 float EnemyGetMaxHealth(Enemy *enemy)
47 {
48 return enemyClassConfigs[enemy->enemyType].health;
49 }
50
51 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY)
52 {
53 int16_t castleX = 0;
54 int16_t castleY = 0;
55 int16_t dx = castleX - currentX;
56 int16_t dy = castleY - currentY;
57 if (dx == 0 && dy == 0)
58 {
59 *nextX = currentX;
60 *nextY = currentY;
61 return 1;
62 }
63 Vector2 gradient = PathFindingGetGradient((Vector3){currentX, 0, currentY});
64
65 if (gradient.x == 0 && gradient.y == 0)
66 {
67 *nextX = currentX;
68 *nextY = currentY;
69 return 1;
70 }
71
72 if (fabsf(gradient.x) > fabsf(gradient.y))
73 {
74 *nextX = currentX + (int16_t)(gradient.x > 0.0f ? 1 : -1);
75 *nextY = currentY;
76 return 0;
77 }
78 *nextX = currentX;
79 *nextY = currentY + (int16_t)(gradient.y > 0.0f ? 1 : -1);
80 return 0;
81 }
82
83
84 // this function predicts the movement of the unit for the next deltaT seconds
85 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount)
86 {
87 const float pointReachedDistance = 0.25f;
88 const float pointReachedDistance2 = pointReachedDistance * pointReachedDistance;
89 const float maxSimStepTime = 0.015625f;
90
91 float maxAcceleration = enemyClassConfigs[enemy->enemyType].maxAcceleration;
92 float maxSpeed = EnemyGetCurrentMaxSpeed(enemy);
93 int16_t nextX = enemy->nextX;
94 int16_t nextY = enemy->nextY;
95 Vector2 position = enemy->simPosition;
96 int passedCount = 0;
97 for (float t = 0.0f; t < deltaT; t += maxSimStepTime)
98 {
99 float stepTime = fminf(deltaT - t, maxSimStepTime);
100 Vector2 target = (Vector2){nextX, nextY};
101 float speed = Vector2Length(*velocity);
102 // draw the target position for debugging
103 DrawCubeWires((Vector3){target.x, 0.2f, target.y}, 0.1f, 0.4f, 0.1f, RED);
104 Vector2 lookForwardPos = Vector2Add(position, Vector2Scale(*velocity, speed));
105 if (Vector2DistanceSqr(target, lookForwardPos) <= pointReachedDistance2)
106 {
107 // we reached the target position, let's move to the next waypoint
108 EnemyGetNextPosition(nextX, nextY, &nextX, &nextY);
109 target = (Vector2){nextX, nextY};
110 // track how many waypoints we passed
111 passedCount++;
112 }
113
114 // acceleration towards the target
115 Vector2 unitDirection = Vector2Normalize(Vector2Subtract(target, lookForwardPos));
116 Vector2 acceleration = Vector2Scale(unitDirection, maxAcceleration * stepTime);
117 *velocity = Vector2Add(*velocity, acceleration);
118
119 // limit the speed to the maximum speed
120 if (speed > maxSpeed)
121 {
122 *velocity = Vector2Scale(*velocity, maxSpeed / speed);
123 }
124
125 // move the enemy
126 position = Vector2Add(position, Vector2Scale(*velocity, stepTime));
127 }
128
129 if (waypointPassedCount)
130 {
131 (*waypointPassedCount) = passedCount;
132 }
133
134 return position;
135 }
136
137 void EnemyDraw()
138 {
139 for (int i = 0; i < enemyCount; i++)
140 {
141 Enemy enemy = enemies[i];
142 if (enemy.enemyType == ENEMY_TYPE_NONE)
143 {
144 continue;
145 }
146
147 Vector2 position = EnemyGetPosition(&enemy, gameTime.time - enemy.startMovingTime, &enemy.simVelocity, 0);
148
149 // don't draw any trails for now; might replace this with footprints later
150 // if (enemy.movePathCount > 0)
151 // {
152 // Vector3 p = {enemy.movePath[0].x, 0.2f, enemy.movePath[0].y};
153 // DrawLine3D(p, (Vector3){position.x, 0.2f, position.y}, GREEN);
154 // }
155 // for (int j = 1; j < enemy.movePathCount; j++)
156 // {
157 // Vector3 p = {enemy.movePath[j - 1].x, 0.2f, enemy.movePath[j - 1].y};
158 // Vector3 q = {enemy.movePath[j].x, 0.2f, enemy.movePath[j].y};
159 // DrawLine3D(p, q, GREEN);
160 // }
161
162 switch (enemy.enemyType)
163 {
164 case ENEMY_TYPE_MINION:
165 DrawSpriteUnit(enemySprites[ENEMY_TYPE_MINION], (Vector3){position.x, 0.0f, position.y},
166 enemy.walkedDistance, 0, 0);
167 break;
168 }
169 }
170 }
171
172 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource)
173 {
174 // damage the tower
175 float explosionDamge = enemyClassConfigs[enemy->enemyType].explosionDamage;
176 float explosionRange = enemyClassConfigs[enemy->enemyType].explosionRange;
177 float explosionPushbackPower = enemyClassConfigs[enemy->enemyType].explosionPushbackPower;
178 float explosionRange2 = explosionRange * explosionRange;
179 tower->damage += enemyClassConfigs[enemy->enemyType].explosionDamage;
180 // explode the enemy
181 if (tower->damage >= TowerGetMaxHealth(tower))
182 {
183 tower->towerType = TOWER_TYPE_NONE;
184 }
185
186 ParticleAdd(PARTICLE_TYPE_EXPLOSION,
187 explosionSource,
188 (Vector3){0, 0.1f, 0}, (Vector3){1.0f, 1.0f, 1.0f}, 1.0f);
189
190 enemy->enemyType = ENEMY_TYPE_NONE;
191
192 // push back enemies & dealing damage
193 for (int i = 0; i < enemyCount; i++)
194 {
195 Enemy *other = &enemies[i];
196 if (other->enemyType == ENEMY_TYPE_NONE)
197 {
198 continue;
199 }
200 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, other->simPosition);
201 if (distanceSqr > 0 && distanceSqr < explosionRange2)
202 {
203 Vector2 direction = Vector2Normalize(Vector2Subtract(other->simPosition, enemy->simPosition));
204 other->simPosition = Vector2Add(other->simPosition, Vector2Scale(direction, explosionPushbackPower));
205 EnemyAddDamage(other, explosionDamge);
206 }
207 }
208 }
209
210 void EnemyUpdate()
211 {
212 const float castleX = 0;
213 const float castleY = 0;
214 const float maxPathDistance2 = 0.25f * 0.25f;
215
216 for (int i = 0; i < enemyCount; i++)
217 {
218 Enemy *enemy = &enemies[i];
219 if (enemy->enemyType == ENEMY_TYPE_NONE)
220 {
221 continue;
222 }
223
224 int waypointPassedCount = 0;
225 Vector2 prevPosition = enemy->simPosition;
226 enemy->simPosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &enemy->simVelocity, &waypointPassedCount);
227 enemy->startMovingTime = gameTime.time;
228 enemy->walkedDistance += Vector2Distance(prevPosition, enemy->simPosition);
229 // track path of unit
230 if (enemy->movePathCount == 0 || Vector2DistanceSqr(enemy->simPosition, enemy->movePath[0]) > maxPathDistance2)
231 {
232 for (int j = ENEMY_MAX_PATH_COUNT - 1; j > 0; j--)
233 {
234 enemy->movePath[j] = enemy->movePath[j - 1];
235 }
236 enemy->movePath[0] = enemy->simPosition;
237 if (++enemy->movePathCount > ENEMY_MAX_PATH_COUNT)
238 {
239 enemy->movePathCount = ENEMY_MAX_PATH_COUNT;
240 }
241 }
242
243 if (waypointPassedCount > 0)
244 {
245 enemy->currentX = enemy->nextX;
246 enemy->currentY = enemy->nextY;
247 if (EnemyGetNextPosition(enemy->currentX, enemy->currentY, &enemy->nextX, &enemy->nextY) &&
248 Vector2DistanceSqr(enemy->simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
249 {
250 // enemy reached the castle; remove it
251 enemy->enemyType = ENEMY_TYPE_NONE;
252 continue;
253 }
254 }
255 }
256
257 // handle collisions between enemies
258 for (int i = 0; i < enemyCount - 1; i++)
259 {
260 Enemy *enemyA = &enemies[i];
261 if (enemyA->enemyType == ENEMY_TYPE_NONE)
262 {
263 continue;
264 }
265 for (int j = i + 1; j < enemyCount; j++)
266 {
267 Enemy *enemyB = &enemies[j];
268 if (enemyB->enemyType == ENEMY_TYPE_NONE)
269 {
270 continue;
271 }
272 float distanceSqr = Vector2DistanceSqr(enemyA->simPosition, enemyB->simPosition);
273 float radiusA = enemyClassConfigs[enemyA->enemyType].radius;
274 float radiusB = enemyClassConfigs[enemyB->enemyType].radius;
275 float radiusSum = radiusA + radiusB;
276 if (distanceSqr < radiusSum * radiusSum && distanceSqr > 0.001f)
277 {
278 // collision
279 float distance = sqrtf(distanceSqr);
280 float overlap = radiusSum - distance;
281 // move the enemies apart, but softly; if we have a clog of enemies,
282 // moving them perfectly apart can cause them to jitter
283 float positionCorrection = overlap / 5.0f;
284 Vector2 direction = (Vector2){
285 (enemyB->simPosition.x - enemyA->simPosition.x) / distance * positionCorrection,
286 (enemyB->simPosition.y - enemyA->simPosition.y) / distance * positionCorrection};
287 enemyA->simPosition = Vector2Subtract(enemyA->simPosition, direction);
288 enemyB->simPosition = Vector2Add(enemyB->simPosition, direction);
289 }
290 }
291 }
292
293 // handle collisions between enemies and towers
294 for (int i = 0; i < enemyCount; i++)
295 {
296 Enemy *enemy = &enemies[i];
297 if (enemy->enemyType == ENEMY_TYPE_NONE)
298 {
299 continue;
300 }
301 enemy->contactTime -= gameTime.deltaTime;
302 if (enemy->contactTime < 0.0f)
303 {
304 enemy->contactTime = 0.0f;
305 }
306
307 float enemyRadius = enemyClassConfigs[enemy->enemyType].radius;
308 // linear search over towers; could be optimized by using path finding tower map,
309 // but for now, we keep it simple
310 for (int j = 0; j < towerCount; j++)
311 {
312 Tower *tower = &towers[j];
313 if (tower->towerType == TOWER_TYPE_NONE)
314 {
315 continue;
316 }
317 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){tower->x, tower->y});
318 float combinedRadius = enemyRadius + 0.708; // sqrt(0.5^2 + 0.5^2), corner-center distance of square with side length 1
319 if (distanceSqr > combinedRadius * combinedRadius)
320 {
321 continue;
322 }
323 // potential collision; square / circle intersection
324 float dx = tower->x - enemy->simPosition.x;
325 float dy = tower->y - enemy->simPosition.y;
326 float absDx = fabsf(dx);
327 float absDy = fabsf(dy);
328 Vector3 contactPoint = {0};
329 if (absDx <= 0.5f && absDx <= absDy) {
330 // vertical collision; push the enemy out horizontally
331 float overlap = enemyRadius + 0.5f - absDy;
332 if (overlap < 0.0f)
333 {
334 continue;
335 }
336 float direction = dy > 0.0f ? -1.0f : 1.0f;
337 enemy->simPosition.y += direction * overlap;
338 contactPoint = (Vector3){enemy->simPosition.x, 0.2f, tower->y + direction * 0.5f};
339 }
340 else if (absDy <= 0.5f && absDy <= absDx)
341 {
342 // horizontal collision; push the enemy out vertically
343 float overlap = enemyRadius + 0.5f - absDx;
344 if (overlap < 0.0f)
345 {
346 continue;
347 }
348 float direction = dx > 0.0f ? -1.0f : 1.0f;
349 enemy->simPosition.x += direction * overlap;
350 contactPoint = (Vector3){tower->x + direction * 0.5f, 0.2f, enemy->simPosition.y};
351 }
352 else
353 {
354 // possible collision with a corner
355 float cornerDX = dx > 0.0f ? -0.5f : 0.5f;
356 float cornerDY = dy > 0.0f ? -0.5f : 0.5f;
357 float cornerX = tower->x + cornerDX;
358 float cornerY = tower->y + cornerDY;
359 float cornerDistanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){cornerX, cornerY});
360 if (cornerDistanceSqr > enemyRadius * enemyRadius)
361 {
362 continue;
363 }
364 // push the enemy out along the diagonal
365 float cornerDistance = sqrtf(cornerDistanceSqr);
366 float overlap = enemyRadius - cornerDistance;
367 float directionX = cornerDistance > 0.0f ? (cornerX - enemy->simPosition.x) / cornerDistance : -cornerDX;
368 float directionY = cornerDistance > 0.0f ? (cornerY - enemy->simPosition.y) / cornerDistance : -cornerDY;
369 enemy->simPosition.x -= directionX * overlap;
370 enemy->simPosition.y -= directionY * overlap;
371 contactPoint = (Vector3){cornerX, 0.2f, cornerY};
372 }
373
374 if (enemyClassConfigs[enemy->enemyType].explosionDamage > 0.0f)
375 {
376 enemy->contactTime += gameTime.deltaTime * 2.0f; // * 2 to undo the subtraction above
377 if (enemy->contactTime >= enemyClassConfigs[enemy->enemyType].requiredContactTime)
378 {
379 EnemyTriggerExplode(enemy, tower, contactPoint);
380 }
381 }
382 }
383 }
384 }
385
386 EnemyId EnemyGetId(Enemy *enemy)
387 {
388 return (EnemyId){enemy - enemies, enemy->generation};
389 }
390
391 Enemy *EnemyTryResolve(EnemyId enemyId)
392 {
393 if (enemyId.index >= ENEMY_MAX_COUNT)
394 {
395 return 0;
396 }
397 Enemy *enemy = &enemies[enemyId.index];
398 if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE)
399 {
400 return 0;
401 }
402 return enemy;
403 }
404
405 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY)
406 {
407 Enemy *spawn = 0;
408 for (int i = 0; i < enemyCount; i++)
409 {
410 Enemy *enemy = &enemies[i];
411 if (enemy->enemyType == ENEMY_TYPE_NONE)
412 {
413 spawn = enemy;
414 break;
415 }
416 }
417
418 if (enemyCount < ENEMY_MAX_COUNT && !spawn)
419 {
420 spawn = &enemies[enemyCount++];
421 }
422
423 if (spawn)
424 {
425 spawn->currentX = currentX;
426 spawn->currentY = currentY;
427 spawn->nextX = currentX;
428 spawn->nextY = currentY;
429 spawn->simPosition = (Vector2){currentX, currentY};
430 spawn->simVelocity = (Vector2){0, 0};
431 spawn->enemyType = enemyType;
432 spawn->startMovingTime = gameTime.time;
433 spawn->damage = 0.0f;
434 spawn->futureDamage = 0.0f;
435 spawn->generation++;
436 spawn->movePathCount = 0;
437 spawn->walkedDistance = 0.0f;
438 }
439
440 return spawn;
441 }
442
443 int EnemyAddDamageRange(Vector2 position, float range, float damage)
444 {
445 int count = 0;
446 float range2 = range * range;
447 for (int i = 0; i < enemyCount; i++)
448 {
449 Enemy *enemy = &enemies[i];
450 if (enemy->enemyType == ENEMY_TYPE_NONE)
451 {
452 continue;
453 }
454 float distance2 = Vector2DistanceSqr(position, enemy->simPosition);
455 if (distance2 <= range2)
456 {
457 EnemyAddDamage(enemy, damage);
458 count++;
459 }
460 }
461 return count;
462 }
463
464 int EnemyAddDamage(Enemy *enemy, float damage)
465 {
466 enemy->damage += damage;
467 if (enemy->damage >= EnemyGetMaxHealth(enemy))
468 {
469 currentLevel->playerGold += enemyClassConfigs[enemy->enemyType].goldValue;
470 enemy->enemyType = ENEMY_TYPE_NONE;
471 return 1;
472 }
473
474 return 0;
475 }
476
477 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
478 {
479 int16_t castleX = 0;
480 int16_t castleY = 0;
481 Enemy* closest = 0;
482 int16_t closestDistance = 0;
483 float range2 = range * range;
484 for (int i = 0; i < enemyCount; i++)
485 {
486 Enemy* enemy = &enemies[i];
487 if (enemy->enemyType == ENEMY_TYPE_NONE)
488 {
489 continue;
490 }
491 float maxHealth = EnemyGetMaxHealth(enemy);
492 if (enemy->futureDamage >= maxHealth)
493 {
494 // ignore enemies that will die soon
495 continue;
496 }
497 int16_t dx = castleX - enemy->currentX;
498 int16_t dy = castleY - enemy->currentY;
499 int16_t distance = abs(dx) + abs(dy);
500 if (!closest || distance < closestDistance)
501 {
502 float tdx = towerX - enemy->currentX;
503 float tdy = towerY - enemy->currentY;
504 float tdistance2 = tdx * tdx + tdy * tdy;
505 if (tdistance2 <= range2)
506 {
507 closest = enemy;
508 closestDistance = distance;
509 }
510 }
511 }
512 return closest;
513 }
514
515 int EnemyCount()
516 {
517 int count = 0;
518 for (int i = 0; i < enemyCount; i++)
519 {
520 if (enemies[i].enemyType != ENEMY_TYPE_NONE)
521 {
522 count++;
523 }
524 }
525 return count;
526 }
527
528 void EnemyDrawHealthbars(Camera3D camera)
529 {
530 for (int i = 0; i < enemyCount; i++)
531 {
532 Enemy *enemy = &enemies[i];
533 if (enemy->enemyType == ENEMY_TYPE_NONE || enemy->damage == 0.0f)
534 {
535 continue;
536 }
537 Vector3 position = (Vector3){enemy->simPosition.x, 0.5f, enemy->simPosition.y};
538 float maxHealth = EnemyGetMaxHealth(enemy);
539 float health = maxHealth - enemy->damage;
540 float healthRatio = health / maxHealth;
541
542 DrawHealthBar(camera, position, healthRatio, GREEN, 15.0f);
543 }
544 }
1 #include "td_main.h"
2 #include <raymath.h>
3
4 // 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()
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 < towerCount; i++)
131 {
132 Tower *tower = &towers[i];
133 if (tower->towerType == TOWER_TYPE_NONE || tower->towerType == TOWER_TYPE_BASE)
134 {
135 continue;
136 }
137 int16_t mapX, mapY;
138 // technically, if the tower cell scale is not in sync with the pathfinding map scale,
139 // this would not work correctly and needs to be refined to allow towers covering multiple cells
140 // or having multiple towers in one cell; for simplicity, we assume that the tower covers exactly
141 // one cell. For now.
142 if (!PathFindingFromWorldToMapPosition((Vector3){tower->x, 0.0f, tower->y}, &mapX, &mapY))
143 {
144 continue;
145 }
146 int index = mapY * width + mapX;
147 pathfindingMap.towerIndex[index] = i;
148 }
149
150 // we start at the castle and add the castle to the queue
151 pathfindingMap.maxDistance = 0.0f;
152 pathfindingNodeQueueCount = 0;
153 PathFindingNodePush(castleMapX, castleMapY, castleMapX, castleMapY, 0.0f);
154 PathfindingNode *node = 0;
155 while ((node = PathFindingNodePop()))
156 {
157 if (node->x < 0 || node->x >= width || node->y < 0 || node->y >= height)
158 {
159 continue;
160 }
161 int index = node->y * width + node->x;
162 if (pathfindingMap.distances[index] >= 0 && pathfindingMap.distances[index] <= node->distance)
163 {
164 continue;
165 }
166
167 int deltaX = node->x - node->fromX;
168 int deltaY = node->y - node->fromY;
169 // even if the cell is blocked by a tower, we still may want to store the direction
170 // (though this might not be needed, IDK right now)
171 pathfindingMap.deltaSrc[index].x = (char) deltaX;
172 pathfindingMap.deltaSrc[index].y = (char) deltaY;
173
174 // we skip nodes that are blocked by towers
175 if (pathfindingMap.towerIndex[index] >= 0)
176 {
177 node->distance += 8.0f;
178 }
179 pathfindingMap.distances[index] = node->distance;
180 pathfindingMap.maxDistance = fmaxf(pathfindingMap.maxDistance, node->distance);
181 PathFindingNodePush(node->x, node->y + 1, node->x, node->y, node->distance + 1.0f);
182 PathFindingNodePush(node->x, node->y - 1, node->x, node->y, node->distance + 1.0f);
183 PathFindingNodePush(node->x + 1, node->y, node->x, node->y, node->distance + 1.0f);
184 PathFindingNodePush(node->x - 1, node->y, node->x, node->y, node->distance + 1.0f);
185 }
186 }
187
188 void PathFindingMapDraw()
189 {
190 float cellSize = pathfindingMap.scale * 0.9f;
191 float highlightDistance = fmodf(GetTime() * 4.0f, pathfindingMap.maxDistance);
192 for (int x = 0; x < pathfindingMap.width; x++)
193 {
194 for (int y = 0; y < pathfindingMap.height; y++)
195 {
196 float distance = pathfindingMap.distances[y * pathfindingMap.width + x];
197 float colorV = distance < 0 ? 0 : fminf(distance / pathfindingMap.maxDistance, 1.0f);
198 Color color = distance < 0 ? BLUE : (Color){fminf(colorV, 1.0f) * 255, 0, 0, 255};
199 Vector3 position = Vector3Transform((Vector3){x, -0.25f, y}, pathfindingMap.toWorldSpace);
200 // animate the distance "wave" to show how the pathfinding algorithm expands
201 // from the castle
202 if (distance + 0.5f > highlightDistance && distance - 0.5f < highlightDistance)
203 {
204 color = BLACK;
205 }
206 DrawCube(position, cellSize, 0.1f, cellSize, color);
207 }
208 }
209 }
210
211 Vector2 PathFindingGetGradient(Vector3 world)
212 {
213 int16_t mapX, mapY;
214 if (PathFindingFromWorldToMapPosition(world, &mapX, &mapY))
215 {
216 DeltaSrc delta = pathfindingMap.deltaSrc[mapY * pathfindingMap.width + mapX];
217 return (Vector2){(float)-delta.x, (float)-delta.y};
218 }
219 // fallback to a simple gradient calculation
220 float n = PathFindingGetDistance(mapX, mapY - 1);
221 float s = PathFindingGetDistance(mapX, mapY + 1);
222 float w = PathFindingGetDistance(mapX - 1, mapY);
223 float e = PathFindingGetDistance(mapX + 1, mapY);
224 return (Vector2){w - e + 0.25f, n - s + 0.125f};
225 }
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
When building a more complex level, it's now much clearer how the enemies will move through it:
However, it would be better if the path was updated when previewing a new building placement. For that, we have to look into how the pathfinding handles the placed towers during the distance map calculation:
1 void PathFindingMapUpdate()
2 {
3 //...
4 for (int i = 0; i < towerCount; i++)
5 {
6 Tower *tower = &towers[i];
7 if (tower->towerType == TOWER_TYPE_NONE || tower->towerType == TOWER_TYPE_BASE)
8 {
9 continue;
10 }
11 int16_t mapX, mapY;
12 if (!PathFindingFromWorldToMapPosition((Vector3){tower->x, 0.0f, tower->y}, &mapX, &mapY))
13 {
14 continue;
15 }
16 int index = mapY * width + mapX;
17 pathfindingMap.towerIndex[index] = i;
18 }
19 }
What we want to do here is to block cells where buildings are about to be built. We can achieve this in 2 ways: We could place a building that is flagged as "preview" but that blocks the path already or we supply a list of cells that are blocked to the pathfinding system.
Both approaches are valid. I have already another feature in mind that I want to add soon that may play into the decision which approach to take: At a later point, we have to refine the building placement. It should show the tower we are about to place and this tower is following the mouse hover or can be moved via touch input on mobile.
The first approach (placing a preview building) could make this easier to implement - at first. Because when moving the tower around, it should hover and move smoothly, not just jump from cell to cell. This again would be difficult to implement with the first approach.
So I will go with the second approach: Supplying a list of blocked cells to the pathfinding system and keeping the grid and the temporary cell blocking separate:
1 #include "td_main.h"
2 #include <raymath.h>
3 #include <stdlib.h>
4 #include <math.h>
5
6 //# Variables
7 GUIState guiState = {0};
8 GameTime gameTime = {0};
9
10 Model floorTileAModel = {0};
11 Model floorTileBModel = {0};
12 Model treeModel[2] = {0};
13 Model firTreeModel[2] = {0};
14 Model rockModels[5] = {0};
15 Model grassPatchModel[1] = {0};
16
17 Texture2D palette, spriteSheet;
18
19 Level levels[] = {
20 [0] = {
21 .state = LEVEL_STATE_BUILDING,
22 .initialGold = 20,
23 .waves[0] = {
24 .enemyType = ENEMY_TYPE_MINION,
25 .wave = 0,
26 .count = 10,
27 .interval = 2.5f,
28 .delay = 1.0f,
29 .spawnPosition = {0, 6},
30 },
31 .waves[1] = {
32 .enemyType = ENEMY_TYPE_MINION,
33 .wave = 1,
34 .count = 20,
35 .interval = 1.5f,
36 .delay = 1.0f,
37 .spawnPosition = {0, 6},
38 },
39 .waves[2] = {
40 .enemyType = ENEMY_TYPE_MINION,
41 .wave = 2,
42 .count = 30,
43 .interval = 1.2f,
44 .delay = 1.0f,
45 .spawnPosition = {0, 6},
46 }
47 },
48 };
49
50 Level *currentLevel = levels;
51
52 //# Game
53
54 static Model LoadGLBModel(char *filename)
55 {
56 Model model = LoadModel(TextFormat("data/%s.glb",filename));
57 if (model.materialCount > 1)
58 {
59 model.materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
60 }
61 return model;
62 }
63
64 void LoadAssets()
65 {
66 // load a sprite sheet that contains all units
67 spriteSheet = LoadTexture("data/spritesheet.png");
68 SetTextureFilter(spriteSheet, TEXTURE_FILTER_BILINEAR);
69
70 // we'll use a palette texture to colorize the all buildings and environment art
71 palette = LoadTexture("data/palette.png");
72 // The texture uses gradients on very small space, so we'll enable bilinear filtering
73 SetTextureFilter(palette, TEXTURE_FILTER_BILINEAR);
74
75 floorTileAModel = LoadGLBModel("floor-tile-a");
76 floorTileBModel = LoadGLBModel("floor-tile-b");
77 treeModel[0] = LoadGLBModel("leaftree-large-1-a");
78 treeModel[1] = LoadGLBModel("leaftree-large-1-b");
79 firTreeModel[0] = LoadGLBModel("firtree-1-a");
80 firTreeModel[1] = LoadGLBModel("firtree-1-b");
81 rockModels[0] = LoadGLBModel("rock-1");
82 rockModels[1] = LoadGLBModel("rock-2");
83 rockModels[2] = LoadGLBModel("rock-3");
84 rockModels[3] = LoadGLBModel("rock-4");
85 rockModels[4] = LoadGLBModel("rock-5");
86 grassPatchModel[0] = LoadGLBModel("grass-patch-1");
87 }
88
89 void InitLevel(Level *level)
90 {
91 level->seed = (int)(GetTime() * 100.0f);
92
93 TowerInit();
94 EnemyInit();
95 ProjectileInit();
96 ParticleInit();
97 TowerTryAdd(TOWER_TYPE_BASE, 0, 0);
98
99 level->placementMode = 0;
100 level->state = LEVEL_STATE_BUILDING;
101 level->nextState = LEVEL_STATE_NONE;
102 level->playerGold = level->initialGold;
103 level->currentWave = 0;
104
105 Camera *camera = &level->camera;
106 camera->position = (Vector3){4.0f, 8.0f, 8.0f};
107 camera->target = (Vector3){0.0f, 0.0f, 0.0f};
108 camera->up = (Vector3){0.0f, 1.0f, 0.0f};
109 camera->fovy = 10.0f;
110 camera->projection = CAMERA_ORTHOGRAPHIC;
111 }
112
113 void DrawLevelHud(Level *level)
114 {
115 const char *text = TextFormat("Gold: %d", level->playerGold);
116 Font font = GetFontDefault();
117 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 120, 10}, font.baseSize * 2.0f, 2.0f, BLACK);
118 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 122, 8}, font.baseSize * 2.0f, 2.0f, YELLOW);
119 }
120
121 void DrawLevelReportLostWave(Level *level)
122 {
123 BeginMode3D(level->camera);
124 DrawLevelGround(level);
125 TowerDraw();
126 EnemyDraw();
127 ProjectileDraw();
128 ParticleDraw();
129 guiState.isBlocked = 0;
130 EndMode3D();
131
132 TowerDrawHealthBars(level->camera);
133
134 const char *text = "Wave lost";
135 int textWidth = MeasureText(text, 20);
136 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
137
138 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
139 {
140 level->nextState = LEVEL_STATE_RESET;
141 }
142 }
143
144 int HasLevelNextWave(Level *level)
145 {
146 for (int i = 0; i < 10; i++)
147 {
148 EnemyWave *wave = &level->waves[i];
149 if (wave->wave == level->currentWave)
150 {
151 return 1;
152 }
153 }
154 return 0;
155 }
156
157 void DrawLevelReportWonWave(Level *level)
158 {
159 BeginMode3D(level->camera);
160 DrawLevelGround(level);
161 TowerDraw();
162 EnemyDraw();
163 ProjectileDraw();
164 ParticleDraw();
165 guiState.isBlocked = 0;
166 EndMode3D();
167
168 TowerDrawHealthBars(level->camera);
169
170 const char *text = "Wave won";
171 int textWidth = MeasureText(text, 20);
172 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
173
174
175 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
176 {
177 level->nextState = LEVEL_STATE_RESET;
178 }
179
180 if (HasLevelNextWave(level))
181 {
182 if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
183 {
184 level->nextState = LEVEL_STATE_BUILDING;
185 }
186 }
187 else {
188 if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
189 {
190 level->nextState = LEVEL_STATE_WON_LEVEL;
191 }
192 }
193 }
194
195 void DrawBuildingBuildButton(Level *level, int x, int y, int width, int height, uint8_t towerType, const char *name)
196 {
197 static ButtonState buttonStates[8] = {0};
198 int cost = GetTowerCosts(towerType);
199 const char *text = TextFormat("%s: %d", name, cost);
200 buttonStates[towerType].isSelected = level->placementMode == towerType;
201 buttonStates[towerType].isDisabled = level->playerGold < cost;
202 if (Button(text, x, y, width, height, &buttonStates[towerType]))
203 {
204 level->placementMode = buttonStates[towerType].isSelected ? 0 : towerType;
205 }
206 }
207
208 float GetRandomFloat(float min, float max)
209 {
210 int random = GetRandomValue(0, 0xfffffff);
211 return ((float)random / (float)0xfffffff) * (max - min) + min;
212 }
213
214 void DrawLevelGround(Level *level)
215 {
216 // draw checkerboard ground pattern
217 for (int x = -5; x <= 5; x += 1)
218 {
219 for (int y = -5; y <= 5; y += 1)
220 {
221 Model *model = (x + y) % 2 == 0 ? &floorTileAModel : &floorTileBModel;
222 DrawModel(*model, (Vector3){x, 0.0f, y}, 1.0f, WHITE);
223 }
224 }
225
226 int oldSeed = GetRandomValue(0, 0xfffffff);
227 SetRandomSeed(level->seed);
228 // increase probability for trees via duplicated entries
229 Model borderModels[64];
230 int maxRockCount = GetRandomValue(2, 6);
231 int maxTreeCount = GetRandomValue(10, 20);
232 int maxFirTreeCount = GetRandomValue(5, 10);
233 int maxLeafTreeCount = maxTreeCount - maxFirTreeCount;
234 int grassPatchCount = GetRandomValue(5, 30);
235
236 int modelCount = 0;
237 for (int i = 0; i < maxRockCount && modelCount < 63; i++)
238 {
239 borderModels[modelCount++] = rockModels[GetRandomValue(0, 5)];
240 }
241 for (int i = 0; i < maxLeafTreeCount && modelCount < 63; i++)
242 {
243 borderModels[modelCount++] = treeModel[GetRandomValue(0, 1)];
244 }
245 for (int i = 0; i < maxFirTreeCount && modelCount < 63; i++)
246 {
247 borderModels[modelCount++] = firTreeModel[GetRandomValue(0, 1)];
248 }
249 for (int i = 0; i < grassPatchCount && modelCount < 63; i++)
250 {
251 borderModels[modelCount++] = grassPatchModel[0];
252 }
253
254 // draw some objects around the border of the map
255 Vector3 up = {0, 1, 0};
256 // a pseudo random number generator to get the same result every time
257 const float wiggle = 0.75f;
258 const int layerCount = 3;
259 for (int layer = 0; layer < layerCount; layer++)
260 {
261 int layerPos = 6 + layer;
262 for (int x = -6 + layer; x <= 6 + layer; x += 1)
263 {
264 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)],
265 (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, -layerPos + GetRandomFloat(0.0f, wiggle)},
266 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
267 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)],
268 (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, layerPos + GetRandomFloat(0.0f, wiggle)},
269 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
270 }
271
272 for (int z = -5 + layer; z <= 5 + layer; z += 1)
273 {
274 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)],
275 (Vector3){-layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)},
276 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
277 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)],
278 (Vector3){layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)},
279 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
280 }
281 }
282
283 SetRandomSeed(oldSeed);
284 }
285
286 void DrawEnemyPath(Level *level)
287 {
288 for (int i = 0; i < ENEMY_MAX_WAVE_COUNT; i++)
289 {
290 EnemyWave *wave = &level->waves[i];
291 if (wave->wave != level->currentWave)
292 {
293 continue;
294 }
295 Vector3 position = (Vector3){wave->spawnPosition.x, 0.0f, wave->spawnPosition.y};
296 DrawCube(position, 0.25f, 2.0f, 0.25f, RED);
297 for (int j = 0; j < 100; j++)
298 {
299 Vector2 gradient = PathFindingGetGradient(position);
300 if (gradient.x == 0 && gradient.y == 0)
301 {
302 break;
303 }
304 position.x += gradient.x;
305 position.z += gradient.y;
306 DrawCube(position, 0.2f, 1.25f, 0.2f, ORANGE);
307 }
308 }
309 }
310
311 void DrawLevelBuildingState(Level *level)
312 {
313 BeginMode3D(level->camera);
314 DrawLevelGround(level);
315
316 int blockedCellCount = 0;
317 Vector2 blockedCells[1];
318 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera);
319 float planeDistance = ray.position.y / -ray.direction.y;
320 float planeX = ray.direction.x * planeDistance + ray.position.x;
321 float planeY = ray.direction.z * planeDistance + ray.position.z;
322 int16_t mapX = (int16_t)floorf(planeX + 0.5f);
323 int16_t mapY = (int16_t)floorf(planeY + 0.5f);
324 if (level->placementMode && !guiState.isBlocked && mapX >= -5 && mapX <= 5 && mapY >= -5 && mapY <= 5)
325 {
326 DrawCubeWires((Vector3){mapX, 0.2f, mapY}, 1.0f, 0.4f, 1.0f, RED);
327 blockedCells[blockedCellCount++] = (Vector2){mapX, mapY};
328 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
329 {
330 if (TowerTryAdd(level->placementMode, mapX, mapY))
331 {
332 level->playerGold -= GetTowerCosts(level->placementMode);
333 level->placementMode = TOWER_TYPE_NONE;
334 }
335 }
336 }
337
338 PathFindingMapUpdate(blockedCellCount, blockedCells);
339 DrawEnemyPath(level);
340 TowerDraw();
341 EnemyDraw();
342 ProjectileDraw();
343 ParticleDraw();
344
345 guiState.isBlocked = 0;
346
347 EndMode3D();
348
349 TowerDrawHealthBars(level->camera);
350
351 static ButtonState buildWallButtonState = {0};
352 static ButtonState buildGunButtonState = {0};
353 buildWallButtonState.isSelected = level->placementMode == TOWER_TYPE_WALL;
354 buildGunButtonState.isSelected = level->placementMode == TOWER_TYPE_ARCHER;
355
356 DrawBuildingBuildButton(level, 10, 10, 110, 30, TOWER_TYPE_WALL, "Wall");
357 DrawBuildingBuildButton(level, 10, 50, 110, 30, TOWER_TYPE_ARCHER, "Archer");
358 DrawBuildingBuildButton(level, 10, 90, 110, 30, TOWER_TYPE_BALLISTA, "Ballista");
359 DrawBuildingBuildButton(level, 10, 130, 110, 30, TOWER_TYPE_CATAPULT, "Catapult");
360
361 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
362 {
363 level->nextState = LEVEL_STATE_RESET;
364 }
365
366 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0))
367 {
368 level->nextState = LEVEL_STATE_BATTLE;
369 }
370
371 const char *text = "Building phase";
372 int textWidth = MeasureText(text, 20);
373 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
374 }
375
376 void InitBattleStateConditions(Level *level)
377 {
378 level->state = LEVEL_STATE_BATTLE;
379 level->nextState = LEVEL_STATE_NONE;
380 level->waveEndTimer = 0.0f;
381 for (int i = 0; i < 10; i++)
382 {
383 EnemyWave *wave = &level->waves[i];
384 wave->spawned = 0;
385 wave->timeToSpawnNext = wave->delay;
386 }
387 }
388
389 void DrawLevelBattleState(Level *level)
390 {
391 BeginMode3D(level->camera);
392 DrawLevelGround(level);
393 TowerDraw();
394 EnemyDraw();
395 ProjectileDraw();
396 ParticleDraw();
397 guiState.isBlocked = 0;
398 EndMode3D();
399
400 EnemyDrawHealthbars(level->camera);
401 TowerDrawHealthBars(level->camera);
402
403 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
404 {
405 level->nextState = LEVEL_STATE_RESET;
406 }
407
408 int maxCount = 0;
409 int remainingCount = 0;
410 for (int i = 0; i < 10; i++)
411 {
412 EnemyWave *wave = &level->waves[i];
413 if (wave->wave != level->currentWave)
414 {
415 continue;
416 }
417 maxCount += wave->count;
418 remainingCount += wave->count - wave->spawned;
419 }
420 int aliveCount = EnemyCount();
421 remainingCount += aliveCount;
422
423 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount);
424 int textWidth = MeasureText(text, 20);
425 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
426 }
427
428 void DrawLevel(Level *level)
429 {
430 switch (level->state)
431 {
432 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break;
433 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break;
434 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break;
435 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break;
436 default: break;
437 }
438
439 DrawLevelHud(level);
440 }
441
442 void UpdateLevel(Level *level)
443 {
444 if (level->state == LEVEL_STATE_BATTLE)
445 {
446 int activeWaves = 0;
447 for (int i = 0; i < 10; i++)
448 {
449 EnemyWave *wave = &level->waves[i];
450 if (wave->spawned >= wave->count || wave->wave != level->currentWave)
451 {
452 continue;
453 }
454 activeWaves++;
455 wave->timeToSpawnNext -= gameTime.deltaTime;
456 if (wave->timeToSpawnNext <= 0.0f)
457 {
458 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y);
459 if (enemy)
460 {
461 wave->timeToSpawnNext = wave->interval;
462 wave->spawned++;
463 }
464 }
465 }
466 if (GetTowerByType(TOWER_TYPE_BASE) == 0) {
467 level->waveEndTimer += gameTime.deltaTime;
468 if (level->waveEndTimer >= 2.0f)
469 {
470 level->nextState = LEVEL_STATE_LOST_WAVE;
471 }
472 }
473 else if (activeWaves == 0 && EnemyCount() == 0)
474 {
475 level->waveEndTimer += gameTime.deltaTime;
476 if (level->waveEndTimer >= 2.0f)
477 {
478 level->nextState = LEVEL_STATE_WON_WAVE;
479 }
480 }
481 }
482
483 PathFindingMapUpdate(0, 0);
484 EnemyUpdate();
485 TowerUpdate();
486 ProjectileUpdate();
487 ParticleUpdate();
488
489 if (level->nextState == LEVEL_STATE_RESET)
490 {
491 InitLevel(level);
492 }
493
494 if (level->nextState == LEVEL_STATE_BATTLE)
495 {
496 InitBattleStateConditions(level);
497 }
498
499 if (level->nextState == LEVEL_STATE_WON_WAVE)
500 {
501 level->currentWave++;
502 level->state = LEVEL_STATE_WON_WAVE;
503 }
504
505 if (level->nextState == LEVEL_STATE_LOST_WAVE)
506 {
507 level->state = LEVEL_STATE_LOST_WAVE;
508 }
509
510 if (level->nextState == LEVEL_STATE_BUILDING)
511 {
512 level->state = LEVEL_STATE_BUILDING;
513 }
514
515 if (level->nextState == LEVEL_STATE_WON_LEVEL)
516 {
517 // make something of this later
518 InitLevel(level);
519 }
520
521 level->nextState = LEVEL_STATE_NONE;
522 }
523
524 float nextSpawnTime = 0.0f;
525
526 void ResetGame()
527 {
528 InitLevel(currentLevel);
529 }
530
531 void InitGame()
532 {
533 TowerInit();
534 EnemyInit();
535 ProjectileInit();
536 ParticleInit();
537 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f);
538
539 currentLevel = levels;
540 InitLevel(currentLevel);
541 }
542
543 //# Immediate GUI functions
544
545 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth)
546 {
547 const float healthBarHeight = 6.0f;
548 const float healthBarOffset = 15.0f;
549 const float inset = 2.0f;
550 const float innerWidth = healthBarWidth - inset * 2;
551 const float innerHeight = healthBarHeight - inset * 2;
552
553 Vector2 screenPos = GetWorldToScreen(position, camera);
554 float centerX = screenPos.x - healthBarWidth * 0.5f;
555 float topY = screenPos.y - healthBarOffset;
556 DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK);
557 float healthWidth = innerWidth * healthRatio;
558 DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor);
559 }
560
561 int Button(const char *text, int x, int y, int width, int height, ButtonState *state)
562 {
563 Rectangle bounds = {x, y, width, height};
564 int isPressed = 0;
565 int isSelected = state && state->isSelected;
566 int isDisabled = state && state->isDisabled;
567 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !isDisabled)
568 {
569 Color color = isSelected ? DARKGRAY : GRAY;
570 DrawRectangle(x, y, width, height, color);
571 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
572 {
573 isPressed = 1;
574 }
575 guiState.isBlocked = 1;
576 }
577 else
578 {
579 Color color = isSelected ? WHITE : LIGHTGRAY;
580 DrawRectangle(x, y, width, height, color);
581 }
582 Font font = GetFontDefault();
583 Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1);
584 Color textColor = isDisabled ? GRAY : BLACK;
585 DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, textColor);
586 return isPressed;
587 }
588
589 //# Main game loop
590
591 void GameUpdate()
592 {
593 float dt = GetFrameTime();
594 // cap maximum delta time to 0.1 seconds to prevent large time steps
595 if (dt > 0.1f) dt = 0.1f;
596 gameTime.time += dt;
597 gameTime.deltaTime = dt;
598
599 UpdateLevel(currentLevel);
600 }
601
602 int main(void)
603 {
604 int screenWidth, screenHeight;
605 GetPreferredSize(&screenWidth, &screenHeight);
606 InitWindow(screenWidth, screenHeight, "Tower defense");
607 SetTargetFPS(30);
608
609 LoadAssets();
610 InitGame();
611
612 while (!WindowShouldClose())
613 {
614 if (IsPaused()) {
615 // canvas is not visible in browser - do nothing
616 continue;
617 }
618
619 BeginDrawing();
620 ClearBackground((Color){0x4E, 0x63, 0x26, 0xFF});
621
622 GameUpdate();
623 DrawLevel(currentLevel);
624
625 EndDrawing();
626 }
627
628 CloseWindow();
629
630 return 0;
631 }
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 #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 #define ENEMY_MAX_PATH_COUNT 8
12 #define ENEMY_MAX_COUNT 400
13 #define ENEMY_TYPE_NONE 0
14 #define ENEMY_TYPE_MINION 1
15
16 #define PARTICLE_MAX_COUNT 400
17 #define PARTICLE_TYPE_NONE 0
18 #define PARTICLE_TYPE_EXPLOSION 1
19
20 typedef struct Particle
21 {
22 uint8_t particleType;
23 float spawnTime;
24 float lifetime;
25 Vector3 position;
26 Vector3 velocity;
27 Vector3 scale;
28 } Particle;
29
30 #define TOWER_MAX_COUNT 400
31 enum TowerType
32 {
33 TOWER_TYPE_NONE,
34 TOWER_TYPE_BASE,
35 TOWER_TYPE_ARCHER,
36 TOWER_TYPE_BALLISTA,
37 TOWER_TYPE_CATAPULT,
38 TOWER_TYPE_WALL,
39 TOWER_TYPE_COUNT
40 };
41
42 typedef struct HitEffectConfig
43 {
44 float damage;
45 float areaDamageRadius;
46 float pushbackPowerDistance;
47 } HitEffectConfig;
48
49 typedef struct TowerTypeConfig
50 {
51 float cooldown;
52 float range;
53 float projectileSpeed;
54
55 uint8_t cost;
56 uint8_t projectileType;
57 uint16_t maxHealth;
58
59 HitEffectConfig hitEffect;
60 } TowerTypeConfig;
61
62 typedef struct Tower
63 {
64 int16_t x, y;
65 uint8_t towerType;
66 Vector2 lastTargetPosition;
67 float cooldown;
68 float damage;
69 } Tower;
70
71 typedef struct GameTime
72 {
73 float time;
74 float deltaTime;
75 } GameTime;
76
77 typedef struct ButtonState {
78 char isSelected;
79 char isDisabled;
80 } ButtonState;
81
82 typedef struct GUIState {
83 int isBlocked;
84 } GUIState;
85
86 typedef enum LevelState
87 {
88 LEVEL_STATE_NONE,
89 LEVEL_STATE_BUILDING,
90 LEVEL_STATE_BATTLE,
91 LEVEL_STATE_WON_WAVE,
92 LEVEL_STATE_LOST_WAVE,
93 LEVEL_STATE_WON_LEVEL,
94 LEVEL_STATE_RESET,
95 } LevelState;
96
97 typedef struct EnemyWave {
98 uint8_t enemyType;
99 uint8_t wave;
100 uint16_t count;
101 float interval;
102 float delay;
103 Vector2 spawnPosition;
104
105 uint16_t spawned;
106 float timeToSpawnNext;
107 } EnemyWave;
108
109 #define ENEMY_MAX_WAVE_COUNT 10
110
111 typedef struct Level
112 {
113 int seed;
114 LevelState state;
115 LevelState nextState;
116 Camera3D camera;
117 int placementMode;
118
119 int initialGold;
120 int playerGold;
121
122 EnemyWave waves[ENEMY_MAX_WAVE_COUNT];
123 int currentWave;
124 float waveEndTimer;
125 } Level;
126
127 typedef struct DeltaSrc
128 {
129 char x, y;
130 } DeltaSrc;
131
132 typedef struct PathfindingMap
133 {
134 int width, height;
135 float scale;
136 float *distances;
137 long *towerIndex;
138 DeltaSrc *deltaSrc;
139 float maxDistance;
140 Matrix toMapSpace;
141 Matrix toWorldSpace;
142 } PathfindingMap;
143
144 // when we execute the pathfinding algorithm, we need to store the active nodes
145 // in a queue. Each node has a position, a distance from the start, and the
146 // position of the node that we came from.
147 typedef struct PathfindingNode
148 {
149 int16_t x, y, fromX, fromY;
150 float distance;
151 } PathfindingNode;
152
153 typedef struct EnemyId
154 {
155 uint16_t index;
156 uint16_t generation;
157 } EnemyId;
158
159 typedef struct EnemyClassConfig
160 {
161 float speed;
162 float health;
163 float radius;
164 float maxAcceleration;
165 float requiredContactTime;
166 float explosionDamage;
167 float explosionRange;
168 float explosionPushbackPower;
169 int goldValue;
170 } EnemyClassConfig;
171
172 typedef struct Enemy
173 {
174 int16_t currentX, currentY;
175 int16_t nextX, nextY;
176 Vector2 simPosition;
177 Vector2 simVelocity;
178 uint16_t generation;
179 float walkedDistance;
180 float startMovingTime;
181 float damage, futureDamage;
182 float contactTime;
183 uint8_t enemyType;
184 uint8_t movePathCount;
185 Vector2 movePath[ENEMY_MAX_PATH_COUNT];
186 } Enemy;
187
188 // a unit that uses sprites to be drawn
189 #define SPRITE_UNIT_PHASE_WEAPON_IDLE 0
190 #define SPRITE_UNIT_PHASE_WEAPON_COOLDOWN 1
191 typedef struct SpriteUnit
192 {
193 Rectangle srcRect;
194 Vector2 offset;
195 int frameCount;
196 float frameDuration;
197 Rectangle srcWeaponIdleRect;
198 Vector2 srcWeaponIdleOffset;
199 Rectangle srcWeaponCooldownRect;
200 Vector2 srcWeaponCooldownOffset;
201 } SpriteUnit;
202
203 #define PROJECTILE_MAX_COUNT 1200
204 #define PROJECTILE_TYPE_NONE 0
205 #define PROJECTILE_TYPE_ARROW 1
206 #define PROJECTILE_TYPE_CATAPULT 2
207 #define PROJECTILE_TYPE_BALLISTA 3
208
209 typedef struct Projectile
210 {
211 uint8_t projectileType;
212 float shootTime;
213 float arrivalTime;
214 float distance;
215 Vector3 position;
216 Vector3 target;
217 Vector3 directionNormal;
218 EnemyId targetEnemy;
219 HitEffectConfig hitEffectConfig;
220 } Projectile;
221
222 //# Function declarations
223 float TowerGetMaxHealth(Tower *tower);
224 int Button(const char *text, int x, int y, int width, int height, ButtonState *state);
225 int EnemyAddDamageRange(Vector2 position, float range, float damage);
226 int EnemyAddDamage(Enemy *enemy, float damage);
227
228 //# Enemy functions
229 void EnemyInit();
230 void EnemyDraw();
231 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource);
232 void EnemyUpdate();
233 float EnemyGetCurrentMaxSpeed(Enemy *enemy);
234 float EnemyGetMaxHealth(Enemy *enemy);
235 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY);
236 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount);
237 EnemyId EnemyGetId(Enemy *enemy);
238 Enemy *EnemyTryResolve(EnemyId enemyId);
239 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY);
240 int EnemyAddDamage(Enemy *enemy, float damage);
241 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range);
242 int EnemyCount();
243 void EnemyDrawHealthbars(Camera3D camera);
244
245 //# Tower functions
246 void TowerInit();
247 Tower *TowerGetAt(int16_t x, int16_t y);
248 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y);
249 Tower *GetTowerByType(uint8_t towerType);
250 int GetTowerCosts(uint8_t towerType);
251 float TowerGetMaxHealth(Tower *tower);
252 void TowerDraw();
253 void TowerUpdate();
254 void TowerDrawHealthBars(Camera3D camera);
255 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase);
256
257 //# Particles
258 void ParticleInit();
259 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime);
260 void ParticleUpdate();
261 void ParticleDraw();
262
263 //# Projectiles
264 void ProjectileInit();
265 void ProjectileDraw();
266 void ProjectileUpdate();
267 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig);
268
269 //# Pathfinding map
270 void PathfindingMapInit(int width, int height, Vector3 translate, float scale);
271 float PathFindingGetDistance(int mapX, int mapY);
272 Vector2 PathFindingGetGradient(Vector3 world);
273 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY);
274 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells);
275 void PathFindingMapDraw();
276
277 //# UI
278 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth);
279
280 //# Level
281 void DrawLevelGround(Level *level);
282 void DrawEnemyPath(Level *level);
283
284 //# variables
285 extern Level *currentLevel;
286 extern Enemy enemies[ENEMY_MAX_COUNT];
287 extern int enemyCount;
288 extern EnemyClassConfig enemyClassConfigs[];
289
290 extern GUIState guiState;
291 extern GameTime gameTime;
292 extern Tower towers[TOWER_MAX_COUNT];
293 extern int towerCount;
294
295 extern Texture2D palette, spriteSheet;
296
297 #endif
1 #include "td_main.h"
2 #include <raymath.h>
3
4 static TowerTypeConfig towerTypeConfigs[TOWER_TYPE_COUNT] = {
5 [TOWER_TYPE_BASE] = {
6 .maxHealth = 10,
7 },
8 [TOWER_TYPE_ARCHER] = {
9 .cooldown = 0.5f,
10 .range = 3.0f,
11 .cost = 6,
12 .maxHealth = 10,
13 .projectileSpeed = 4.0f,
14 .projectileType = PROJECTILE_TYPE_ARROW,
15 .hitEffect = {
16 .damage = 3.0f,
17 }
18 },
19 [TOWER_TYPE_BALLISTA] = {
20 .cooldown = 1.5f,
21 .range = 6.0f,
22 .cost = 9,
23 .maxHealth = 10,
24 .projectileSpeed = 10.0f,
25 .projectileType = PROJECTILE_TYPE_BALLISTA,
26 .hitEffect = {
27 .damage = 6.0f,
28 .pushbackPowerDistance = 0.25f,
29 }
30 },
31 [TOWER_TYPE_CATAPULT] = {
32 .cooldown = 1.7f,
33 .range = 5.0f,
34 .cost = 10,
35 .maxHealth = 10,
36 .projectileSpeed = 3.0f,
37 .projectileType = PROJECTILE_TYPE_CATAPULT,
38 .hitEffect = {
39 .damage = 2.0f,
40 .areaDamageRadius = 1.75f,
41 }
42 },
43 [TOWER_TYPE_WALL] = {
44 .cost = 2,
45 .maxHealth = 10,
46 },
47 };
48
49 Tower towers[TOWER_MAX_COUNT];
50 int towerCount = 0;
51
52 Model towerModels[TOWER_TYPE_COUNT];
53
54 // definition of our archer unit
55 SpriteUnit archerUnit = {
56 .srcRect = {0, 0, 16, 16},
57 .offset = {7, 1},
58 .frameCount = 1,
59 .frameDuration = 0.0f,
60 .srcWeaponIdleRect = {16, 0, 6, 16},
61 .srcWeaponIdleOffset = {8, 0},
62 .srcWeaponCooldownRect = {22, 0, 11, 16},
63 .srcWeaponCooldownOffset = {10, 0},
64 };
65
66 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase)
67 {
68 float xScale = flip ? -1.0f : 1.0f;
69 Camera3D camera = currentLevel->camera;
70 float size = 0.5f;
71 Vector2 offset = (Vector2){ unit.offset.x / 16.0f * size, unit.offset.y / 16.0f * size * xScale };
72 Vector2 scale = (Vector2){ unit.srcRect.width / 16.0f * size, unit.srcRect.height / 16.0f * size };
73 // we want the sprite to face the camera, so we need to calculate the up vector
74 Vector3 forward = Vector3Subtract(camera.target, camera.position);
75 Vector3 up = {0, 1, 0};
76 Vector3 right = Vector3CrossProduct(forward, up);
77 up = Vector3Normalize(Vector3CrossProduct(right, forward));
78
79 Rectangle srcRect = unit.srcRect;
80 if (unit.frameCount > 1)
81 {
82 srcRect.x += (int)(t / unit.frameDuration) % unit.frameCount * srcRect.width;
83 }
84 if (flip)
85 {
86 srcRect.x += srcRect.width;
87 srcRect.width = -srcRect.width;
88 }
89 DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
90
91 if (phase == SPRITE_UNIT_PHASE_WEAPON_COOLDOWN && unit.srcWeaponCooldownRect.width > 0)
92 {
93 offset = (Vector2){ unit.srcWeaponCooldownOffset.x / 16.0f * size, unit.srcWeaponCooldownOffset.y / 16.0f * size };
94 scale = (Vector2){ unit.srcWeaponCooldownRect.width / 16.0f * size, unit.srcWeaponCooldownRect.height / 16.0f * size };
95 srcRect = unit.srcWeaponCooldownRect;
96 if (flip)
97 {
98 // position.x = flip * scale.x * 0.5f;
99 srcRect.x += srcRect.width;
100 srcRect.width = -srcRect.width;
101 offset.x = scale.x - offset.x;
102 }
103 DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
104 }
105 else if (phase == SPRITE_UNIT_PHASE_WEAPON_IDLE && unit.srcWeaponIdleRect.width > 0)
106 {
107 offset = (Vector2){ unit.srcWeaponIdleOffset.x / 16.0f * size, unit.srcWeaponIdleOffset.y / 16.0f * size };
108 scale = (Vector2){ unit.srcWeaponIdleRect.width / 16.0f * size, unit.srcWeaponIdleRect.height / 16.0f * size };
109 srcRect = unit.srcWeaponIdleRect;
110 if (flip)
111 {
112 // position.x = flip * scale.x * 0.5f;
113 srcRect.x += srcRect.width;
114 srcRect.width = -srcRect.width;
115 offset.x = scale.x - offset.x;
116 }
117 DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
118 }
119 }
120
121 void TowerInit()
122 {
123 for (int i = 0; i < TOWER_MAX_COUNT; i++)
124 {
125 towers[i] = (Tower){0};
126 }
127 towerCount = 0;
128
129 towerModels[TOWER_TYPE_BASE] = LoadModel("data/keep.glb");
130 towerModels[TOWER_TYPE_WALL] = LoadModel("data/wall-0000.glb");
131
132 for (int i = 0; i < TOWER_TYPE_COUNT; i++)
133 {
134 if (towerModels[i].materials)
135 {
136 // assign the palette texture to the material of the model (0 is not used afaik)
137 towerModels[i].materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
138 }
139 }
140 }
141
142 static void TowerGunUpdate(Tower *tower)
143 {
144 TowerTypeConfig config = towerTypeConfigs[tower->towerType];
145 if (tower->cooldown <= 0.0f)
146 {
147 Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, config.range);
148 if (enemy)
149 {
150 tower->cooldown = config.cooldown;
151 // shoot the enemy; determine future position of the enemy
152 float bulletSpeed = config.projectileSpeed;
153 Vector2 velocity = enemy->simVelocity;
154 Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0);
155 Vector2 towerPosition = {tower->x, tower->y};
156 float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed;
157 for (int i = 0; i < 8; i++) {
158 velocity = enemy->simVelocity;
159 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0);
160 float distance = Vector2Distance(towerPosition, futurePosition);
161 float eta2 = distance / bulletSpeed;
162 if (fabs(eta - eta2) < 0.01f) {
163 break;
164 }
165 eta = (eta2 + eta) * 0.5f;
166 }
167
168 ProjectileTryAdd(config.projectileType, enemy,
169 (Vector3){towerPosition.x, 1.33f, towerPosition.y},
170 (Vector3){futurePosition.x, 0.25f, futurePosition.y},
171 bulletSpeed, config.hitEffect);
172 enemy->futureDamage += config.hitEffect.damage;
173 tower->lastTargetPosition = futurePosition;
174 }
175 }
176 else
177 {
178 tower->cooldown -= gameTime.deltaTime;
179 }
180 }
181
182 Tower *TowerGetAt(int16_t x, int16_t y)
183 {
184 for (int i = 0; i < towerCount; i++)
185 {
186 if (towers[i].x == x && towers[i].y == y && towers[i].towerType != TOWER_TYPE_NONE)
187 {
188 return &towers[i];
189 }
190 }
191 return 0;
192 }
193
194 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y)
195 {
196 if (towerCount >= TOWER_MAX_COUNT)
197 {
198 return 0;
199 }
200
201 Tower *tower = TowerGetAt(x, y);
202 if (tower)
203 {
204 return 0;
205 }
206
207 tower = &towers[towerCount++];
208 tower->x = x;
209 tower->y = y;
210 tower->towerType = towerType;
211 tower->cooldown = 0.0f;
212 tower->damage = 0.0f;
213 return tower;
214 }
215
216 Tower *GetTowerByType(uint8_t towerType)
217 {
218 for (int i = 0; i < towerCount; i++)
219 {
220 if (towers[i].towerType == towerType)
221 {
222 return &towers[i];
223 }
224 }
225 return 0;
226 }
227
228 int GetTowerCosts(uint8_t towerType)
229 {
230 return towerTypeConfigs[towerType].cost;
231 }
232
233 float TowerGetMaxHealth(Tower *tower)
234 {
235 return towerTypeConfigs[tower->towerType].maxHealth;
236 }
237
238 void TowerDraw()
239 {
240 for (int i = 0; i < towerCount; i++)
241 {
242 Tower tower = towers[i];
243 if (tower.towerType == TOWER_TYPE_NONE)
244 {
245 continue;
246 }
247
248 switch (tower.towerType)
249 {
250 case TOWER_TYPE_ARCHER:
251 {
252 Vector2 screenPosTower = GetWorldToScreen((Vector3){tower.x, 0.0f, tower.y}, currentLevel->camera);
253 Vector2 screenPosTarget = GetWorldToScreen((Vector3){tower.lastTargetPosition.x, 0.0f, tower.lastTargetPosition.y}, currentLevel->camera);
254 DrawModel(towerModels[TOWER_TYPE_WALL], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
255 DrawSpriteUnit(archerUnit, (Vector3){tower.x, 1.0f, tower.y}, 0, screenPosTarget.x > screenPosTower.x,
256 tower.cooldown > 0.2f ? SPRITE_UNIT_PHASE_WEAPON_COOLDOWN : SPRITE_UNIT_PHASE_WEAPON_IDLE);
257 }
258 break;
259 case TOWER_TYPE_BALLISTA:
260 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, BROWN);
261 break;
262 case TOWER_TYPE_CATAPULT:
263 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, DARKGRAY);
264 break;
265 default:
266 if (towerModels[tower.towerType].materials)
267 {
268 DrawModel(towerModels[tower.towerType], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
269 } else {
270 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY);
271 }
272 break;
273 }
274 }
275 }
276
277 void TowerUpdate()
278 {
279 for (int i = 0; i < towerCount; i++)
280 {
281 Tower *tower = &towers[i];
282 switch (tower->towerType)
283 {
284 case TOWER_TYPE_CATAPULT:
285 case TOWER_TYPE_BALLISTA:
286 case TOWER_TYPE_ARCHER:
287 TowerGunUpdate(tower);
288 break;
289 }
290 }
291 }
292
293 void TowerDrawHealthBars(Camera3D camera)
294 {
295 for (int i = 0; i < towerCount; i++)
296 {
297 Tower *tower = &towers[i];
298 if (tower->towerType == TOWER_TYPE_NONE || tower->damage <= 0.0f)
299 {
300 continue;
301 }
302
303 Vector3 position = (Vector3){tower->x, 0.5f, tower->y};
304 float maxHealth = TowerGetMaxHealth(tower);
305 float health = maxHealth - tower->damage;
306 float healthRatio = health / maxHealth;
307
308 DrawHealthBar(camera, position, healthRatio, GREEN, 35.0f);
309 }
310 }
1 #include "td_main.h"
2 #include <raymath.h>
3 #include <stdlib.h>
4 #include <math.h>
5
6 EnemyClassConfig enemyClassConfigs[] = {
7 [ENEMY_TYPE_MINION] = {
8 .health = 10.0f,
9 .speed = 0.6f,
10 .radius = 0.25f,
11 .maxAcceleration = 1.0f,
12 .explosionDamage = 1.0f,
13 .requiredContactTime = 0.5f,
14 .explosionRange = 1.0f,
15 .explosionPushbackPower = 0.25f,
16 .goldValue = 1,
17 },
18 };
19
20 Enemy enemies[ENEMY_MAX_COUNT];
21 int enemyCount = 0;
22
23 SpriteUnit enemySprites[] = {
24 [ENEMY_TYPE_MINION] = {
25 .srcRect = {0, 16, 16, 16},
26 .offset = {8.0f, 0.0f},
27 .frameCount = 6,
28 .frameDuration = 0.1f,
29 },
30 };
31
32 void EnemyInit()
33 {
34 for (int i = 0; i < ENEMY_MAX_COUNT; i++)
35 {
36 enemies[i] = (Enemy){0};
37 }
38 enemyCount = 0;
39 }
40
41 float EnemyGetCurrentMaxSpeed(Enemy *enemy)
42 {
43 return enemyClassConfigs[enemy->enemyType].speed;
44 }
45
46 float EnemyGetMaxHealth(Enemy *enemy)
47 {
48 return enemyClassConfigs[enemy->enemyType].health;
49 }
50
51 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY)
52 {
53 int16_t castleX = 0;
54 int16_t castleY = 0;
55 int16_t dx = castleX - currentX;
56 int16_t dy = castleY - currentY;
57 if (dx == 0 && dy == 0)
58 {
59 *nextX = currentX;
60 *nextY = currentY;
61 return 1;
62 }
63 Vector2 gradient = PathFindingGetGradient((Vector3){currentX, 0, currentY});
64
65 if (gradient.x == 0 && gradient.y == 0)
66 {
67 *nextX = currentX;
68 *nextY = currentY;
69 return 1;
70 }
71
72 if (fabsf(gradient.x) > fabsf(gradient.y))
73 {
74 *nextX = currentX + (int16_t)(gradient.x > 0.0f ? 1 : -1);
75 *nextY = currentY;
76 return 0;
77 }
78 *nextX = currentX;
79 *nextY = currentY + (int16_t)(gradient.y > 0.0f ? 1 : -1);
80 return 0;
81 }
82
83
84 // this function predicts the movement of the unit for the next deltaT seconds
85 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount)
86 {
87 const float pointReachedDistance = 0.25f;
88 const float pointReachedDistance2 = pointReachedDistance * pointReachedDistance;
89 const float maxSimStepTime = 0.015625f;
90
91 float maxAcceleration = enemyClassConfigs[enemy->enemyType].maxAcceleration;
92 float maxSpeed = EnemyGetCurrentMaxSpeed(enemy);
93 int16_t nextX = enemy->nextX;
94 int16_t nextY = enemy->nextY;
95 Vector2 position = enemy->simPosition;
96 int passedCount = 0;
97 for (float t = 0.0f; t < deltaT; t += maxSimStepTime)
98 {
99 float stepTime = fminf(deltaT - t, maxSimStepTime);
100 Vector2 target = (Vector2){nextX, nextY};
101 float speed = Vector2Length(*velocity);
102 // draw the target position for debugging
103 DrawCubeWires((Vector3){target.x, 0.2f, target.y}, 0.1f, 0.4f, 0.1f, RED);
104 Vector2 lookForwardPos = Vector2Add(position, Vector2Scale(*velocity, speed));
105 if (Vector2DistanceSqr(target, lookForwardPos) <= pointReachedDistance2)
106 {
107 // we reached the target position, let's move to the next waypoint
108 EnemyGetNextPosition(nextX, nextY, &nextX, &nextY);
109 target = (Vector2){nextX, nextY};
110 // track how many waypoints we passed
111 passedCount++;
112 }
113
114 // acceleration towards the target
115 Vector2 unitDirection = Vector2Normalize(Vector2Subtract(target, lookForwardPos));
116 Vector2 acceleration = Vector2Scale(unitDirection, maxAcceleration * stepTime);
117 *velocity = Vector2Add(*velocity, acceleration);
118
119 // limit the speed to the maximum speed
120 if (speed > maxSpeed)
121 {
122 *velocity = Vector2Scale(*velocity, maxSpeed / speed);
123 }
124
125 // move the enemy
126 position = Vector2Add(position, Vector2Scale(*velocity, stepTime));
127 }
128
129 if (waypointPassedCount)
130 {
131 (*waypointPassedCount) = passedCount;
132 }
133
134 return position;
135 }
136
137 void EnemyDraw()
138 {
139 for (int i = 0; i < enemyCount; i++)
140 {
141 Enemy enemy = enemies[i];
142 if (enemy.enemyType == ENEMY_TYPE_NONE)
143 {
144 continue;
145 }
146
147 Vector2 position = EnemyGetPosition(&enemy, gameTime.time - enemy.startMovingTime, &enemy.simVelocity, 0);
148
149 // don't draw any trails for now; might replace this with footprints later
150 // if (enemy.movePathCount > 0)
151 // {
152 // Vector3 p = {enemy.movePath[0].x, 0.2f, enemy.movePath[0].y};
153 // DrawLine3D(p, (Vector3){position.x, 0.2f, position.y}, GREEN);
154 // }
155 // for (int j = 1; j < enemy.movePathCount; j++)
156 // {
157 // Vector3 p = {enemy.movePath[j - 1].x, 0.2f, enemy.movePath[j - 1].y};
158 // Vector3 q = {enemy.movePath[j].x, 0.2f, enemy.movePath[j].y};
159 // DrawLine3D(p, q, GREEN);
160 // }
161
162 switch (enemy.enemyType)
163 {
164 case ENEMY_TYPE_MINION:
165 DrawSpriteUnit(enemySprites[ENEMY_TYPE_MINION], (Vector3){position.x, 0.0f, position.y},
166 enemy.walkedDistance, 0, 0);
167 break;
168 }
169 }
170 }
171
172 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource)
173 {
174 // damage the tower
175 float explosionDamge = enemyClassConfigs[enemy->enemyType].explosionDamage;
176 float explosionRange = enemyClassConfigs[enemy->enemyType].explosionRange;
177 float explosionPushbackPower = enemyClassConfigs[enemy->enemyType].explosionPushbackPower;
178 float explosionRange2 = explosionRange * explosionRange;
179 tower->damage += enemyClassConfigs[enemy->enemyType].explosionDamage;
180 // explode the enemy
181 if (tower->damage >= TowerGetMaxHealth(tower))
182 {
183 tower->towerType = TOWER_TYPE_NONE;
184 }
185
186 ParticleAdd(PARTICLE_TYPE_EXPLOSION,
187 explosionSource,
188 (Vector3){0, 0.1f, 0}, (Vector3){1.0f, 1.0f, 1.0f}, 1.0f);
189
190 enemy->enemyType = ENEMY_TYPE_NONE;
191
192 // push back enemies & dealing damage
193 for (int i = 0; i < enemyCount; i++)
194 {
195 Enemy *other = &enemies[i];
196 if (other->enemyType == ENEMY_TYPE_NONE)
197 {
198 continue;
199 }
200 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, other->simPosition);
201 if (distanceSqr > 0 && distanceSqr < explosionRange2)
202 {
203 Vector2 direction = Vector2Normalize(Vector2Subtract(other->simPosition, enemy->simPosition));
204 other->simPosition = Vector2Add(other->simPosition, Vector2Scale(direction, explosionPushbackPower));
205 EnemyAddDamage(other, explosionDamge);
206 }
207 }
208 }
209
210 void EnemyUpdate()
211 {
212 const float castleX = 0;
213 const float castleY = 0;
214 const float maxPathDistance2 = 0.25f * 0.25f;
215
216 for (int i = 0; i < enemyCount; i++)
217 {
218 Enemy *enemy = &enemies[i];
219 if (enemy->enemyType == ENEMY_TYPE_NONE)
220 {
221 continue;
222 }
223
224 int waypointPassedCount = 0;
225 Vector2 prevPosition = enemy->simPosition;
226 enemy->simPosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &enemy->simVelocity, &waypointPassedCount);
227 enemy->startMovingTime = gameTime.time;
228 enemy->walkedDistance += Vector2Distance(prevPosition, enemy->simPosition);
229 // track path of unit
230 if (enemy->movePathCount == 0 || Vector2DistanceSqr(enemy->simPosition, enemy->movePath[0]) > maxPathDistance2)
231 {
232 for (int j = ENEMY_MAX_PATH_COUNT - 1; j > 0; j--)
233 {
234 enemy->movePath[j] = enemy->movePath[j - 1];
235 }
236 enemy->movePath[0] = enemy->simPosition;
237 if (++enemy->movePathCount > ENEMY_MAX_PATH_COUNT)
238 {
239 enemy->movePathCount = ENEMY_MAX_PATH_COUNT;
240 }
241 }
242
243 if (waypointPassedCount > 0)
244 {
245 enemy->currentX = enemy->nextX;
246 enemy->currentY = enemy->nextY;
247 if (EnemyGetNextPosition(enemy->currentX, enemy->currentY, &enemy->nextX, &enemy->nextY) &&
248 Vector2DistanceSqr(enemy->simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
249 {
250 // enemy reached the castle; remove it
251 enemy->enemyType = ENEMY_TYPE_NONE;
252 continue;
253 }
254 }
255 }
256
257 // handle collisions between enemies
258 for (int i = 0; i < enemyCount - 1; i++)
259 {
260 Enemy *enemyA = &enemies[i];
261 if (enemyA->enemyType == ENEMY_TYPE_NONE)
262 {
263 continue;
264 }
265 for (int j = i + 1; j < enemyCount; j++)
266 {
267 Enemy *enemyB = &enemies[j];
268 if (enemyB->enemyType == ENEMY_TYPE_NONE)
269 {
270 continue;
271 }
272 float distanceSqr = Vector2DistanceSqr(enemyA->simPosition, enemyB->simPosition);
273 float radiusA = enemyClassConfigs[enemyA->enemyType].radius;
274 float radiusB = enemyClassConfigs[enemyB->enemyType].radius;
275 float radiusSum = radiusA + radiusB;
276 if (distanceSqr < radiusSum * radiusSum && distanceSqr > 0.001f)
277 {
278 // collision
279 float distance = sqrtf(distanceSqr);
280 float overlap = radiusSum - distance;
281 // move the enemies apart, but softly; if we have a clog of enemies,
282 // moving them perfectly apart can cause them to jitter
283 float positionCorrection = overlap / 5.0f;
284 Vector2 direction = (Vector2){
285 (enemyB->simPosition.x - enemyA->simPosition.x) / distance * positionCorrection,
286 (enemyB->simPosition.y - enemyA->simPosition.y) / distance * positionCorrection};
287 enemyA->simPosition = Vector2Subtract(enemyA->simPosition, direction);
288 enemyB->simPosition = Vector2Add(enemyB->simPosition, direction);
289 }
290 }
291 }
292
293 // handle collisions between enemies and towers
294 for (int i = 0; i < enemyCount; i++)
295 {
296 Enemy *enemy = &enemies[i];
297 if (enemy->enemyType == ENEMY_TYPE_NONE)
298 {
299 continue;
300 }
301 enemy->contactTime -= gameTime.deltaTime;
302 if (enemy->contactTime < 0.0f)
303 {
304 enemy->contactTime = 0.0f;
305 }
306
307 float enemyRadius = enemyClassConfigs[enemy->enemyType].radius;
308 // linear search over towers; could be optimized by using path finding tower map,
309 // but for now, we keep it simple
310 for (int j = 0; j < towerCount; j++)
311 {
312 Tower *tower = &towers[j];
313 if (tower->towerType == TOWER_TYPE_NONE)
314 {
315 continue;
316 }
317 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){tower->x, tower->y});
318 float combinedRadius = enemyRadius + 0.708; // sqrt(0.5^2 + 0.5^2), corner-center distance of square with side length 1
319 if (distanceSqr > combinedRadius * combinedRadius)
320 {
321 continue;
322 }
323 // potential collision; square / circle intersection
324 float dx = tower->x - enemy->simPosition.x;
325 float dy = tower->y - enemy->simPosition.y;
326 float absDx = fabsf(dx);
327 float absDy = fabsf(dy);
328 Vector3 contactPoint = {0};
329 if (absDx <= 0.5f && absDx <= absDy) {
330 // vertical collision; push the enemy out horizontally
331 float overlap = enemyRadius + 0.5f - absDy;
332 if (overlap < 0.0f)
333 {
334 continue;
335 }
336 float direction = dy > 0.0f ? -1.0f : 1.0f;
337 enemy->simPosition.y += direction * overlap;
338 contactPoint = (Vector3){enemy->simPosition.x, 0.2f, tower->y + direction * 0.5f};
339 }
340 else if (absDy <= 0.5f && absDy <= absDx)
341 {
342 // horizontal collision; push the enemy out vertically
343 float overlap = enemyRadius + 0.5f - absDx;
344 if (overlap < 0.0f)
345 {
346 continue;
347 }
348 float direction = dx > 0.0f ? -1.0f : 1.0f;
349 enemy->simPosition.x += direction * overlap;
350 contactPoint = (Vector3){tower->x + direction * 0.5f, 0.2f, enemy->simPosition.y};
351 }
352 else
353 {
354 // possible collision with a corner
355 float cornerDX = dx > 0.0f ? -0.5f : 0.5f;
356 float cornerDY = dy > 0.0f ? -0.5f : 0.5f;
357 float cornerX = tower->x + cornerDX;
358 float cornerY = tower->y + cornerDY;
359 float cornerDistanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){cornerX, cornerY});
360 if (cornerDistanceSqr > enemyRadius * enemyRadius)
361 {
362 continue;
363 }
364 // push the enemy out along the diagonal
365 float cornerDistance = sqrtf(cornerDistanceSqr);
366 float overlap = enemyRadius - cornerDistance;
367 float directionX = cornerDistance > 0.0f ? (cornerX - enemy->simPosition.x) / cornerDistance : -cornerDX;
368 float directionY = cornerDistance > 0.0f ? (cornerY - enemy->simPosition.y) / cornerDistance : -cornerDY;
369 enemy->simPosition.x -= directionX * overlap;
370 enemy->simPosition.y -= directionY * overlap;
371 contactPoint = (Vector3){cornerX, 0.2f, cornerY};
372 }
373
374 if (enemyClassConfigs[enemy->enemyType].explosionDamage > 0.0f)
375 {
376 enemy->contactTime += gameTime.deltaTime * 2.0f; // * 2 to undo the subtraction above
377 if (enemy->contactTime >= enemyClassConfigs[enemy->enemyType].requiredContactTime)
378 {
379 EnemyTriggerExplode(enemy, tower, contactPoint);
380 }
381 }
382 }
383 }
384 }
385
386 EnemyId EnemyGetId(Enemy *enemy)
387 {
388 return (EnemyId){enemy - enemies, enemy->generation};
389 }
390
391 Enemy *EnemyTryResolve(EnemyId enemyId)
392 {
393 if (enemyId.index >= ENEMY_MAX_COUNT)
394 {
395 return 0;
396 }
397 Enemy *enemy = &enemies[enemyId.index];
398 if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE)
399 {
400 return 0;
401 }
402 return enemy;
403 }
404
405 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY)
406 {
407 Enemy *spawn = 0;
408 for (int i = 0; i < enemyCount; i++)
409 {
410 Enemy *enemy = &enemies[i];
411 if (enemy->enemyType == ENEMY_TYPE_NONE)
412 {
413 spawn = enemy;
414 break;
415 }
416 }
417
418 if (enemyCount < ENEMY_MAX_COUNT && !spawn)
419 {
420 spawn = &enemies[enemyCount++];
421 }
422
423 if (spawn)
424 {
425 spawn->currentX = currentX;
426 spawn->currentY = currentY;
427 spawn->nextX = currentX;
428 spawn->nextY = currentY;
429 spawn->simPosition = (Vector2){currentX, currentY};
430 spawn->simVelocity = (Vector2){0, 0};
431 spawn->enemyType = enemyType;
432 spawn->startMovingTime = gameTime.time;
433 spawn->damage = 0.0f;
434 spawn->futureDamage = 0.0f;
435 spawn->generation++;
436 spawn->movePathCount = 0;
437 spawn->walkedDistance = 0.0f;
438 }
439
440 return spawn;
441 }
442
443 int EnemyAddDamageRange(Vector2 position, float range, float damage)
444 {
445 int count = 0;
446 float range2 = range * range;
447 for (int i = 0; i < enemyCount; i++)
448 {
449 Enemy *enemy = &enemies[i];
450 if (enemy->enemyType == ENEMY_TYPE_NONE)
451 {
452 continue;
453 }
454 float distance2 = Vector2DistanceSqr(position, enemy->simPosition);
455 if (distance2 <= range2)
456 {
457 EnemyAddDamage(enemy, damage);
458 count++;
459 }
460 }
461 return count;
462 }
463
464 int EnemyAddDamage(Enemy *enemy, float damage)
465 {
466 enemy->damage += damage;
467 if (enemy->damage >= EnemyGetMaxHealth(enemy))
468 {
469 currentLevel->playerGold += enemyClassConfigs[enemy->enemyType].goldValue;
470 enemy->enemyType = ENEMY_TYPE_NONE;
471 return 1;
472 }
473
474 return 0;
475 }
476
477 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
478 {
479 int16_t castleX = 0;
480 int16_t castleY = 0;
481 Enemy* closest = 0;
482 int16_t closestDistance = 0;
483 float range2 = range * range;
484 for (int i = 0; i < enemyCount; i++)
485 {
486 Enemy* enemy = &enemies[i];
487 if (enemy->enemyType == ENEMY_TYPE_NONE)
488 {
489 continue;
490 }
491 float maxHealth = EnemyGetMaxHealth(enemy);
492 if (enemy->futureDamage >= maxHealth)
493 {
494 // ignore enemies that will die soon
495 continue;
496 }
497 int16_t dx = castleX - enemy->currentX;
498 int16_t dy = castleY - enemy->currentY;
499 int16_t distance = abs(dx) + abs(dy);
500 if (!closest || distance < closestDistance)
501 {
502 float tdx = towerX - enemy->currentX;
503 float tdy = towerY - enemy->currentY;
504 float tdistance2 = tdx * tdx + tdy * tdy;
505 if (tdistance2 <= range2)
506 {
507 closest = enemy;
508 closestDistance = distance;
509 }
510 }
511 }
512 return closest;
513 }
514
515 int EnemyCount()
516 {
517 int count = 0;
518 for (int i = 0; i < enemyCount; i++)
519 {
520 if (enemies[i].enemyType != ENEMY_TYPE_NONE)
521 {
522 count++;
523 }
524 }
525 return count;
526 }
527
528 void EnemyDrawHealthbars(Camera3D camera)
529 {
530 for (int i = 0; i < enemyCount; i++)
531 {
532 Enemy *enemy = &enemies[i];
533 if (enemy->enemyType == ENEMY_TYPE_NONE || enemy->damage == 0.0f)
534 {
535 continue;
536 }
537 Vector3 position = (Vector3){enemy->simPosition.x, 0.5f, enemy->simPosition.y};
538 float maxHealth = EnemyGetMaxHealth(enemy);
539 float health = maxHealth - enemy->damage;
540 float healthRatio = health / maxHealth;
541
542 DrawHealthBar(camera, position, healthRatio, GREEN, 15.0f);
543 }
544 }
1 #include "td_main.h"
2 #include <raymath.h>
3
4 static Projectile projectiles[PROJECTILE_MAX_COUNT];
5 static int projectileCount = 0;
6
7 typedef struct ProjectileConfig
8 {
9 float arcFactor;
10 Color color;
11 Color trailColor;
12 } ProjectileConfig;
13
14 ProjectileConfig projectileConfigs[] = {
15 [PROJECTILE_TYPE_ARROW] = {
16 .arcFactor = 0.15f,
17 .color = RED,
18 .trailColor = BROWN,
19 },
20 [PROJECTILE_TYPE_CATAPULT] = {
21 .arcFactor = 0.5f,
22 .color = RED,
23 .trailColor = GRAY,
24 },
25 [PROJECTILE_TYPE_BALLISTA] = {
26 .arcFactor = 0.025f,
27 .color = RED,
28 .trailColor = BROWN,
29 },
30 };
31
32 void ProjectileInit()
33 {
34 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
35 {
36 projectiles[i] = (Projectile){0};
37 }
38 }
39
40 void ProjectileDraw()
41 {
42 for (int i = 0; i < projectileCount; i++)
43 {
44 Projectile projectile = projectiles[i];
45 if (projectile.projectileType == PROJECTILE_TYPE_NONE)
46 {
47 continue;
48 }
49 float transition = (gameTime.time - projectile.shootTime) / (projectile.arrivalTime - projectile.shootTime);
50 if (transition >= 1.0f)
51 {
52 continue;
53 }
54
55 ProjectileConfig config = projectileConfigs[projectile.projectileType];
56 for (float transitionOffset = 0.0f; transitionOffset < 1.0f; transitionOffset += 0.1f)
57 {
58 float t = transition + transitionOffset * 0.3f;
59 if (t > 1.0f)
60 {
61 break;
62 }
63 Vector3 position = Vector3Lerp(projectile.position, projectile.target, t);
64 Color color = config.color;
65 color = ColorLerp(config.trailColor, config.color, transitionOffset * transitionOffset);
66 // fake a ballista flight path using parabola equation
67 float parabolaT = t - 0.5f;
68 parabolaT = 1.0f - 4.0f * parabolaT * parabolaT;
69 position.y += config.arcFactor * parabolaT * projectile.distance;
70
71 float size = 0.06f * (transitionOffset + 0.25f);
72 DrawCube(position, size, size, size, color);
73 }
74 }
75 }
76
77 void ProjectileUpdate()
78 {
79 for (int i = 0; i < projectileCount; i++)
80 {
81 Projectile *projectile = &projectiles[i];
82 if (projectile->projectileType == PROJECTILE_TYPE_NONE)
83 {
84 continue;
85 }
86 float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime);
87 if (transition >= 1.0f)
88 {
89 projectile->projectileType = PROJECTILE_TYPE_NONE;
90 Enemy *enemy = EnemyTryResolve(projectile->targetEnemy);
91 if (enemy && projectile->hitEffectConfig.pushbackPowerDistance > 0.0f)
92 {
93 Vector2 direction = Vector2Normalize(Vector2Subtract((Vector2){projectile->target.x, projectile->target.z}, enemy->simPosition));
94 enemy->simPosition = Vector2Add(enemy->simPosition, Vector2Scale(direction, projectile->hitEffectConfig.pushbackPowerDistance));
95 }
96
97 if (projectile->hitEffectConfig.areaDamageRadius > 0.0f)
98 {
99 EnemyAddDamageRange((Vector2){projectile->target.x, projectile->target.z}, projectile->hitEffectConfig.areaDamageRadius, projectile->hitEffectConfig.damage);
100 // pancaked sphere explosion
101 float r = projectile->hitEffectConfig.areaDamageRadius;
102 ParticleAdd(PARTICLE_TYPE_EXPLOSION, projectile->target, (Vector3){0}, (Vector3){r, r * 0.2f, r}, 0.33f);
103 }
104 else if (projectile->hitEffectConfig.damage > 0.0f && enemy)
105 {
106 EnemyAddDamage(enemy, projectile->hitEffectConfig.damage);
107 }
108 continue;
109 }
110 }
111 }
112
113 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig)
114 {
115 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
116 {
117 Projectile *projectile = &projectiles[i];
118 if (projectile->projectileType == PROJECTILE_TYPE_NONE)
119 {
120 projectile->projectileType = projectileType;
121 projectile->shootTime = gameTime.time;
122 float distance = Vector3Distance(position, target);
123 projectile->arrivalTime = gameTime.time + distance / speed;
124 projectile->position = position;
125 projectile->target = target;
126 projectile->directionNormal = Vector3Scale(Vector3Subtract(target, position), 1.0f / distance);
127 projectile->distance = distance;
128 projectile->targetEnemy = EnemyGetId(enemy);
129 projectileCount = projectileCount <= i ? i + 1 : projectileCount;
130 projectile->hitEffectConfig = hitEffectConfig;
131 return projectile;
132 }
133 }
134 return 0;
135 }
1 #include "td_main.h"
2 #include <raymath.h>
3 #include <rlgl.h>
4
5 static Particle particles[PARTICLE_MAX_COUNT];
6 static int particleCount = 0;
7
8 void ParticleInit()
9 {
10 for (int i = 0; i < PARTICLE_MAX_COUNT; i++)
11 {
12 particles[i] = (Particle){0};
13 }
14 particleCount = 0;
15 }
16
17 static void DrawExplosionParticle(Particle *particle, float transition)
18 {
19 Vector3 scale = particle->scale;
20 float size = 1.0f * (1.0f - transition);
21 Color startColor = WHITE;
22 Color endColor = RED;
23 Color color = ColorLerp(startColor, endColor, transition);
24
25 rlPushMatrix();
26 rlTranslatef(particle->position.x, particle->position.y, particle->position.z);
27 rlScalef(scale.x, scale.y, scale.z);
28 DrawSphere(Vector3Zero(), size, color);
29 rlPopMatrix();
30 }
31
32 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime)
33 {
34 if (particleCount >= PARTICLE_MAX_COUNT)
35 {
36 return;
37 }
38
39 int index = -1;
40 for (int i = 0; i < particleCount; i++)
41 {
42 if (particles[i].particleType == PARTICLE_TYPE_NONE)
43 {
44 index = i;
45 break;
46 }
47 }
48
49 if (index == -1)
50 {
51 index = particleCount++;
52 }
53
54 Particle *particle = &particles[index];
55 particle->particleType = particleType;
56 particle->spawnTime = gameTime.time;
57 particle->lifetime = lifetime;
58 particle->position = position;
59 particle->velocity = velocity;
60 particle->scale = scale;
61 }
62
63 void ParticleUpdate()
64 {
65 for (int i = 0; i < particleCount; i++)
66 {
67 Particle *particle = &particles[i];
68 if (particle->particleType == PARTICLE_TYPE_NONE)
69 {
70 continue;
71 }
72
73 float age = gameTime.time - particle->spawnTime;
74
75 if (particle->lifetime > age)
76 {
77 particle->position = Vector3Add(particle->position, Vector3Scale(particle->velocity, gameTime.deltaTime));
78 }
79 else {
80 particle->particleType = PARTICLE_TYPE_NONE;
81 }
82 }
83 }
84
85 void ParticleDraw()
86 {
87 for (int i = 0; i < particleCount; i++)
88 {
89 Particle particle = particles[i];
90 if (particle.particleType == PARTICLE_TYPE_NONE)
91 {
92 continue;
93 }
94
95 float age = gameTime.time - particle.spawnTime;
96 float transition = age / particle.lifetime;
97 switch (particle.particleType)
98 {
99 case PARTICLE_TYPE_EXPLOSION:
100 DrawExplosionParticle(&particle, transition);
101 break;
102 default:
103 DrawCube(particle.position, 0.3f, 0.5f, 0.3f, RED);
104 break;
105 }
106 }
107 }
1 #include "raylib.h"
2 #include "preferred_size.h"
3
4 // Since the canvas size is not known at compile time, we need to query it at runtime;
5 // the following platform specific code obtains the canvas size and we will use this
6 // size as the preferred size for the window at init time. We're ignoring here the
7 // possibility of the canvas size changing during runtime - this would require to
8 // poll the canvas size in the game loop or establishing a callback to be notified
9
10 #ifdef PLATFORM_WEB
11 #include <emscripten.h>
12 EMSCRIPTEN_RESULT emscripten_get_element_css_size(const char *target, double *width, double *height);
13
14 void GetPreferredSize(int *screenWidth, int *screenHeight)
15 {
16 double canvasWidth, canvasHeight;
17 emscripten_get_element_css_size("#" CANVAS_NAME, &canvasWidth, &canvasHeight);
18 *screenWidth = (int)canvasWidth;
19 *screenHeight = (int)canvasHeight;
20 TraceLog(LOG_INFO, "preferred size for %s: %d %d", CANVAS_NAME, *screenWidth, *screenHeight);
21 }
22
23 int IsPaused()
24 {
25 const char *js = "(function(){\n"
26 " var canvas = document.getElementById(\"" CANVAS_NAME "\");\n"
27 " var rect = canvas.getBoundingClientRect();\n"
28 " var isVisible = (\n"
29 " rect.top >= 0 &&\n"
30 " rect.left >= 0 &&\n"
31 " rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&\n"
32 " rect.right <= (window.innerWidth || document.documentElement.clientWidth)\n"
33 " );\n"
34 " return isVisible ? 0 : 1;\n"
35 "})()";
36 return emscripten_run_script_int(js);
37 }
38
39 #else
40 void GetPreferredSize(int *screenWidth, int *screenHeight)
41 {
42 *screenWidth = 600;
43 *screenHeight = 240;
44 }
45 int IsPaused()
46 {
47 return 0;
48 }
49 #endif
1 #ifndef PREFERRED_SIZE_H
2 #define PREFERRED_SIZE_H
3
4 void GetPreferredSize(int *screenWidth, int *screenHeight);
5 int IsPaused();
6
7 #endif
The path preview is now reacting to the position of the mouse cursor. The path preview is however only for debugging. Let's add some graphics for the path. There are again multiple ways to do this. I first considered creating 4 models of arrow lines and rendering them in each cell, according to the direction the enemy will move to. But one factor to consider is to communicate the direction of movement, which I found a bit difficult to achieve with this approach.
My second idea is to draw smaller arrows on the path the enemies will take, using the same function as enemies use to move. This way, we can reuse the code and there is only a single arrow model. Drawing them with small distances while animating them in a forward movement fashion should give a good impression of the path the enemies will take.
1 #include "td_main.h"
2 #include <raymath.h>
3 #include <stdlib.h>
4 #include <math.h>
5
6 //# Variables
7 GUIState guiState = {0};
8 GameTime gameTime = {0};
9
10 Model floorTileAModel = {0};
11 Model floorTileBModel = {0};
12 Model treeModel[2] = {0};
13 Model firTreeModel[2] = {0};
14 Model rockModels[5] = {0};
15 Model grassPatchModel[1] = {0};
16
17 Model pathArrowModel = {0};
18
19 Texture2D palette, spriteSheet;
20
21 Level levels[] = {
22 [0] = {
23 .state = LEVEL_STATE_BUILDING,
24 .initialGold = 20,
25 .waves[0] = {
26 .enemyType = ENEMY_TYPE_MINION,
27 .wave = 0,
28 .count = 10,
29 .interval = 2.5f,
30 .delay = 1.0f,
31 .spawnPosition = {0, 6},
32 },
33 .waves[1] = {
34 .enemyType = ENEMY_TYPE_MINION,
35 .wave = 1,
36 .count = 20,
37 .interval = 1.5f,
38 .delay = 1.0f,
39 .spawnPosition = {0, 6},
40 },
41 .waves[2] = {
42 .enemyType = ENEMY_TYPE_MINION,
43 .wave = 2,
44 .count = 30,
45 .interval = 1.2f,
46 .delay = 1.0f,
47 .spawnPosition = {0, 6},
48 }
49 },
50 };
51
52 Level *currentLevel = levels;
53
54 //# Game
55
56 static Model LoadGLBModel(char *filename)
57 {
58 Model model = LoadModel(TextFormat("data/%s.glb",filename));
59 for (int i = 0; i < model.materialCount; i++)
60 {
61 model.materials[i].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
62 }
63 return model;
64 }
65
66 void LoadAssets()
67 {
68 // load a sprite sheet that contains all units
69 spriteSheet = LoadTexture("data/spritesheet.png");
70 SetTextureFilter(spriteSheet, TEXTURE_FILTER_BILINEAR);
71
72 // we'll use a palette texture to colorize the all buildings and environment art
73 palette = LoadTexture("data/palette.png");
74 // The texture uses gradients on very small space, so we'll enable bilinear filtering
75 SetTextureFilter(palette, TEXTURE_FILTER_BILINEAR);
76
77 floorTileAModel = LoadGLBModel("floor-tile-a");
78 floorTileBModel = LoadGLBModel("floor-tile-b");
79 treeModel[0] = LoadGLBModel("leaftree-large-1-a");
80 treeModel[1] = LoadGLBModel("leaftree-large-1-b");
81 firTreeModel[0] = LoadGLBModel("firtree-1-a");
82 firTreeModel[1] = LoadGLBModel("firtree-1-b");
83 rockModels[0] = LoadGLBModel("rock-1");
84 rockModels[1] = LoadGLBModel("rock-2");
85 rockModels[2] = LoadGLBModel("rock-3");
86 rockModels[3] = LoadGLBModel("rock-4");
87 rockModels[4] = LoadGLBModel("rock-5");
88 grassPatchModel[0] = LoadGLBModel("grass-patch-1");
89
90 pathArrowModel = LoadGLBModel("direction-arrow-x");
91 }
92
93 void InitLevel(Level *level)
94 {
95 level->seed = (int)(GetTime() * 100.0f);
96
97 TowerInit();
98 EnemyInit();
99 ProjectileInit();
100 ParticleInit();
101 TowerTryAdd(TOWER_TYPE_BASE, 0, 0);
102
103 level->placementMode = 0;
104 level->state = LEVEL_STATE_BUILDING;
105 level->nextState = LEVEL_STATE_NONE;
106 level->playerGold = level->initialGold;
107 level->currentWave = 0;
108
109 Camera *camera = &level->camera;
110 camera->position = (Vector3){4.0f, 8.0f, 8.0f};
111 camera->target = (Vector3){0.0f, 0.0f, 0.0f};
112 camera->up = (Vector3){0.0f, 1.0f, 0.0f};
113 camera->fovy = 10.0f;
114 camera->projection = CAMERA_ORTHOGRAPHIC;
115 }
116
117 void DrawLevelHud(Level *level)
118 {
119 const char *text = TextFormat("Gold: %d", level->playerGold);
120 Font font = GetFontDefault();
121 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 120, 10}, font.baseSize * 2.0f, 2.0f, BLACK);
122 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 122, 8}, font.baseSize * 2.0f, 2.0f, YELLOW);
123 }
124
125 void DrawLevelReportLostWave(Level *level)
126 {
127 BeginMode3D(level->camera);
128 DrawLevelGround(level);
129 TowerDraw();
130 EnemyDraw();
131 ProjectileDraw();
132 ParticleDraw();
133 guiState.isBlocked = 0;
134 EndMode3D();
135
136 TowerDrawHealthBars(level->camera);
137
138 const char *text = "Wave lost";
139 int textWidth = MeasureText(text, 20);
140 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
141
142 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
143 {
144 level->nextState = LEVEL_STATE_RESET;
145 }
146 }
147
148 int HasLevelNextWave(Level *level)
149 {
150 for (int i = 0; i < 10; i++)
151 {
152 EnemyWave *wave = &level->waves[i];
153 if (wave->wave == level->currentWave)
154 {
155 return 1;
156 }
157 }
158 return 0;
159 }
160
161 void DrawLevelReportWonWave(Level *level)
162 {
163 BeginMode3D(level->camera);
164 DrawLevelGround(level);
165 TowerDraw();
166 EnemyDraw();
167 ProjectileDraw();
168 ParticleDraw();
169 guiState.isBlocked = 0;
170 EndMode3D();
171
172 TowerDrawHealthBars(level->camera);
173
174 const char *text = "Wave won";
175 int textWidth = MeasureText(text, 20);
176 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
177
178
179 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
180 {
181 level->nextState = LEVEL_STATE_RESET;
182 }
183
184 if (HasLevelNextWave(level))
185 {
186 if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
187 {
188 level->nextState = LEVEL_STATE_BUILDING;
189 }
190 }
191 else {
192 if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
193 {
194 level->nextState = LEVEL_STATE_WON_LEVEL;
195 }
196 }
197 }
198
199 void DrawBuildingBuildButton(Level *level, int x, int y, int width, int height, uint8_t towerType, const char *name)
200 {
201 static ButtonState buttonStates[8] = {0};
202 int cost = GetTowerCosts(towerType);
203 const char *text = TextFormat("%s: %d", name, cost);
204 buttonStates[towerType].isSelected = level->placementMode == towerType;
205 buttonStates[towerType].isDisabled = level->playerGold < cost;
206 if (Button(text, x, y, width, height, &buttonStates[towerType]))
207 {
208 level->placementMode = buttonStates[towerType].isSelected ? 0 : towerType;
209 }
210 }
211
212 float GetRandomFloat(float min, float max)
213 {
214 int random = GetRandomValue(0, 0xfffffff);
215 return ((float)random / (float)0xfffffff) * (max - min) + min;
216 }
217
218 void DrawLevelGround(Level *level)
219 {
220 // draw checkerboard ground pattern
221 for (int x = -5; x <= 5; x += 1)
222 {
223 for (int y = -5; y <= 5; y += 1)
224 {
225 Model *model = (x + y) % 2 == 0 ? &floorTileAModel : &floorTileBModel;
226 DrawModel(*model, (Vector3){x, 0.0f, y}, 1.0f, WHITE);
227 }
228 }
229
230 int oldSeed = GetRandomValue(0, 0xfffffff);
231 SetRandomSeed(level->seed);
232 // increase probability for trees via duplicated entries
233 Model borderModels[64];
234 int maxRockCount = GetRandomValue(2, 6);
235 int maxTreeCount = GetRandomValue(10, 20);
236 int maxFirTreeCount = GetRandomValue(5, 10);
237 int maxLeafTreeCount = maxTreeCount - maxFirTreeCount;
238 int grassPatchCount = GetRandomValue(5, 30);
239
240 int modelCount = 0;
241 for (int i = 0; i < maxRockCount && modelCount < 63; i++)
242 {
243 borderModels[modelCount++] = rockModels[GetRandomValue(0, 5)];
244 }
245 for (int i = 0; i < maxLeafTreeCount && modelCount < 63; i++)
246 {
247 borderModels[modelCount++] = treeModel[GetRandomValue(0, 1)];
248 }
249 for (int i = 0; i < maxFirTreeCount && modelCount < 63; i++)
250 {
251 borderModels[modelCount++] = firTreeModel[GetRandomValue(0, 1)];
252 }
253 for (int i = 0; i < grassPatchCount && modelCount < 63; i++)
254 {
255 borderModels[modelCount++] = grassPatchModel[0];
256 }
257
258 // draw some objects around the border of the map
259 Vector3 up = {0, 1, 0};
260 // a pseudo random number generator to get the same result every time
261 const float wiggle = 0.75f;
262 const int layerCount = 3;
263 for (int layer = 0; layer < layerCount; layer++)
264 {
265 int layerPos = 6 + layer;
266 for (int x = -6 + layer; x <= 6 + layer; x += 1)
267 {
268 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)],
269 (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, -layerPos + GetRandomFloat(0.0f, wiggle)},
270 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
271 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)],
272 (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, layerPos + GetRandomFloat(0.0f, wiggle)},
273 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
274 }
275
276 for (int z = -5 + layer; z <= 5 + layer; z += 1)
277 {
278 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)],
279 (Vector3){-layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)},
280 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
281 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)],
282 (Vector3){layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)},
283 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
284 }
285 }
286
287 SetRandomSeed(oldSeed);
288 }
289
290 void DrawEnemyPath(Level *level)
291 {
292 const int castleX = 0, castleY = 0;
293 const int maxWaypointCount = 200;
294 const float timeStep = 1.0f;
295 Vector3 arrowScale = {0.75f, 0.75f, 0.75f};
296
297 // we start with a time offset to simulate the path,
298 // this way the arrows are animated in a forward moving direction
299 // The time is wrapped around the time step to get a smooth animation
300 float timeOffset = fmodf(GetTime(), timeStep);
301
302 for (int i = 0; i < ENEMY_MAX_WAVE_COUNT; i++)
303 {
304 EnemyWave *wave = &level->waves[i];
305 if (wave->wave != level->currentWave)
306 {
307 continue;
308 }
309
310 // use this dummy enemy to simulate the path
311 Enemy dummy = {
312 .enemyType = ENEMY_TYPE_MINION,
313 .simPosition = (Vector2){wave->spawnPosition.x, wave->spawnPosition.y},
314 .nextX = wave->spawnPosition.x,
315 .nextY = wave->spawnPosition.y,
316 .currentX = wave->spawnPosition.x,
317 .currentY = wave->spawnPosition.y,
318 };
319
320 float deltaTime = timeOffset;
321 for (int j = 0; j < maxWaypointCount; j++)
322 {
323 int waypointPassedCount = 0;
324 Vector2 pos = EnemyGetPosition(&dummy, deltaTime, &dummy.simVelocity, &waypointPassedCount);
325 // after the initial variable starting offset, we use a fixed time step
326 deltaTime = timeStep;
327 dummy.simPosition = pos;
328
329 // Update the dummy's position just like we do in the regular enemy update loop
330 for (int k = 0; k < waypointPassedCount; k++)
331 {
332 dummy.currentX = dummy.nextX;
333 dummy.currentY = dummy.nextY;
334 if (EnemyGetNextPosition(dummy.currentX, dummy.currentY, &dummy.nextX, &dummy.nextY) &&
335 Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
336 {
337 break;
338 }
339 }
340 if (Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
341 {
342 break;
343 }
344
345 // get the angle we need to rotate the arrow model. The velocity is just fine for this.
346 float angle = atan2f(dummy.simVelocity.x, dummy.simVelocity.y) * RAD2DEG - 90.0f;
347 DrawModelEx(pathArrowModel, (Vector3){pos.x, 0.15f, pos.y}, (Vector3){0, 1, 0}, angle, arrowScale, WHITE);
348 }
349 }
350 }
351
352 void DrawLevelBuildingState(Level *level)
353 {
354 BeginMode3D(level->camera);
355 DrawLevelGround(level);
356
357 int blockedCellCount = 0;
358 Vector2 blockedCells[1];
359 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera);
360 float planeDistance = ray.position.y / -ray.direction.y;
361 float planeX = ray.direction.x * planeDistance + ray.position.x;
362 float planeY = ray.direction.z * planeDistance + ray.position.z;
363 int16_t mapX = (int16_t)floorf(planeX + 0.5f);
364 int16_t mapY = (int16_t)floorf(planeY + 0.5f);
365 if (level->placementMode && !guiState.isBlocked && mapX >= -5 && mapX <= 5 && mapY >= -5 && mapY <= 5)
366 {
367 DrawCubeWires((Vector3){mapX, 0.2f, mapY}, 1.0f, 0.4f, 1.0f, RED);
368 blockedCells[blockedCellCount++] = (Vector2){mapX, mapY};
369 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
370 {
371 if (TowerTryAdd(level->placementMode, mapX, mapY))
372 {
373 level->playerGold -= GetTowerCosts(level->placementMode);
374 level->placementMode = TOWER_TYPE_NONE;
375 }
376 }
377 }
378
379 PathFindingMapUpdate(blockedCellCount, blockedCells);
380 DrawEnemyPath(level);
381 TowerDraw();
382 EnemyDraw();
383 ProjectileDraw();
384 ParticleDraw();
385
386 guiState.isBlocked = 0;
387
388 EndMode3D();
389
390 TowerDrawHealthBars(level->camera);
391
392 static ButtonState buildWallButtonState = {0};
393 static ButtonState buildGunButtonState = {0};
394 buildWallButtonState.isSelected = level->placementMode == TOWER_TYPE_WALL;
395 buildGunButtonState.isSelected = level->placementMode == TOWER_TYPE_ARCHER;
396
397 DrawBuildingBuildButton(level, 10, 10, 110, 30, TOWER_TYPE_WALL, "Wall");
398 DrawBuildingBuildButton(level, 10, 50, 110, 30, TOWER_TYPE_ARCHER, "Archer");
399 DrawBuildingBuildButton(level, 10, 90, 110, 30, TOWER_TYPE_BALLISTA, "Ballista");
400 DrawBuildingBuildButton(level, 10, 130, 110, 30, TOWER_TYPE_CATAPULT, "Catapult");
401
402 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
403 {
404 level->nextState = LEVEL_STATE_RESET;
405 }
406
407 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0))
408 {
409 level->nextState = LEVEL_STATE_BATTLE;
410 }
411
412 const char *text = "Building phase";
413 int textWidth = MeasureText(text, 20);
414 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
415 }
416
417 void InitBattleStateConditions(Level *level)
418 {
419 level->state = LEVEL_STATE_BATTLE;
420 level->nextState = LEVEL_STATE_NONE;
421 level->waveEndTimer = 0.0f;
422 for (int i = 0; i < 10; i++)
423 {
424 EnemyWave *wave = &level->waves[i];
425 wave->spawned = 0;
426 wave->timeToSpawnNext = wave->delay;
427 }
428 }
429
430 void DrawLevelBattleState(Level *level)
431 {
432 BeginMode3D(level->camera);
433 DrawLevelGround(level);
434 TowerDraw();
435 EnemyDraw();
436 ProjectileDraw();
437 ParticleDraw();
438 guiState.isBlocked = 0;
439 EndMode3D();
440
441 EnemyDrawHealthbars(level->camera);
442 TowerDrawHealthBars(level->camera);
443
444 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
445 {
446 level->nextState = LEVEL_STATE_RESET;
447 }
448
449 int maxCount = 0;
450 int remainingCount = 0;
451 for (int i = 0; i < 10; i++)
452 {
453 EnemyWave *wave = &level->waves[i];
454 if (wave->wave != level->currentWave)
455 {
456 continue;
457 }
458 maxCount += wave->count;
459 remainingCount += wave->count - wave->spawned;
460 }
461 int aliveCount = EnemyCount();
462 remainingCount += aliveCount;
463
464 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount);
465 int textWidth = MeasureText(text, 20);
466 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
467 }
468
469 void DrawLevel(Level *level)
470 {
471 switch (level->state)
472 {
473 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break;
474 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break;
475 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break;
476 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break;
477 default: break;
478 }
479
480 DrawLevelHud(level);
481 }
482
483 void UpdateLevel(Level *level)
484 {
485 if (level->state == LEVEL_STATE_BATTLE)
486 {
487 int activeWaves = 0;
488 for (int i = 0; i < 10; i++)
489 {
490 EnemyWave *wave = &level->waves[i];
491 if (wave->spawned >= wave->count || wave->wave != level->currentWave)
492 {
493 continue;
494 }
495 activeWaves++;
496 wave->timeToSpawnNext -= gameTime.deltaTime;
497 if (wave->timeToSpawnNext <= 0.0f)
498 {
499 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y);
500 if (enemy)
501 {
502 wave->timeToSpawnNext = wave->interval;
503 wave->spawned++;
504 }
505 }
506 }
507 if (GetTowerByType(TOWER_TYPE_BASE) == 0) {
508 level->waveEndTimer += gameTime.deltaTime;
509 if (level->waveEndTimer >= 2.0f)
510 {
511 level->nextState = LEVEL_STATE_LOST_WAVE;
512 }
513 }
514 else if (activeWaves == 0 && EnemyCount() == 0)
515 {
516 level->waveEndTimer += gameTime.deltaTime;
517 if (level->waveEndTimer >= 2.0f)
518 {
519 level->nextState = LEVEL_STATE_WON_WAVE;
520 }
521 }
522 }
523
524 PathFindingMapUpdate(0, 0);
525 EnemyUpdate();
526 TowerUpdate();
527 ProjectileUpdate();
528 ParticleUpdate();
529
530 if (level->nextState == LEVEL_STATE_RESET)
531 {
532 InitLevel(level);
533 }
534
535 if (level->nextState == LEVEL_STATE_BATTLE)
536 {
537 InitBattleStateConditions(level);
538 }
539
540 if (level->nextState == LEVEL_STATE_WON_WAVE)
541 {
542 level->currentWave++;
543 level->state = LEVEL_STATE_WON_WAVE;
544 }
545
546 if (level->nextState == LEVEL_STATE_LOST_WAVE)
547 {
548 level->state = LEVEL_STATE_LOST_WAVE;
549 }
550
551 if (level->nextState == LEVEL_STATE_BUILDING)
552 {
553 level->state = LEVEL_STATE_BUILDING;
554 }
555
556 if (level->nextState == LEVEL_STATE_WON_LEVEL)
557 {
558 // make something of this later
559 InitLevel(level);
560 }
561
562 level->nextState = LEVEL_STATE_NONE;
563 }
564
565 float nextSpawnTime = 0.0f;
566
567 void ResetGame()
568 {
569 InitLevel(currentLevel);
570 }
571
572 void InitGame()
573 {
574 TowerInit();
575 EnemyInit();
576 ProjectileInit();
577 ParticleInit();
578 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f);
579
580 currentLevel = levels;
581 InitLevel(currentLevel);
582 }
583
584 //# Immediate GUI functions
585
586 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth)
587 {
588 const float healthBarHeight = 6.0f;
589 const float healthBarOffset = 15.0f;
590 const float inset = 2.0f;
591 const float innerWidth = healthBarWidth - inset * 2;
592 const float innerHeight = healthBarHeight - inset * 2;
593
594 Vector2 screenPos = GetWorldToScreen(position, camera);
595 float centerX = screenPos.x - healthBarWidth * 0.5f;
596 float topY = screenPos.y - healthBarOffset;
597 DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK);
598 float healthWidth = innerWidth * healthRatio;
599 DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor);
600 }
601
602 int Button(const char *text, int x, int y, int width, int height, ButtonState *state)
603 {
604 Rectangle bounds = {x, y, width, height};
605 int isPressed = 0;
606 int isSelected = state && state->isSelected;
607 int isDisabled = state && state->isDisabled;
608 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !isDisabled)
609 {
610 Color color = isSelected ? DARKGRAY : GRAY;
611 DrawRectangle(x, y, width, height, color);
612 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
613 {
614 isPressed = 1;
615 }
616 guiState.isBlocked = 1;
617 }
618 else
619 {
620 Color color = isSelected ? WHITE : LIGHTGRAY;
621 DrawRectangle(x, y, width, height, color);
622 }
623 Font font = GetFontDefault();
624 Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1);
625 Color textColor = isDisabled ? GRAY : BLACK;
626 DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, textColor);
627 return isPressed;
628 }
629
630 //# Main game loop
631
632 void GameUpdate()
633 {
634 float dt = GetFrameTime();
635 // cap maximum delta time to 0.1 seconds to prevent large time steps
636 if (dt > 0.1f) dt = 0.1f;
637 gameTime.time += dt;
638 gameTime.deltaTime = dt;
639
640 UpdateLevel(currentLevel);
641 }
642
643 int main(void)
644 {
645 int screenWidth, screenHeight;
646 GetPreferredSize(&screenWidth, &screenHeight);
647 InitWindow(screenWidth, screenHeight, "Tower defense");
648 SetTargetFPS(30);
649
650 LoadAssets();
651 InitGame();
652
653 while (!WindowShouldClose())
654 {
655 if (IsPaused()) {
656 // canvas is not visible in browser - do nothing
657 continue;
658 }
659
660 BeginDrawing();
661 ClearBackground((Color){0x4E, 0x63, 0x26, 0xFF});
662
663 GameUpdate();
664 DrawLevel(currentLevel);
665
666 EndDrawing();
667 }
668
669 CloseWindow();
670
671 return 0;
672 }
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 #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 #define ENEMY_MAX_PATH_COUNT 8
12 #define ENEMY_MAX_COUNT 400
13 #define ENEMY_TYPE_NONE 0
14 #define ENEMY_TYPE_MINION 1
15
16 #define PARTICLE_MAX_COUNT 400
17 #define PARTICLE_TYPE_NONE 0
18 #define PARTICLE_TYPE_EXPLOSION 1
19
20 typedef struct Particle
21 {
22 uint8_t particleType;
23 float spawnTime;
24 float lifetime;
25 Vector3 position;
26 Vector3 velocity;
27 Vector3 scale;
28 } Particle;
29
30 #define TOWER_MAX_COUNT 400
31 enum TowerType
32 {
33 TOWER_TYPE_NONE,
34 TOWER_TYPE_BASE,
35 TOWER_TYPE_ARCHER,
36 TOWER_TYPE_BALLISTA,
37 TOWER_TYPE_CATAPULT,
38 TOWER_TYPE_WALL,
39 TOWER_TYPE_COUNT
40 };
41
42 typedef struct HitEffectConfig
43 {
44 float damage;
45 float areaDamageRadius;
46 float pushbackPowerDistance;
47 } HitEffectConfig;
48
49 typedef struct TowerTypeConfig
50 {
51 float cooldown;
52 float range;
53 float projectileSpeed;
54
55 uint8_t cost;
56 uint8_t projectileType;
57 uint16_t maxHealth;
58
59 HitEffectConfig hitEffect;
60 } TowerTypeConfig;
61
62 typedef struct Tower
63 {
64 int16_t x, y;
65 uint8_t towerType;
66 Vector2 lastTargetPosition;
67 float cooldown;
68 float damage;
69 } Tower;
70
71 typedef struct GameTime
72 {
73 float time;
74 float deltaTime;
75 } GameTime;
76
77 typedef struct ButtonState {
78 char isSelected;
79 char isDisabled;
80 } ButtonState;
81
82 typedef struct GUIState {
83 int isBlocked;
84 } GUIState;
85
86 typedef enum LevelState
87 {
88 LEVEL_STATE_NONE,
89 LEVEL_STATE_BUILDING,
90 LEVEL_STATE_BATTLE,
91 LEVEL_STATE_WON_WAVE,
92 LEVEL_STATE_LOST_WAVE,
93 LEVEL_STATE_WON_LEVEL,
94 LEVEL_STATE_RESET,
95 } LevelState;
96
97 typedef struct EnemyWave {
98 uint8_t enemyType;
99 uint8_t wave;
100 uint16_t count;
101 float interval;
102 float delay;
103 Vector2 spawnPosition;
104
105 uint16_t spawned;
106 float timeToSpawnNext;
107 } EnemyWave;
108
109 #define ENEMY_MAX_WAVE_COUNT 10
110
111 typedef struct Level
112 {
113 int seed;
114 LevelState state;
115 LevelState nextState;
116 Camera3D camera;
117 int placementMode;
118
119 int initialGold;
120 int playerGold;
121
122 EnemyWave waves[ENEMY_MAX_WAVE_COUNT];
123 int currentWave;
124 float waveEndTimer;
125 } Level;
126
127 typedef struct DeltaSrc
128 {
129 char x, y;
130 } DeltaSrc;
131
132 typedef struct PathfindingMap
133 {
134 int width, height;
135 float scale;
136 float *distances;
137 long *towerIndex;
138 DeltaSrc *deltaSrc;
139 float maxDistance;
140 Matrix toMapSpace;
141 Matrix toWorldSpace;
142 } PathfindingMap;
143
144 // when we execute the pathfinding algorithm, we need to store the active nodes
145 // in a queue. Each node has a position, a distance from the start, and the
146 // position of the node that we came from.
147 typedef struct PathfindingNode
148 {
149 int16_t x, y, fromX, fromY;
150 float distance;
151 } PathfindingNode;
152
153 typedef struct EnemyId
154 {
155 uint16_t index;
156 uint16_t generation;
157 } EnemyId;
158
159 typedef struct EnemyClassConfig
160 {
161 float speed;
162 float health;
163 float radius;
164 float maxAcceleration;
165 float requiredContactTime;
166 float explosionDamage;
167 float explosionRange;
168 float explosionPushbackPower;
169 int goldValue;
170 } EnemyClassConfig;
171
172 typedef struct Enemy
173 {
174 int16_t currentX, currentY;
175 int16_t nextX, nextY;
176 Vector2 simPosition;
177 Vector2 simVelocity;
178 uint16_t generation;
179 float walkedDistance;
180 float startMovingTime;
181 float damage, futureDamage;
182 float contactTime;
183 uint8_t enemyType;
184 uint8_t movePathCount;
185 Vector2 movePath[ENEMY_MAX_PATH_COUNT];
186 } Enemy;
187
188 // a unit that uses sprites to be drawn
189 #define SPRITE_UNIT_PHASE_WEAPON_IDLE 0
190 #define SPRITE_UNIT_PHASE_WEAPON_COOLDOWN 1
191 typedef struct SpriteUnit
192 {
193 Rectangle srcRect;
194 Vector2 offset;
195 int frameCount;
196 float frameDuration;
197 Rectangle srcWeaponIdleRect;
198 Vector2 srcWeaponIdleOffset;
199 Rectangle srcWeaponCooldownRect;
200 Vector2 srcWeaponCooldownOffset;
201 } SpriteUnit;
202
203 #define PROJECTILE_MAX_COUNT 1200
204 #define PROJECTILE_TYPE_NONE 0
205 #define PROJECTILE_TYPE_ARROW 1
206 #define PROJECTILE_TYPE_CATAPULT 2
207 #define PROJECTILE_TYPE_BALLISTA 3
208
209 typedef struct Projectile
210 {
211 uint8_t projectileType;
212 float shootTime;
213 float arrivalTime;
214 float distance;
215 Vector3 position;
216 Vector3 target;
217 Vector3 directionNormal;
218 EnemyId targetEnemy;
219 HitEffectConfig hitEffectConfig;
220 } Projectile;
221
222 //# Function declarations
223 float TowerGetMaxHealth(Tower *tower);
224 int Button(const char *text, int x, int y, int width, int height, ButtonState *state);
225 int EnemyAddDamageRange(Vector2 position, float range, float damage);
226 int EnemyAddDamage(Enemy *enemy, float damage);
227
228 //# Enemy functions
229 void EnemyInit();
230 void EnemyDraw();
231 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource);
232 void EnemyUpdate();
233 float EnemyGetCurrentMaxSpeed(Enemy *enemy);
234 float EnemyGetMaxHealth(Enemy *enemy);
235 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY);
236 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount);
237 EnemyId EnemyGetId(Enemy *enemy);
238 Enemy *EnemyTryResolve(EnemyId enemyId);
239 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY);
240 int EnemyAddDamage(Enemy *enemy, float damage);
241 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range);
242 int EnemyCount();
243 void EnemyDrawHealthbars(Camera3D camera);
244
245 //# Tower functions
246 void TowerInit();
247 Tower *TowerGetAt(int16_t x, int16_t y);
248 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y);
249 Tower *GetTowerByType(uint8_t towerType);
250 int GetTowerCosts(uint8_t towerType);
251 float TowerGetMaxHealth(Tower *tower);
252 void TowerDraw();
253 void TowerUpdate();
254 void TowerDrawHealthBars(Camera3D camera);
255 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase);
256
257 //# Particles
258 void ParticleInit();
259 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime);
260 void ParticleUpdate();
261 void ParticleDraw();
262
263 //# Projectiles
264 void ProjectileInit();
265 void ProjectileDraw();
266 void ProjectileUpdate();
267 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig);
268
269 //# Pathfinding map
270 void PathfindingMapInit(int width, int height, Vector3 translate, float scale);
271 float PathFindingGetDistance(int mapX, int mapY);
272 Vector2 PathFindingGetGradient(Vector3 world);
273 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY);
274 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells);
275 void PathFindingMapDraw();
276
277 //# UI
278 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth);
279
280 //# Level
281 void DrawLevelGround(Level *level);
282 void DrawEnemyPath(Level *level);
283
284 //# variables
285 extern Level *currentLevel;
286 extern Enemy enemies[ENEMY_MAX_COUNT];
287 extern int enemyCount;
288 extern EnemyClassConfig enemyClassConfigs[];
289
290 extern GUIState guiState;
291 extern GameTime gameTime;
292 extern Tower towers[TOWER_MAX_COUNT];
293 extern int towerCount;
294
295 extern Texture2D palette, spriteSheet;
296
297 #endif
1 #include "td_main.h"
2 #include <raymath.h>
3
4 static TowerTypeConfig towerTypeConfigs[TOWER_TYPE_COUNT] = {
5 [TOWER_TYPE_BASE] = {
6 .maxHealth = 10,
7 },
8 [TOWER_TYPE_ARCHER] = {
9 .cooldown = 0.5f,
10 .range = 3.0f,
11 .cost = 6,
12 .maxHealth = 10,
13 .projectileSpeed = 4.0f,
14 .projectileType = PROJECTILE_TYPE_ARROW,
15 .hitEffect = {
16 .damage = 3.0f,
17 }
18 },
19 [TOWER_TYPE_BALLISTA] = {
20 .cooldown = 1.5f,
21 .range = 6.0f,
22 .cost = 9,
23 .maxHealth = 10,
24 .projectileSpeed = 10.0f,
25 .projectileType = PROJECTILE_TYPE_BALLISTA,
26 .hitEffect = {
27 .damage = 6.0f,
28 .pushbackPowerDistance = 0.25f,
29 }
30 },
31 [TOWER_TYPE_CATAPULT] = {
32 .cooldown = 1.7f,
33 .range = 5.0f,
34 .cost = 10,
35 .maxHealth = 10,
36 .projectileSpeed = 3.0f,
37 .projectileType = PROJECTILE_TYPE_CATAPULT,
38 .hitEffect = {
39 .damage = 2.0f,
40 .areaDamageRadius = 1.75f,
41 }
42 },
43 [TOWER_TYPE_WALL] = {
44 .cost = 2,
45 .maxHealth = 10,
46 },
47 };
48
49 Tower towers[TOWER_MAX_COUNT];
50 int towerCount = 0;
51
52 Model towerModels[TOWER_TYPE_COUNT];
53
54 // definition of our archer unit
55 SpriteUnit archerUnit = {
56 .srcRect = {0, 0, 16, 16},
57 .offset = {7, 1},
58 .frameCount = 1,
59 .frameDuration = 0.0f,
60 .srcWeaponIdleRect = {16, 0, 6, 16},
61 .srcWeaponIdleOffset = {8, 0},
62 .srcWeaponCooldownRect = {22, 0, 11, 16},
63 .srcWeaponCooldownOffset = {10, 0},
64 };
65
66 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase)
67 {
68 float xScale = flip ? -1.0f : 1.0f;
69 Camera3D camera = currentLevel->camera;
70 float size = 0.5f;
71 Vector2 offset = (Vector2){ unit.offset.x / 16.0f * size, unit.offset.y / 16.0f * size * xScale };
72 Vector2 scale = (Vector2){ unit.srcRect.width / 16.0f * size, unit.srcRect.height / 16.0f * size };
73 // we want the sprite to face the camera, so we need to calculate the up vector
74 Vector3 forward = Vector3Subtract(camera.target, camera.position);
75 Vector3 up = {0, 1, 0};
76 Vector3 right = Vector3CrossProduct(forward, up);
77 up = Vector3Normalize(Vector3CrossProduct(right, forward));
78
79 Rectangle srcRect = unit.srcRect;
80 if (unit.frameCount > 1)
81 {
82 srcRect.x += (int)(t / unit.frameDuration) % unit.frameCount * srcRect.width;
83 }
84 if (flip)
85 {
86 srcRect.x += srcRect.width;
87 srcRect.width = -srcRect.width;
88 }
89 DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
90
91 if (phase == SPRITE_UNIT_PHASE_WEAPON_COOLDOWN && unit.srcWeaponCooldownRect.width > 0)
92 {
93 offset = (Vector2){ unit.srcWeaponCooldownOffset.x / 16.0f * size, unit.srcWeaponCooldownOffset.y / 16.0f * size };
94 scale = (Vector2){ unit.srcWeaponCooldownRect.width / 16.0f * size, unit.srcWeaponCooldownRect.height / 16.0f * size };
95 srcRect = unit.srcWeaponCooldownRect;
96 if (flip)
97 {
98 // position.x = flip * scale.x * 0.5f;
99 srcRect.x += srcRect.width;
100 srcRect.width = -srcRect.width;
101 offset.x = scale.x - offset.x;
102 }
103 DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
104 }
105 else if (phase == SPRITE_UNIT_PHASE_WEAPON_IDLE && unit.srcWeaponIdleRect.width > 0)
106 {
107 offset = (Vector2){ unit.srcWeaponIdleOffset.x / 16.0f * size, unit.srcWeaponIdleOffset.y / 16.0f * size };
108 scale = (Vector2){ unit.srcWeaponIdleRect.width / 16.0f * size, unit.srcWeaponIdleRect.height / 16.0f * size };
109 srcRect = unit.srcWeaponIdleRect;
110 if (flip)
111 {
112 // position.x = flip * scale.x * 0.5f;
113 srcRect.x += srcRect.width;
114 srcRect.width = -srcRect.width;
115 offset.x = scale.x - offset.x;
116 }
117 DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
118 }
119 }
120
121 void TowerInit()
122 {
123 for (int i = 0; i < TOWER_MAX_COUNT; i++)
124 {
125 towers[i] = (Tower){0};
126 }
127 towerCount = 0;
128
129 towerModels[TOWER_TYPE_BASE] = LoadModel("data/keep.glb");
130 towerModels[TOWER_TYPE_WALL] = LoadModel("data/wall-0000.glb");
131
132 for (int i = 0; i < TOWER_TYPE_COUNT; i++)
133 {
134 if (towerModels[i].materials)
135 {
136 // assign the palette texture to the material of the model (0 is not used afaik)
137 towerModels[i].materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
138 }
139 }
140 }
141
142 static void TowerGunUpdate(Tower *tower)
143 {
144 TowerTypeConfig config = towerTypeConfigs[tower->towerType];
145 if (tower->cooldown <= 0.0f)
146 {
147 Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, config.range);
148 if (enemy)
149 {
150 tower->cooldown = config.cooldown;
151 // shoot the enemy; determine future position of the enemy
152 float bulletSpeed = config.projectileSpeed;
153 Vector2 velocity = enemy->simVelocity;
154 Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0);
155 Vector2 towerPosition = {tower->x, tower->y};
156 float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed;
157 for (int i = 0; i < 8; i++) {
158 velocity = enemy->simVelocity;
159 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0);
160 float distance = Vector2Distance(towerPosition, futurePosition);
161 float eta2 = distance / bulletSpeed;
162 if (fabs(eta - eta2) < 0.01f) {
163 break;
164 }
165 eta = (eta2 + eta) * 0.5f;
166 }
167
168 ProjectileTryAdd(config.projectileType, enemy,
169 (Vector3){towerPosition.x, 1.33f, towerPosition.y},
170 (Vector3){futurePosition.x, 0.25f, futurePosition.y},
171 bulletSpeed, config.hitEffect);
172 enemy->futureDamage += config.hitEffect.damage;
173 tower->lastTargetPosition = futurePosition;
174 }
175 }
176 else
177 {
178 tower->cooldown -= gameTime.deltaTime;
179 }
180 }
181
182 Tower *TowerGetAt(int16_t x, int16_t y)
183 {
184 for (int i = 0; i < towerCount; i++)
185 {
186 if (towers[i].x == x && towers[i].y == y && towers[i].towerType != TOWER_TYPE_NONE)
187 {
188 return &towers[i];
189 }
190 }
191 return 0;
192 }
193
194 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y)
195 {
196 if (towerCount >= TOWER_MAX_COUNT)
197 {
198 return 0;
199 }
200
201 Tower *tower = TowerGetAt(x, y);
202 if (tower)
203 {
204 return 0;
205 }
206
207 tower = &towers[towerCount++];
208 tower->x = x;
209 tower->y = y;
210 tower->towerType = towerType;
211 tower->cooldown = 0.0f;
212 tower->damage = 0.0f;
213 return tower;
214 }
215
216 Tower *GetTowerByType(uint8_t towerType)
217 {
218 for (int i = 0; i < towerCount; i++)
219 {
220 if (towers[i].towerType == towerType)
221 {
222 return &towers[i];
223 }
224 }
225 return 0;
226 }
227
228 int GetTowerCosts(uint8_t towerType)
229 {
230 return towerTypeConfigs[towerType].cost;
231 }
232
233 float TowerGetMaxHealth(Tower *tower)
234 {
235 return towerTypeConfigs[tower->towerType].maxHealth;
236 }
237
238 void TowerDraw()
239 {
240 for (int i = 0; i < towerCount; i++)
241 {
242 Tower tower = towers[i];
243 if (tower.towerType == TOWER_TYPE_NONE)
244 {
245 continue;
246 }
247
248 switch (tower.towerType)
249 {
250 case TOWER_TYPE_ARCHER:
251 {
252 Vector2 screenPosTower = GetWorldToScreen((Vector3){tower.x, 0.0f, tower.y}, currentLevel->camera);
253 Vector2 screenPosTarget = GetWorldToScreen((Vector3){tower.lastTargetPosition.x, 0.0f, tower.lastTargetPosition.y}, currentLevel->camera);
254 DrawModel(towerModels[TOWER_TYPE_WALL], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
255 DrawSpriteUnit(archerUnit, (Vector3){tower.x, 1.0f, tower.y}, 0, screenPosTarget.x > screenPosTower.x,
256 tower.cooldown > 0.2f ? SPRITE_UNIT_PHASE_WEAPON_COOLDOWN : SPRITE_UNIT_PHASE_WEAPON_IDLE);
257 }
258 break;
259 case TOWER_TYPE_BALLISTA:
260 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, BROWN);
261 break;
262 case TOWER_TYPE_CATAPULT:
263 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, DARKGRAY);
264 break;
265 default:
266 if (towerModels[tower.towerType].materials)
267 {
268 DrawModel(towerModels[tower.towerType], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
269 } else {
270 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY);
271 }
272 break;
273 }
274 }
275 }
276
277 void TowerUpdate()
278 {
279 for (int i = 0; i < towerCount; i++)
280 {
281 Tower *tower = &towers[i];
282 switch (tower->towerType)
283 {
284 case TOWER_TYPE_CATAPULT:
285 case TOWER_TYPE_BALLISTA:
286 case TOWER_TYPE_ARCHER:
287 TowerGunUpdate(tower);
288 break;
289 }
290 }
291 }
292
293 void TowerDrawHealthBars(Camera3D camera)
294 {
295 for (int i = 0; i < towerCount; i++)
296 {
297 Tower *tower = &towers[i];
298 if (tower->towerType == TOWER_TYPE_NONE || tower->damage <= 0.0f)
299 {
300 continue;
301 }
302
303 Vector3 position = (Vector3){tower->x, 0.5f, tower->y};
304 float maxHealth = TowerGetMaxHealth(tower);
305 float health = maxHealth - tower->damage;
306 float healthRatio = health / maxHealth;
307
308 DrawHealthBar(camera, position, healthRatio, GREEN, 35.0f);
309 }
310 }
1 #include "td_main.h"
2 #include <raymath.h>
3 #include <stdlib.h>
4 #include <math.h>
5
6 EnemyClassConfig enemyClassConfigs[] = {
7 [ENEMY_TYPE_MINION] = {
8 .health = 10.0f,
9 .speed = 0.6f,
10 .radius = 0.25f,
11 .maxAcceleration = 1.0f,
12 .explosionDamage = 1.0f,
13 .requiredContactTime = 0.5f,
14 .explosionRange = 1.0f,
15 .explosionPushbackPower = 0.25f,
16 .goldValue = 1,
17 },
18 };
19
20 Enemy enemies[ENEMY_MAX_COUNT];
21 int enemyCount = 0;
22
23 SpriteUnit enemySprites[] = {
24 [ENEMY_TYPE_MINION] = {
25 .srcRect = {0, 16, 16, 16},
26 .offset = {8.0f, 0.0f},
27 .frameCount = 6,
28 .frameDuration = 0.1f,
29 },
30 };
31
32 void EnemyInit()
33 {
34 for (int i = 0; i < ENEMY_MAX_COUNT; i++)
35 {
36 enemies[i] = (Enemy){0};
37 }
38 enemyCount = 0;
39 }
40
41 float EnemyGetCurrentMaxSpeed(Enemy *enemy)
42 {
43 return enemyClassConfigs[enemy->enemyType].speed;
44 }
45
46 float EnemyGetMaxHealth(Enemy *enemy)
47 {
48 return enemyClassConfigs[enemy->enemyType].health;
49 }
50
51 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY)
52 {
53 int16_t castleX = 0;
54 int16_t castleY = 0;
55 int16_t dx = castleX - currentX;
56 int16_t dy = castleY - currentY;
57 if (dx == 0 && dy == 0)
58 {
59 *nextX = currentX;
60 *nextY = currentY;
61 return 1;
62 }
63 Vector2 gradient = PathFindingGetGradient((Vector3){currentX, 0, currentY});
64
65 if (gradient.x == 0 && gradient.y == 0)
66 {
67 *nextX = currentX;
68 *nextY = currentY;
69 return 1;
70 }
71
72 if (fabsf(gradient.x) > fabsf(gradient.y))
73 {
74 *nextX = currentX + (int16_t)(gradient.x > 0.0f ? 1 : -1);
75 *nextY = currentY;
76 return 0;
77 }
78 *nextX = currentX;
79 *nextY = currentY + (int16_t)(gradient.y > 0.0f ? 1 : -1);
80 return 0;
81 }
82
83
84 // this function predicts the movement of the unit for the next deltaT seconds
85 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount)
86 {
87 const float pointReachedDistance = 0.25f;
88 const float pointReachedDistance2 = pointReachedDistance * pointReachedDistance;
89 const float maxSimStepTime = 0.015625f;
90
91 float maxAcceleration = enemyClassConfigs[enemy->enemyType].maxAcceleration;
92 float maxSpeed = EnemyGetCurrentMaxSpeed(enemy);
93 int16_t nextX = enemy->nextX;
94 int16_t nextY = enemy->nextY;
95 Vector2 position = enemy->simPosition;
96 int passedCount = 0;
97 for (float t = 0.0f; t < deltaT; t += maxSimStepTime)
98 {
99 float stepTime = fminf(deltaT - t, maxSimStepTime);
100 Vector2 target = (Vector2){nextX, nextY};
101 float speed = Vector2Length(*velocity);
102 // draw the target position for debugging
103 //DrawCubeWires((Vector3){target.x, 0.2f, target.y}, 0.1f, 0.4f, 0.1f, RED);
104 Vector2 lookForwardPos = Vector2Add(position, Vector2Scale(*velocity, speed));
105 if (Vector2DistanceSqr(target, lookForwardPos) <= pointReachedDistance2)
106 {
107 // we reached the target position, let's move to the next waypoint
108 EnemyGetNextPosition(nextX, nextY, &nextX, &nextY);
109 target = (Vector2){nextX, nextY};
110 // track how many waypoints we passed
111 passedCount++;
112 }
113
114 // acceleration towards the target
115 Vector2 unitDirection = Vector2Normalize(Vector2Subtract(target, lookForwardPos));
116 Vector2 acceleration = Vector2Scale(unitDirection, maxAcceleration * stepTime);
117 *velocity = Vector2Add(*velocity, acceleration);
118
119 // limit the speed to the maximum speed
120 if (speed > maxSpeed)
121 {
122 *velocity = Vector2Scale(*velocity, maxSpeed / speed);
123 }
124
125 // move the enemy
126 position = Vector2Add(position, Vector2Scale(*velocity, stepTime));
127 }
128
129 if (waypointPassedCount)
130 {
131 (*waypointPassedCount) = passedCount;
132 }
133
134 return position;
135 }
136
137 void EnemyDraw()
138 {
139 for (int i = 0; i < enemyCount; i++)
140 {
141 Enemy enemy = enemies[i];
142 if (enemy.enemyType == ENEMY_TYPE_NONE)
143 {
144 continue;
145 }
146
147 Vector2 position = EnemyGetPosition(&enemy, gameTime.time - enemy.startMovingTime, &enemy.simVelocity, 0);
148
149 // don't draw any trails for now; might replace this with footprints later
150 // if (enemy.movePathCount > 0)
151 // {
152 // Vector3 p = {enemy.movePath[0].x, 0.2f, enemy.movePath[0].y};
153 // DrawLine3D(p, (Vector3){position.x, 0.2f, position.y}, GREEN);
154 // }
155 // for (int j = 1; j < enemy.movePathCount; j++)
156 // {
157 // Vector3 p = {enemy.movePath[j - 1].x, 0.2f, enemy.movePath[j - 1].y};
158 // Vector3 q = {enemy.movePath[j].x, 0.2f, enemy.movePath[j].y};
159 // DrawLine3D(p, q, GREEN);
160 // }
161
162 switch (enemy.enemyType)
163 {
164 case ENEMY_TYPE_MINION:
165 DrawSpriteUnit(enemySprites[ENEMY_TYPE_MINION], (Vector3){position.x, 0.0f, position.y},
166 enemy.walkedDistance, 0, 0);
167 break;
168 }
169 }
170 }
171
172 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource)
173 {
174 // damage the tower
175 float explosionDamge = enemyClassConfigs[enemy->enemyType].explosionDamage;
176 float explosionRange = enemyClassConfigs[enemy->enemyType].explosionRange;
177 float explosionPushbackPower = enemyClassConfigs[enemy->enemyType].explosionPushbackPower;
178 float explosionRange2 = explosionRange * explosionRange;
179 tower->damage += enemyClassConfigs[enemy->enemyType].explosionDamage;
180 // explode the enemy
181 if (tower->damage >= TowerGetMaxHealth(tower))
182 {
183 tower->towerType = TOWER_TYPE_NONE;
184 }
185
186 ParticleAdd(PARTICLE_TYPE_EXPLOSION,
187 explosionSource,
188 (Vector3){0, 0.1f, 0}, (Vector3){1.0f, 1.0f, 1.0f}, 1.0f);
189
190 enemy->enemyType = ENEMY_TYPE_NONE;
191
192 // push back enemies & dealing damage
193 for (int i = 0; i < enemyCount; i++)
194 {
195 Enemy *other = &enemies[i];
196 if (other->enemyType == ENEMY_TYPE_NONE)
197 {
198 continue;
199 }
200 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, other->simPosition);
201 if (distanceSqr > 0 && distanceSqr < explosionRange2)
202 {
203 Vector2 direction = Vector2Normalize(Vector2Subtract(other->simPosition, enemy->simPosition));
204 other->simPosition = Vector2Add(other->simPosition, Vector2Scale(direction, explosionPushbackPower));
205 EnemyAddDamage(other, explosionDamge);
206 }
207 }
208 }
209
210 void EnemyUpdate()
211 {
212 const float castleX = 0;
213 const float castleY = 0;
214 const float maxPathDistance2 = 0.25f * 0.25f;
215
216 for (int i = 0; i < enemyCount; i++)
217 {
218 Enemy *enemy = &enemies[i];
219 if (enemy->enemyType == ENEMY_TYPE_NONE)
220 {
221 continue;
222 }
223
224 int waypointPassedCount = 0;
225 Vector2 prevPosition = enemy->simPosition;
226 enemy->simPosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &enemy->simVelocity, &waypointPassedCount);
227 enemy->startMovingTime = gameTime.time;
228 enemy->walkedDistance += Vector2Distance(prevPosition, enemy->simPosition);
229 // track path of unit
230 if (enemy->movePathCount == 0 || Vector2DistanceSqr(enemy->simPosition, enemy->movePath[0]) > maxPathDistance2)
231 {
232 for (int j = ENEMY_MAX_PATH_COUNT - 1; j > 0; j--)
233 {
234 enemy->movePath[j] = enemy->movePath[j - 1];
235 }
236 enemy->movePath[0] = enemy->simPosition;
237 if (++enemy->movePathCount > ENEMY_MAX_PATH_COUNT)
238 {
239 enemy->movePathCount = ENEMY_MAX_PATH_COUNT;
240 }
241 }
242
243 if (waypointPassedCount > 0)
244 {
245 enemy->currentX = enemy->nextX;
246 enemy->currentY = enemy->nextY;
247 if (EnemyGetNextPosition(enemy->currentX, enemy->currentY, &enemy->nextX, &enemy->nextY) &&
248 Vector2DistanceSqr(enemy->simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
249 {
250 // enemy reached the castle; remove it
251 enemy->enemyType = ENEMY_TYPE_NONE;
252 continue;
253 }
254 }
255 }
256
257 // handle collisions between enemies
258 for (int i = 0; i < enemyCount - 1; i++)
259 {
260 Enemy *enemyA = &enemies[i];
261 if (enemyA->enemyType == ENEMY_TYPE_NONE)
262 {
263 continue;
264 }
265 for (int j = i + 1; j < enemyCount; j++)
266 {
267 Enemy *enemyB = &enemies[j];
268 if (enemyB->enemyType == ENEMY_TYPE_NONE)
269 {
270 continue;
271 }
272 float distanceSqr = Vector2DistanceSqr(enemyA->simPosition, enemyB->simPosition);
273 float radiusA = enemyClassConfigs[enemyA->enemyType].radius;
274 float radiusB = enemyClassConfigs[enemyB->enemyType].radius;
275 float radiusSum = radiusA + radiusB;
276 if (distanceSqr < radiusSum * radiusSum && distanceSqr > 0.001f)
277 {
278 // collision
279 float distance = sqrtf(distanceSqr);
280 float overlap = radiusSum - distance;
281 // move the enemies apart, but softly; if we have a clog of enemies,
282 // moving them perfectly apart can cause them to jitter
283 float positionCorrection = overlap / 5.0f;
284 Vector2 direction = (Vector2){
285 (enemyB->simPosition.x - enemyA->simPosition.x) / distance * positionCorrection,
286 (enemyB->simPosition.y - enemyA->simPosition.y) / distance * positionCorrection};
287 enemyA->simPosition = Vector2Subtract(enemyA->simPosition, direction);
288 enemyB->simPosition = Vector2Add(enemyB->simPosition, direction);
289 }
290 }
291 }
292
293 // handle collisions between enemies and towers
294 for (int i = 0; i < enemyCount; i++)
295 {
296 Enemy *enemy = &enemies[i];
297 if (enemy->enemyType == ENEMY_TYPE_NONE)
298 {
299 continue;
300 }
301 enemy->contactTime -= gameTime.deltaTime;
302 if (enemy->contactTime < 0.0f)
303 {
304 enemy->contactTime = 0.0f;
305 }
306
307 float enemyRadius = enemyClassConfigs[enemy->enemyType].radius;
308 // linear search over towers; could be optimized by using path finding tower map,
309 // but for now, we keep it simple
310 for (int j = 0; j < towerCount; j++)
311 {
312 Tower *tower = &towers[j];
313 if (tower->towerType == TOWER_TYPE_NONE)
314 {
315 continue;
316 }
317 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){tower->x, tower->y});
318 float combinedRadius = enemyRadius + 0.708; // sqrt(0.5^2 + 0.5^2), corner-center distance of square with side length 1
319 if (distanceSqr > combinedRadius * combinedRadius)
320 {
321 continue;
322 }
323 // potential collision; square / circle intersection
324 float dx = tower->x - enemy->simPosition.x;
325 float dy = tower->y - enemy->simPosition.y;
326 float absDx = fabsf(dx);
327 float absDy = fabsf(dy);
328 Vector3 contactPoint = {0};
329 if (absDx <= 0.5f && absDx <= absDy) {
330 // vertical collision; push the enemy out horizontally
331 float overlap = enemyRadius + 0.5f - absDy;
332 if (overlap < 0.0f)
333 {
334 continue;
335 }
336 float direction = dy > 0.0f ? -1.0f : 1.0f;
337 enemy->simPosition.y += direction * overlap;
338 contactPoint = (Vector3){enemy->simPosition.x, 0.2f, tower->y + direction * 0.5f};
339 }
340 else if (absDy <= 0.5f && absDy <= absDx)
341 {
342 // horizontal collision; push the enemy out vertically
343 float overlap = enemyRadius + 0.5f - absDx;
344 if (overlap < 0.0f)
345 {
346 continue;
347 }
348 float direction = dx > 0.0f ? -1.0f : 1.0f;
349 enemy->simPosition.x += direction * overlap;
350 contactPoint = (Vector3){tower->x + direction * 0.5f, 0.2f, enemy->simPosition.y};
351 }
352 else
353 {
354 // possible collision with a corner
355 float cornerDX = dx > 0.0f ? -0.5f : 0.5f;
356 float cornerDY = dy > 0.0f ? -0.5f : 0.5f;
357 float cornerX = tower->x + cornerDX;
358 float cornerY = tower->y + cornerDY;
359 float cornerDistanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){cornerX, cornerY});
360 if (cornerDistanceSqr > enemyRadius * enemyRadius)
361 {
362 continue;
363 }
364 // push the enemy out along the diagonal
365 float cornerDistance = sqrtf(cornerDistanceSqr);
366 float overlap = enemyRadius - cornerDistance;
367 float directionX = cornerDistance > 0.0f ? (cornerX - enemy->simPosition.x) / cornerDistance : -cornerDX;
368 float directionY = cornerDistance > 0.0f ? (cornerY - enemy->simPosition.y) / cornerDistance : -cornerDY;
369 enemy->simPosition.x -= directionX * overlap;
370 enemy->simPosition.y -= directionY * overlap;
371 contactPoint = (Vector3){cornerX, 0.2f, cornerY};
372 }
373
374 if (enemyClassConfigs[enemy->enemyType].explosionDamage > 0.0f)
375 {
376 enemy->contactTime += gameTime.deltaTime * 2.0f; // * 2 to undo the subtraction above
377 if (enemy->contactTime >= enemyClassConfigs[enemy->enemyType].requiredContactTime)
378 {
379 EnemyTriggerExplode(enemy, tower, contactPoint);
380 }
381 }
382 }
383 }
384 }
385
386 EnemyId EnemyGetId(Enemy *enemy)
387 {
388 return (EnemyId){enemy - enemies, enemy->generation};
389 }
390
391 Enemy *EnemyTryResolve(EnemyId enemyId)
392 {
393 if (enemyId.index >= ENEMY_MAX_COUNT)
394 {
395 return 0;
396 }
397 Enemy *enemy = &enemies[enemyId.index];
398 if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE)
399 {
400 return 0;
401 }
402 return enemy;
403 }
404
405 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY)
406 {
407 Enemy *spawn = 0;
408 for (int i = 0; i < enemyCount; i++)
409 {
410 Enemy *enemy = &enemies[i];
411 if (enemy->enemyType == ENEMY_TYPE_NONE)
412 {
413 spawn = enemy;
414 break;
415 }
416 }
417
418 if (enemyCount < ENEMY_MAX_COUNT && !spawn)
419 {
420 spawn = &enemies[enemyCount++];
421 }
422
423 if (spawn)
424 {
425 spawn->currentX = currentX;
426 spawn->currentY = currentY;
427 spawn->nextX = currentX;
428 spawn->nextY = currentY;
429 spawn->simPosition = (Vector2){currentX, currentY};
430 spawn->simVelocity = (Vector2){0, 0};
431 spawn->enemyType = enemyType;
432 spawn->startMovingTime = gameTime.time;
433 spawn->damage = 0.0f;
434 spawn->futureDamage = 0.0f;
435 spawn->generation++;
436 spawn->movePathCount = 0;
437 spawn->walkedDistance = 0.0f;
438 }
439
440 return spawn;
441 }
442
443 int EnemyAddDamageRange(Vector2 position, float range, float damage)
444 {
445 int count = 0;
446 float range2 = range * range;
447 for (int i = 0; i < enemyCount; i++)
448 {
449 Enemy *enemy = &enemies[i];
450 if (enemy->enemyType == ENEMY_TYPE_NONE)
451 {
452 continue;
453 }
454 float distance2 = Vector2DistanceSqr(position, enemy->simPosition);
455 if (distance2 <= range2)
456 {
457 EnemyAddDamage(enemy, damage);
458 count++;
459 }
460 }
461 return count;
462 }
463
464 int EnemyAddDamage(Enemy *enemy, float damage)
465 {
466 enemy->damage += damage;
467 if (enemy->damage >= EnemyGetMaxHealth(enemy))
468 {
469 currentLevel->playerGold += enemyClassConfigs[enemy->enemyType].goldValue;
470 enemy->enemyType = ENEMY_TYPE_NONE;
471 return 1;
472 }
473
474 return 0;
475 }
476
477 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
478 {
479 int16_t castleX = 0;
480 int16_t castleY = 0;
481 Enemy* closest = 0;
482 int16_t closestDistance = 0;
483 float range2 = range * range;
484 for (int i = 0; i < enemyCount; i++)
485 {
486 Enemy* enemy = &enemies[i];
487 if (enemy->enemyType == ENEMY_TYPE_NONE)
488 {
489 continue;
490 }
491 float maxHealth = EnemyGetMaxHealth(enemy);
492 if (enemy->futureDamage >= maxHealth)
493 {
494 // ignore enemies that will die soon
495 continue;
496 }
497 int16_t dx = castleX - enemy->currentX;
498 int16_t dy = castleY - enemy->currentY;
499 int16_t distance = abs(dx) + abs(dy);
500 if (!closest || distance < closestDistance)
501 {
502 float tdx = towerX - enemy->currentX;
503 float tdy = towerY - enemy->currentY;
504 float tdistance2 = tdx * tdx + tdy * tdy;
505 if (tdistance2 <= range2)
506 {
507 closest = enemy;
508 closestDistance = distance;
509 }
510 }
511 }
512 return closest;
513 }
514
515 int EnemyCount()
516 {
517 int count = 0;
518 for (int i = 0; i < enemyCount; i++)
519 {
520 if (enemies[i].enemyType != ENEMY_TYPE_NONE)
521 {
522 count++;
523 }
524 }
525 return count;
526 }
527
528 void EnemyDrawHealthbars(Camera3D camera)
529 {
530 for (int i = 0; i < enemyCount; i++)
531 {
532 Enemy *enemy = &enemies[i];
533 if (enemy->enemyType == ENEMY_TYPE_NONE || enemy->damage == 0.0f)
534 {
535 continue;
536 }
537 Vector3 position = (Vector3){enemy->simPosition.x, 0.5f, enemy->simPosition.y};
538 float maxHealth = EnemyGetMaxHealth(enemy);
539 float health = maxHealth - enemy->damage;
540 float healthRatio = health / maxHealth;
541
542 DrawHealthBar(camera, position, healthRatio, GREEN, 15.0f);
543 }
544 }
1 #include "td_main.h"
2 #include <raymath.h>
3
4 static Projectile projectiles[PROJECTILE_MAX_COUNT];
5 static int projectileCount = 0;
6
7 typedef struct ProjectileConfig
8 {
9 float arcFactor;
10 Color color;
11 Color trailColor;
12 } ProjectileConfig;
13
14 ProjectileConfig projectileConfigs[] = {
15 [PROJECTILE_TYPE_ARROW] = {
16 .arcFactor = 0.15f,
17 .color = RED,
18 .trailColor = BROWN,
19 },
20 [PROJECTILE_TYPE_CATAPULT] = {
21 .arcFactor = 0.5f,
22 .color = RED,
23 .trailColor = GRAY,
24 },
25 [PROJECTILE_TYPE_BALLISTA] = {
26 .arcFactor = 0.025f,
27 .color = RED,
28 .trailColor = BROWN,
29 },
30 };
31
32 void ProjectileInit()
33 {
34 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
35 {
36 projectiles[i] = (Projectile){0};
37 }
38 }
39
40 void ProjectileDraw()
41 {
42 for (int i = 0; i < projectileCount; i++)
43 {
44 Projectile projectile = projectiles[i];
45 if (projectile.projectileType == PROJECTILE_TYPE_NONE)
46 {
47 continue;
48 }
49 float transition = (gameTime.time - projectile.shootTime) / (projectile.arrivalTime - projectile.shootTime);
50 if (transition >= 1.0f)
51 {
52 continue;
53 }
54
55 ProjectileConfig config = projectileConfigs[projectile.projectileType];
56 for (float transitionOffset = 0.0f; transitionOffset < 1.0f; transitionOffset += 0.1f)
57 {
58 float t = transition + transitionOffset * 0.3f;
59 if (t > 1.0f)
60 {
61 break;
62 }
63 Vector3 position = Vector3Lerp(projectile.position, projectile.target, t);
64 Color color = config.color;
65 color = ColorLerp(config.trailColor, config.color, transitionOffset * transitionOffset);
66 // fake a ballista flight path using parabola equation
67 float parabolaT = t - 0.5f;
68 parabolaT = 1.0f - 4.0f * parabolaT * parabolaT;
69 position.y += config.arcFactor * parabolaT * projectile.distance;
70
71 float size = 0.06f * (transitionOffset + 0.25f);
72 DrawCube(position, size, size, size, color);
73 }
74 }
75 }
76
77 void ProjectileUpdate()
78 {
79 for (int i = 0; i < projectileCount; i++)
80 {
81 Projectile *projectile = &projectiles[i];
82 if (projectile->projectileType == PROJECTILE_TYPE_NONE)
83 {
84 continue;
85 }
86 float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime);
87 if (transition >= 1.0f)
88 {
89 projectile->projectileType = PROJECTILE_TYPE_NONE;
90 Enemy *enemy = EnemyTryResolve(projectile->targetEnemy);
91 if (enemy && projectile->hitEffectConfig.pushbackPowerDistance > 0.0f)
92 {
93 Vector2 direction = Vector2Normalize(Vector2Subtract((Vector2){projectile->target.x, projectile->target.z}, enemy->simPosition));
94 enemy->simPosition = Vector2Add(enemy->simPosition, Vector2Scale(direction, projectile->hitEffectConfig.pushbackPowerDistance));
95 }
96
97 if (projectile->hitEffectConfig.areaDamageRadius > 0.0f)
98 {
99 EnemyAddDamageRange((Vector2){projectile->target.x, projectile->target.z}, projectile->hitEffectConfig.areaDamageRadius, projectile->hitEffectConfig.damage);
100 // pancaked sphere explosion
101 float r = projectile->hitEffectConfig.areaDamageRadius;
102 ParticleAdd(PARTICLE_TYPE_EXPLOSION, projectile->target, (Vector3){0}, (Vector3){r, r * 0.2f, r}, 0.33f);
103 }
104 else if (projectile->hitEffectConfig.damage > 0.0f && enemy)
105 {
106 EnemyAddDamage(enemy, projectile->hitEffectConfig.damage);
107 }
108 continue;
109 }
110 }
111 }
112
113 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig)
114 {
115 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
116 {
117 Projectile *projectile = &projectiles[i];
118 if (projectile->projectileType == PROJECTILE_TYPE_NONE)
119 {
120 projectile->projectileType = projectileType;
121 projectile->shootTime = gameTime.time;
122 float distance = Vector3Distance(position, target);
123 projectile->arrivalTime = gameTime.time + distance / speed;
124 projectile->position = position;
125 projectile->target = target;
126 projectile->directionNormal = Vector3Scale(Vector3Subtract(target, position), 1.0f / distance);
127 projectile->distance = distance;
128 projectile->targetEnemy = EnemyGetId(enemy);
129 projectileCount = projectileCount <= i ? i + 1 : projectileCount;
130 projectile->hitEffectConfig = hitEffectConfig;
131 return projectile;
132 }
133 }
134 return 0;
135 }
1 #include "td_main.h"
2 #include <raymath.h>
3 #include <rlgl.h>
4
5 static Particle particles[PARTICLE_MAX_COUNT];
6 static int particleCount = 0;
7
8 void ParticleInit()
9 {
10 for (int i = 0; i < PARTICLE_MAX_COUNT; i++)
11 {
12 particles[i] = (Particle){0};
13 }
14 particleCount = 0;
15 }
16
17 static void DrawExplosionParticle(Particle *particle, float transition)
18 {
19 Vector3 scale = particle->scale;
20 float size = 1.0f * (1.0f - transition);
21 Color startColor = WHITE;
22 Color endColor = RED;
23 Color color = ColorLerp(startColor, endColor, transition);
24
25 rlPushMatrix();
26 rlTranslatef(particle->position.x, particle->position.y, particle->position.z);
27 rlScalef(scale.x, scale.y, scale.z);
28 DrawSphere(Vector3Zero(), size, color);
29 rlPopMatrix();
30 }
31
32 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime)
33 {
34 if (particleCount >= PARTICLE_MAX_COUNT)
35 {
36 return;
37 }
38
39 int index = -1;
40 for (int i = 0; i < particleCount; i++)
41 {
42 if (particles[i].particleType == PARTICLE_TYPE_NONE)
43 {
44 index = i;
45 break;
46 }
47 }
48
49 if (index == -1)
50 {
51 index = particleCount++;
52 }
53
54 Particle *particle = &particles[index];
55 particle->particleType = particleType;
56 particle->spawnTime = gameTime.time;
57 particle->lifetime = lifetime;
58 particle->position = position;
59 particle->velocity = velocity;
60 particle->scale = scale;
61 }
62
63 void ParticleUpdate()
64 {
65 for (int i = 0; i < particleCount; i++)
66 {
67 Particle *particle = &particles[i];
68 if (particle->particleType == PARTICLE_TYPE_NONE)
69 {
70 continue;
71 }
72
73 float age = gameTime.time - particle->spawnTime;
74
75 if (particle->lifetime > age)
76 {
77 particle->position = Vector3Add(particle->position, Vector3Scale(particle->velocity, gameTime.deltaTime));
78 }
79 else {
80 particle->particleType = PARTICLE_TYPE_NONE;
81 }
82 }
83 }
84
85 void ParticleDraw()
86 {
87 for (int i = 0; i < particleCount; i++)
88 {
89 Particle particle = particles[i];
90 if (particle.particleType == PARTICLE_TYPE_NONE)
91 {
92 continue;
93 }
94
95 float age = gameTime.time - particle.spawnTime;
96 float transition = age / particle.lifetime;
97 switch (particle.particleType)
98 {
99 case PARTICLE_TYPE_EXPLOSION:
100 DrawExplosionParticle(&particle, transition);
101 break;
102 default:
103 DrawCube(particle.position, 0.3f, 0.5f, 0.3f, RED);
104 break;
105 }
106 }
107 }
1 #include "raylib.h"
2 #include "preferred_size.h"
3
4 // Since the canvas size is not known at compile time, we need to query it at runtime;
5 // the following platform specific code obtains the canvas size and we will use this
6 // size as the preferred size for the window at init time. We're ignoring here the
7 // possibility of the canvas size changing during runtime - this would require to
8 // poll the canvas size in the game loop or establishing a callback to be notified
9
10 #ifdef PLATFORM_WEB
11 #include <emscripten.h>
12 EMSCRIPTEN_RESULT emscripten_get_element_css_size(const char *target, double *width, double *height);
13
14 void GetPreferredSize(int *screenWidth, int *screenHeight)
15 {
16 double canvasWidth, canvasHeight;
17 emscripten_get_element_css_size("#" CANVAS_NAME, &canvasWidth, &canvasHeight);
18 *screenWidth = (int)canvasWidth;
19 *screenHeight = (int)canvasHeight;
20 TraceLog(LOG_INFO, "preferred size for %s: %d %d", CANVAS_NAME, *screenWidth, *screenHeight);
21 }
22
23 int IsPaused()
24 {
25 const char *js = "(function(){\n"
26 " var canvas = document.getElementById(\"" CANVAS_NAME "\");\n"
27 " var rect = canvas.getBoundingClientRect();\n"
28 " var isVisible = (\n"
29 " rect.top >= 0 &&\n"
30 " rect.left >= 0 &&\n"
31 " rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&\n"
32 " rect.right <= (window.innerWidth || document.documentElement.clientWidth)\n"
33 " );\n"
34 " return isVisible ? 0 : 1;\n"
35 "})()";
36 return emscripten_run_script_int(js);
37 }
38
39 #else
40 void GetPreferredSize(int *screenWidth, int *screenHeight)
41 {
42 *screenWidth = 600;
43 *screenHeight = 240;
44 }
45 int IsPaused()
46 {
47 return 0;
48 }
49 #endif
1 #ifndef PREFERRED_SIZE_H
2 #define PREFERRED_SIZE_H
3
4 void GetPreferredSize(int *screenWidth, int *screenHeight);
5 int IsPaused();
6
7 #endif
I find this very satisfying to watch. It's quite simple, yet very effective. The animation is done by changing the start time offset for the dummy enemy that is used to preview the path.
One annoying thing now is, that the path preview can be blocked by the towers. A popular solution to this is to render the path preview after the towers are rendered and then drawing it twice: Once with depth testing enabled and once with depth testing disabled but where the coloring is a bit darker. Let's try this out:
1 #include "td_main.h"
2 #include <raymath.h>
3 #include <rlgl.h>
4 #include <stdlib.h>
5 #include <math.h>
6
7 //# Variables
8 GUIState guiState = {0};
9 GameTime gameTime = {0};
10
11 Model floorTileAModel = {0};
12 Model floorTileBModel = {0};
13 Model treeModel[2] = {0};
14 Model firTreeModel[2] = {0};
15 Model rockModels[5] = {0};
16 Model grassPatchModel[1] = {0};
17
18 Model pathArrowModel = {0};
19
20 Texture2D palette, spriteSheet;
21
22 Level levels[] = {
23 [0] = {
24 .state = LEVEL_STATE_BUILDING,
25 .initialGold = 20,
26 .waves[0] = {
27 .enemyType = ENEMY_TYPE_MINION,
28 .wave = 0,
29 .count = 10,
30 .interval = 2.5f,
31 .delay = 1.0f,
32 .spawnPosition = {0, 6},
33 },
34 .waves[1] = {
35 .enemyType = ENEMY_TYPE_MINION,
36 .wave = 1,
37 .count = 20,
38 .interval = 1.5f,
39 .delay = 1.0f,
40 .spawnPosition = {0, 6},
41 },
42 .waves[2] = {
43 .enemyType = ENEMY_TYPE_MINION,
44 .wave = 2,
45 .count = 30,
46 .interval = 1.2f,
47 .delay = 1.0f,
48 .spawnPosition = {0, 6},
49 }
50 },
51 };
52
53 Level *currentLevel = levels;
54
55 //# Game
56
57 static Model LoadGLBModel(char *filename)
58 {
59 Model model = LoadModel(TextFormat("data/%s.glb",filename));
60 for (int i = 0; i < model.materialCount; i++)
61 {
62 model.materials[i].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
63 }
64 return model;
65 }
66
67 void LoadAssets()
68 {
69 // load a sprite sheet that contains all units
70 spriteSheet = LoadTexture("data/spritesheet.png");
71 SetTextureFilter(spriteSheet, TEXTURE_FILTER_BILINEAR);
72
73 // we'll use a palette texture to colorize the all buildings and environment art
74 palette = LoadTexture("data/palette.png");
75 // The texture uses gradients on very small space, so we'll enable bilinear filtering
76 SetTextureFilter(palette, TEXTURE_FILTER_BILINEAR);
77
78 floorTileAModel = LoadGLBModel("floor-tile-a");
79 floorTileBModel = LoadGLBModel("floor-tile-b");
80 treeModel[0] = LoadGLBModel("leaftree-large-1-a");
81 treeModel[1] = LoadGLBModel("leaftree-large-1-b");
82 firTreeModel[0] = LoadGLBModel("firtree-1-a");
83 firTreeModel[1] = LoadGLBModel("firtree-1-b");
84 rockModels[0] = LoadGLBModel("rock-1");
85 rockModels[1] = LoadGLBModel("rock-2");
86 rockModels[2] = LoadGLBModel("rock-3");
87 rockModels[3] = LoadGLBModel("rock-4");
88 rockModels[4] = LoadGLBModel("rock-5");
89 grassPatchModel[0] = LoadGLBModel("grass-patch-1");
90
91 pathArrowModel = LoadGLBModel("direction-arrow-x");
92 }
93
94 void InitLevel(Level *level)
95 {
96 level->seed = (int)(GetTime() * 100.0f);
97
98 TowerInit();
99 EnemyInit();
100 ProjectileInit();
101 ParticleInit();
102 TowerTryAdd(TOWER_TYPE_BASE, 0, 0);
103
104 level->placementMode = 0;
105 level->state = LEVEL_STATE_BUILDING;
106 level->nextState = LEVEL_STATE_NONE;
107 level->playerGold = level->initialGold;
108 level->currentWave = 0;
109
110 Camera *camera = &level->camera;
111 camera->position = (Vector3){4.0f, 8.0f, 8.0f};
112 camera->target = (Vector3){0.0f, 0.0f, 0.0f};
113 camera->up = (Vector3){0.0f, 1.0f, 0.0f};
114 camera->fovy = 10.0f;
115 camera->projection = CAMERA_ORTHOGRAPHIC;
116 }
117
118 void DrawLevelHud(Level *level)
119 {
120 const char *text = TextFormat("Gold: %d", level->playerGold);
121 Font font = GetFontDefault();
122 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 120, 10}, font.baseSize * 2.0f, 2.0f, BLACK);
123 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 122, 8}, font.baseSize * 2.0f, 2.0f, YELLOW);
124 }
125
126 void DrawLevelReportLostWave(Level *level)
127 {
128 BeginMode3D(level->camera);
129 DrawLevelGround(level);
130 TowerDraw();
131 EnemyDraw();
132 ProjectileDraw();
133 ParticleDraw();
134 guiState.isBlocked = 0;
135 EndMode3D();
136
137 TowerDrawHealthBars(level->camera);
138
139 const char *text = "Wave lost";
140 int textWidth = MeasureText(text, 20);
141 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
142
143 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
144 {
145 level->nextState = LEVEL_STATE_RESET;
146 }
147 }
148
149 int HasLevelNextWave(Level *level)
150 {
151 for (int i = 0; i < 10; i++)
152 {
153 EnemyWave *wave = &level->waves[i];
154 if (wave->wave == level->currentWave)
155 {
156 return 1;
157 }
158 }
159 return 0;
160 }
161
162 void DrawLevelReportWonWave(Level *level)
163 {
164 BeginMode3D(level->camera);
165 DrawLevelGround(level);
166 TowerDraw();
167 EnemyDraw();
168 ProjectileDraw();
169 ParticleDraw();
170 guiState.isBlocked = 0;
171 EndMode3D();
172
173 TowerDrawHealthBars(level->camera);
174
175 const char *text = "Wave won";
176 int textWidth = MeasureText(text, 20);
177 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
178
179
180 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
181 {
182 level->nextState = LEVEL_STATE_RESET;
183 }
184
185 if (HasLevelNextWave(level))
186 {
187 if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
188 {
189 level->nextState = LEVEL_STATE_BUILDING;
190 }
191 }
192 else {
193 if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
194 {
195 level->nextState = LEVEL_STATE_WON_LEVEL;
196 }
197 }
198 }
199
200 void DrawBuildingBuildButton(Level *level, int x, int y, int width, int height, uint8_t towerType, const char *name)
201 {
202 static ButtonState buttonStates[8] = {0};
203 int cost = GetTowerCosts(towerType);
204 const char *text = TextFormat("%s: %d", name, cost);
205 buttonStates[towerType].isSelected = level->placementMode == towerType;
206 buttonStates[towerType].isDisabled = level->playerGold < cost;
207 if (Button(text, x, y, width, height, &buttonStates[towerType]))
208 {
209 level->placementMode = buttonStates[towerType].isSelected ? 0 : towerType;
210 }
211 }
212
213 float GetRandomFloat(float min, float max)
214 {
215 int random = GetRandomValue(0, 0xfffffff);
216 return ((float)random / (float)0xfffffff) * (max - min) + min;
217 }
218
219 void DrawLevelGround(Level *level)
220 {
221 // draw checkerboard ground pattern
222 for (int x = -5; x <= 5; x += 1)
223 {
224 for (int y = -5; y <= 5; y += 1)
225 {
226 Model *model = (x + y) % 2 == 0 ? &floorTileAModel : &floorTileBModel;
227 DrawModel(*model, (Vector3){x, 0.0f, y}, 1.0f, WHITE);
228 }
229 }
230
231 int oldSeed = GetRandomValue(0, 0xfffffff);
232 SetRandomSeed(level->seed);
233 // increase probability for trees via duplicated entries
234 Model borderModels[64];
235 int maxRockCount = GetRandomValue(2, 6);
236 int maxTreeCount = GetRandomValue(10, 20);
237 int maxFirTreeCount = GetRandomValue(5, 10);
238 int maxLeafTreeCount = maxTreeCount - maxFirTreeCount;
239 int grassPatchCount = GetRandomValue(5, 30);
240
241 int modelCount = 0;
242 for (int i = 0; i < maxRockCount && modelCount < 63; i++)
243 {
244 borderModels[modelCount++] = rockModels[GetRandomValue(0, 5)];
245 }
246 for (int i = 0; i < maxLeafTreeCount && modelCount < 63; i++)
247 {
248 borderModels[modelCount++] = treeModel[GetRandomValue(0, 1)];
249 }
250 for (int i = 0; i < maxFirTreeCount && modelCount < 63; i++)
251 {
252 borderModels[modelCount++] = firTreeModel[GetRandomValue(0, 1)];
253 }
254 for (int i = 0; i < grassPatchCount && modelCount < 63; i++)
255 {
256 borderModels[modelCount++] = grassPatchModel[0];
257 }
258
259 // draw some objects around the border of the map
260 Vector3 up = {0, 1, 0};
261 // a pseudo random number generator to get the same result every time
262 const float wiggle = 0.75f;
263 const int layerCount = 3;
264 for (int layer = 0; layer < layerCount; layer++)
265 {
266 int layerPos = 6 + layer;
267 for (int x = -6 + layer; x <= 6 + layer; x += 1)
268 {
269 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)],
270 (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, -layerPos + GetRandomFloat(0.0f, wiggle)},
271 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
272 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)],
273 (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, layerPos + GetRandomFloat(0.0f, wiggle)},
274 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
275 }
276
277 for (int z = -5 + layer; z <= 5 + layer; z += 1)
278 {
279 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)],
280 (Vector3){-layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)},
281 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
282 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)],
283 (Vector3){layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)},
284 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
285 }
286 }
287
288 SetRandomSeed(oldSeed);
289 }
290
291 void DrawEnemyPath(Level *level, Color arrowColor)
292 {
293 const int castleX = 0, castleY = 0;
294 const int maxWaypointCount = 200;
295 const float timeStep = 1.0f;
296 Vector3 arrowScale = {0.75f, 0.75f, 0.75f};
297
298 // we start with a time offset to simulate the path,
299 // this way the arrows are animated in a forward moving direction
300 // The time is wrapped around the time step to get a smooth animation
301 float timeOffset = fmodf(GetTime(), timeStep);
302
303 for (int i = 0; i < ENEMY_MAX_WAVE_COUNT; i++)
304 {
305 EnemyWave *wave = &level->waves[i];
306 if (wave->wave != level->currentWave)
307 {
308 continue;
309 }
310
311 // use this dummy enemy to simulate the path
312 Enemy dummy = {
313 .enemyType = ENEMY_TYPE_MINION,
314 .simPosition = (Vector2){wave->spawnPosition.x, wave->spawnPosition.y},
315 .nextX = wave->spawnPosition.x,
316 .nextY = wave->spawnPosition.y,
317 .currentX = wave->spawnPosition.x,
318 .currentY = wave->spawnPosition.y,
319 };
320
321 float deltaTime = timeOffset;
322 for (int j = 0; j < maxWaypointCount; j++)
323 {
324 int waypointPassedCount = 0;
325 Vector2 pos = EnemyGetPosition(&dummy, deltaTime, &dummy.simVelocity, &waypointPassedCount);
326 // after the initial variable starting offset, we use a fixed time step
327 deltaTime = timeStep;
328 dummy.simPosition = pos;
329
330 // Update the dummy's position just like we do in the regular enemy update loop
331 for (int k = 0; k < waypointPassedCount; k++)
332 {
333 dummy.currentX = dummy.nextX;
334 dummy.currentY = dummy.nextY;
335 if (EnemyGetNextPosition(dummy.currentX, dummy.currentY, &dummy.nextX, &dummy.nextY) &&
336 Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
337 {
338 break;
339 }
340 }
341 if (Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
342 {
343 break;
344 }
345
346 // get the angle we need to rotate the arrow model. The velocity is just fine for this.
347 float angle = atan2f(dummy.simVelocity.x, dummy.simVelocity.y) * RAD2DEG - 90.0f;
348 DrawModelEx(pathArrowModel, (Vector3){pos.x, 0.15f, pos.y}, (Vector3){0, 1, 0}, angle, arrowScale, arrowColor);
349 }
350 }
351 }
352
353 void DrawLevelBuildingState(Level *level)
354 {
355 BeginMode3D(level->camera);
356 DrawLevelGround(level);
357
358 int blockedCellCount = 0;
359 Vector2 blockedCells[1];
360 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera);
361 float planeDistance = ray.position.y / -ray.direction.y;
362 float planeX = ray.direction.x * planeDistance + ray.position.x;
363 float planeY = ray.direction.z * planeDistance + ray.position.z;
364 int16_t mapX = (int16_t)floorf(planeX + 0.5f);
365 int16_t mapY = (int16_t)floorf(planeY + 0.5f);
366 if (level->placementMode && !guiState.isBlocked && mapX >= -5 && mapX <= 5 && mapY >= -5 && mapY <= 5)
367 {
368 DrawCubeWires((Vector3){mapX, 0.2f, mapY}, 1.0f, 0.4f, 1.0f, RED);
369 blockedCells[blockedCellCount++] = (Vector2){mapX, mapY};
370 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
371 {
372 if (TowerTryAdd(level->placementMode, mapX, mapY))
373 {
374 level->playerGold -= GetTowerCosts(level->placementMode);
375 level->placementMode = TOWER_TYPE_NONE;
376 }
377 }
378 }
379
380 PathFindingMapUpdate(blockedCellCount, blockedCells);
381 TowerDraw();
382 EnemyDraw();
383 ProjectileDraw();
384 ParticleDraw();
385
386 // disable depth testing for the path arrows
387 // flush the 3D batch to draw the arrows on top of everything
388 rlDrawRenderBatchActive();
389 rlDisableDepthTest();
390 DrawEnemyPath(level, (Color){64, 64, 64, 160});
391
392 rlDrawRenderBatchActive();
393 rlEnableDepthTest();
394 DrawEnemyPath(level, WHITE);
395
396 guiState.isBlocked = 0;
397
398 EndMode3D();
399
400 TowerDrawHealthBars(level->camera);
401
402 static ButtonState buildWallButtonState = {0};
403 static ButtonState buildGunButtonState = {0};
404 buildWallButtonState.isSelected = level->placementMode == TOWER_TYPE_WALL;
405 buildGunButtonState.isSelected = level->placementMode == TOWER_TYPE_ARCHER;
406
407 DrawBuildingBuildButton(level, 10, 10, 110, 30, TOWER_TYPE_WALL, "Wall");
408 DrawBuildingBuildButton(level, 10, 50, 110, 30, TOWER_TYPE_ARCHER, "Archer");
409 DrawBuildingBuildButton(level, 10, 90, 110, 30, TOWER_TYPE_BALLISTA, "Ballista");
410 DrawBuildingBuildButton(level, 10, 130, 110, 30, TOWER_TYPE_CATAPULT, "Catapult");
411
412 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
413 {
414 level->nextState = LEVEL_STATE_RESET;
415 }
416
417 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0))
418 {
419 level->nextState = LEVEL_STATE_BATTLE;
420 }
421
422 const char *text = "Building phase";
423 int textWidth = MeasureText(text, 20);
424 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
425 }
426
427 void InitBattleStateConditions(Level *level)
428 {
429 level->state = LEVEL_STATE_BATTLE;
430 level->nextState = LEVEL_STATE_NONE;
431 level->waveEndTimer = 0.0f;
432 for (int i = 0; i < 10; i++)
433 {
434 EnemyWave *wave = &level->waves[i];
435 wave->spawned = 0;
436 wave->timeToSpawnNext = wave->delay;
437 }
438 }
439
440 void DrawLevelBattleState(Level *level)
441 {
442 BeginMode3D(level->camera);
443 DrawLevelGround(level);
444 TowerDraw();
445 EnemyDraw();
446 ProjectileDraw();
447 ParticleDraw();
448 guiState.isBlocked = 0;
449 EndMode3D();
450
451 EnemyDrawHealthbars(level->camera);
452 TowerDrawHealthBars(level->camera);
453
454 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
455 {
456 level->nextState = LEVEL_STATE_RESET;
457 }
458
459 int maxCount = 0;
460 int remainingCount = 0;
461 for (int i = 0; i < 10; i++)
462 {
463 EnemyWave *wave = &level->waves[i];
464 if (wave->wave != level->currentWave)
465 {
466 continue;
467 }
468 maxCount += wave->count;
469 remainingCount += wave->count - wave->spawned;
470 }
471 int aliveCount = EnemyCount();
472 remainingCount += aliveCount;
473
474 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount);
475 int textWidth = MeasureText(text, 20);
476 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
477 }
478
479 void DrawLevel(Level *level)
480 {
481 switch (level->state)
482 {
483 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break;
484 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break;
485 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break;
486 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break;
487 default: break;
488 }
489
490 DrawLevelHud(level);
491 }
492
493 void UpdateLevel(Level *level)
494 {
495 if (level->state == LEVEL_STATE_BATTLE)
496 {
497 int activeWaves = 0;
498 for (int i = 0; i < 10; i++)
499 {
500 EnemyWave *wave = &level->waves[i];
501 if (wave->spawned >= wave->count || wave->wave != level->currentWave)
502 {
503 continue;
504 }
505 activeWaves++;
506 wave->timeToSpawnNext -= gameTime.deltaTime;
507 if (wave->timeToSpawnNext <= 0.0f)
508 {
509 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y);
510 if (enemy)
511 {
512 wave->timeToSpawnNext = wave->interval;
513 wave->spawned++;
514 }
515 }
516 }
517 if (GetTowerByType(TOWER_TYPE_BASE) == 0) {
518 level->waveEndTimer += gameTime.deltaTime;
519 if (level->waveEndTimer >= 2.0f)
520 {
521 level->nextState = LEVEL_STATE_LOST_WAVE;
522 }
523 }
524 else if (activeWaves == 0 && EnemyCount() == 0)
525 {
526 level->waveEndTimer += gameTime.deltaTime;
527 if (level->waveEndTimer >= 2.0f)
528 {
529 level->nextState = LEVEL_STATE_WON_WAVE;
530 }
531 }
532 }
533
534 PathFindingMapUpdate(0, 0);
535 EnemyUpdate();
536 TowerUpdate();
537 ProjectileUpdate();
538 ParticleUpdate();
539
540 if (level->nextState == LEVEL_STATE_RESET)
541 {
542 InitLevel(level);
543 }
544
545 if (level->nextState == LEVEL_STATE_BATTLE)
546 {
547 InitBattleStateConditions(level);
548 }
549
550 if (level->nextState == LEVEL_STATE_WON_WAVE)
551 {
552 level->currentWave++;
553 level->state = LEVEL_STATE_WON_WAVE;
554 }
555
556 if (level->nextState == LEVEL_STATE_LOST_WAVE)
557 {
558 level->state = LEVEL_STATE_LOST_WAVE;
559 }
560
561 if (level->nextState == LEVEL_STATE_BUILDING)
562 {
563 level->state = LEVEL_STATE_BUILDING;
564 }
565
566 if (level->nextState == LEVEL_STATE_WON_LEVEL)
567 {
568 // make something of this later
569 InitLevel(level);
570 }
571
572 level->nextState = LEVEL_STATE_NONE;
573 }
574
575 float nextSpawnTime = 0.0f;
576
577 void ResetGame()
578 {
579 InitLevel(currentLevel);
580 }
581
582 void InitGame()
583 {
584 TowerInit();
585 EnemyInit();
586 ProjectileInit();
587 ParticleInit();
588 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f);
589
590 currentLevel = levels;
591 InitLevel(currentLevel);
592 }
593
594 //# Immediate GUI functions
595
596 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth)
597 {
598 const float healthBarHeight = 6.0f;
599 const float healthBarOffset = 15.0f;
600 const float inset = 2.0f;
601 const float innerWidth = healthBarWidth - inset * 2;
602 const float innerHeight = healthBarHeight - inset * 2;
603
604 Vector2 screenPos = GetWorldToScreen(position, camera);
605 float centerX = screenPos.x - healthBarWidth * 0.5f;
606 float topY = screenPos.y - healthBarOffset;
607 DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK);
608 float healthWidth = innerWidth * healthRatio;
609 DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor);
610 }
611
612 int Button(const char *text, int x, int y, int width, int height, ButtonState *state)
613 {
614 Rectangle bounds = {x, y, width, height};
615 int isPressed = 0;
616 int isSelected = state && state->isSelected;
617 int isDisabled = state && state->isDisabled;
618 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !isDisabled)
619 {
620 Color color = isSelected ? DARKGRAY : GRAY;
621 DrawRectangle(x, y, width, height, color);
622 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
623 {
624 isPressed = 1;
625 }
626 guiState.isBlocked = 1;
627 }
628 else
629 {
630 Color color = isSelected ? WHITE : LIGHTGRAY;
631 DrawRectangle(x, y, width, height, color);
632 }
633 Font font = GetFontDefault();
634 Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1);
635 Color textColor = isDisabled ? GRAY : BLACK;
636 DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, textColor);
637 return isPressed;
638 }
639
640 //# Main game loop
641
642 void GameUpdate()
643 {
644 float dt = GetFrameTime();
645 // cap maximum delta time to 0.1 seconds to prevent large time steps
646 if (dt > 0.1f) dt = 0.1f;
647 gameTime.time += dt;
648 gameTime.deltaTime = dt;
649
650 UpdateLevel(currentLevel);
651 }
652
653 int main(void)
654 {
655 int screenWidth, screenHeight;
656 GetPreferredSize(&screenWidth, &screenHeight);
657 InitWindow(screenWidth, screenHeight, "Tower defense");
658 SetTargetFPS(30);
659
660 LoadAssets();
661 InitGame();
662
663 while (!WindowShouldClose())
664 {
665 if (IsPaused()) {
666 // canvas is not visible in browser - do nothing
667 continue;
668 }
669
670 BeginDrawing();
671 ClearBackground((Color){0x4E, 0x63, 0x26, 0xFF});
672
673 GameUpdate();
674 DrawLevel(currentLevel);
675
676 EndDrawing();
677 }
678
679 CloseWindow();
680
681 return 0;
682 }
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 #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 #define ENEMY_MAX_PATH_COUNT 8
12 #define ENEMY_MAX_COUNT 400
13 #define ENEMY_TYPE_NONE 0
14 #define ENEMY_TYPE_MINION 1
15
16 #define PARTICLE_MAX_COUNT 400
17 #define PARTICLE_TYPE_NONE 0
18 #define PARTICLE_TYPE_EXPLOSION 1
19
20 typedef struct Particle
21 {
22 uint8_t particleType;
23 float spawnTime;
24 float lifetime;
25 Vector3 position;
26 Vector3 velocity;
27 Vector3 scale;
28 } Particle;
29
30 #define TOWER_MAX_COUNT 400
31 enum TowerType
32 {
33 TOWER_TYPE_NONE,
34 TOWER_TYPE_BASE,
35 TOWER_TYPE_ARCHER,
36 TOWER_TYPE_BALLISTA,
37 TOWER_TYPE_CATAPULT,
38 TOWER_TYPE_WALL,
39 TOWER_TYPE_COUNT
40 };
41
42 typedef struct HitEffectConfig
43 {
44 float damage;
45 float areaDamageRadius;
46 float pushbackPowerDistance;
47 } HitEffectConfig;
48
49 typedef struct TowerTypeConfig
50 {
51 float cooldown;
52 float range;
53 float projectileSpeed;
54
55 uint8_t cost;
56 uint8_t projectileType;
57 uint16_t maxHealth;
58
59 HitEffectConfig hitEffect;
60 } TowerTypeConfig;
61
62 typedef struct Tower
63 {
64 int16_t x, y;
65 uint8_t towerType;
66 Vector2 lastTargetPosition;
67 float cooldown;
68 float damage;
69 } Tower;
70
71 typedef struct GameTime
72 {
73 float time;
74 float deltaTime;
75 } GameTime;
76
77 typedef struct ButtonState {
78 char isSelected;
79 char isDisabled;
80 } ButtonState;
81
82 typedef struct GUIState {
83 int isBlocked;
84 } GUIState;
85
86 typedef enum LevelState
87 {
88 LEVEL_STATE_NONE,
89 LEVEL_STATE_BUILDING,
90 LEVEL_STATE_BATTLE,
91 LEVEL_STATE_WON_WAVE,
92 LEVEL_STATE_LOST_WAVE,
93 LEVEL_STATE_WON_LEVEL,
94 LEVEL_STATE_RESET,
95 } LevelState;
96
97 typedef struct EnemyWave {
98 uint8_t enemyType;
99 uint8_t wave;
100 uint16_t count;
101 float interval;
102 float delay;
103 Vector2 spawnPosition;
104
105 uint16_t spawned;
106 float timeToSpawnNext;
107 } EnemyWave;
108
109 #define ENEMY_MAX_WAVE_COUNT 10
110
111 typedef struct Level
112 {
113 int seed;
114 LevelState state;
115 LevelState nextState;
116 Camera3D camera;
117 int placementMode;
118
119 int initialGold;
120 int playerGold;
121
122 EnemyWave waves[ENEMY_MAX_WAVE_COUNT];
123 int currentWave;
124 float waveEndTimer;
125 } Level;
126
127 typedef struct DeltaSrc
128 {
129 char x, y;
130 } DeltaSrc;
131
132 typedef struct PathfindingMap
133 {
134 int width, height;
135 float scale;
136 float *distances;
137 long *towerIndex;
138 DeltaSrc *deltaSrc;
139 float maxDistance;
140 Matrix toMapSpace;
141 Matrix toWorldSpace;
142 } PathfindingMap;
143
144 // when we execute the pathfinding algorithm, we need to store the active nodes
145 // in a queue. Each node has a position, a distance from the start, and the
146 // position of the node that we came from.
147 typedef struct PathfindingNode
148 {
149 int16_t x, y, fromX, fromY;
150 float distance;
151 } PathfindingNode;
152
153 typedef struct EnemyId
154 {
155 uint16_t index;
156 uint16_t generation;
157 } EnemyId;
158
159 typedef struct EnemyClassConfig
160 {
161 float speed;
162 float health;
163 float radius;
164 float maxAcceleration;
165 float requiredContactTime;
166 float explosionDamage;
167 float explosionRange;
168 float explosionPushbackPower;
169 int goldValue;
170 } EnemyClassConfig;
171
172 typedef struct Enemy
173 {
174 int16_t currentX, currentY;
175 int16_t nextX, nextY;
176 Vector2 simPosition;
177 Vector2 simVelocity;
178 uint16_t generation;
179 float walkedDistance;
180 float startMovingTime;
181 float damage, futureDamage;
182 float contactTime;
183 uint8_t enemyType;
184 uint8_t movePathCount;
185 Vector2 movePath[ENEMY_MAX_PATH_COUNT];
186 } Enemy;
187
188 // a unit that uses sprites to be drawn
189 #define SPRITE_UNIT_PHASE_WEAPON_IDLE 0
190 #define SPRITE_UNIT_PHASE_WEAPON_COOLDOWN 1
191 typedef struct SpriteUnit
192 {
193 Rectangle srcRect;
194 Vector2 offset;
195 int frameCount;
196 float frameDuration;
197 Rectangle srcWeaponIdleRect;
198 Vector2 srcWeaponIdleOffset;
199 Rectangle srcWeaponCooldownRect;
200 Vector2 srcWeaponCooldownOffset;
201 } SpriteUnit;
202
203 #define PROJECTILE_MAX_COUNT 1200
204 #define PROJECTILE_TYPE_NONE 0
205 #define PROJECTILE_TYPE_ARROW 1
206 #define PROJECTILE_TYPE_CATAPULT 2
207 #define PROJECTILE_TYPE_BALLISTA 3
208
209 typedef struct Projectile
210 {
211 uint8_t projectileType;
212 float shootTime;
213 float arrivalTime;
214 float distance;
215 Vector3 position;
216 Vector3 target;
217 Vector3 directionNormal;
218 EnemyId targetEnemy;
219 HitEffectConfig hitEffectConfig;
220 } Projectile;
221
222 //# Function declarations
223 float TowerGetMaxHealth(Tower *tower);
224 int Button(const char *text, int x, int y, int width, int height, ButtonState *state);
225 int EnemyAddDamageRange(Vector2 position, float range, float damage);
226 int EnemyAddDamage(Enemy *enemy, float damage);
227
228 //# Enemy functions
229 void EnemyInit();
230 void EnemyDraw();
231 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource);
232 void EnemyUpdate();
233 float EnemyGetCurrentMaxSpeed(Enemy *enemy);
234 float EnemyGetMaxHealth(Enemy *enemy);
235 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY);
236 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount);
237 EnemyId EnemyGetId(Enemy *enemy);
238 Enemy *EnemyTryResolve(EnemyId enemyId);
239 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY);
240 int EnemyAddDamage(Enemy *enemy, float damage);
241 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range);
242 int EnemyCount();
243 void EnemyDrawHealthbars(Camera3D camera);
244
245 //# Tower functions
246 void TowerInit();
247 Tower *TowerGetAt(int16_t x, int16_t y);
248 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y);
249 Tower *GetTowerByType(uint8_t towerType);
250 int GetTowerCosts(uint8_t towerType);
251 float TowerGetMaxHealth(Tower *tower);
252 void TowerDraw();
253 void TowerUpdate();
254 void TowerDrawHealthBars(Camera3D camera);
255 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase);
256
257 //# Particles
258 void ParticleInit();
259 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime);
260 void ParticleUpdate();
261 void ParticleDraw();
262
263 //# Projectiles
264 void ProjectileInit();
265 void ProjectileDraw();
266 void ProjectileUpdate();
267 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig);
268
269 //# Pathfinding map
270 void PathfindingMapInit(int width, int height, Vector3 translate, float scale);
271 float PathFindingGetDistance(int mapX, int mapY);
272 Vector2 PathFindingGetGradient(Vector3 world);
273 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY);
274 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells);
275 void PathFindingMapDraw();
276
277 //# UI
278 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth);
279
280 //# Level
281 void DrawLevelGround(Level *level);
282 void DrawEnemyPath(Level *level, Color arrowColor);
283
284 //# variables
285 extern Level *currentLevel;
286 extern Enemy enemies[ENEMY_MAX_COUNT];
287 extern int enemyCount;
288 extern EnemyClassConfig enemyClassConfigs[];
289
290 extern GUIState guiState;
291 extern GameTime gameTime;
292 extern Tower towers[TOWER_MAX_COUNT];
293 extern int towerCount;
294
295 extern Texture2D palette, spriteSheet;
296
297 #endif
1 #include "td_main.h"
2 #include <raymath.h>
3
4 static TowerTypeConfig towerTypeConfigs[TOWER_TYPE_COUNT] = {
5 [TOWER_TYPE_BASE] = {
6 .maxHealth = 10,
7 },
8 [TOWER_TYPE_ARCHER] = {
9 .cooldown = 0.5f,
10 .range = 3.0f,
11 .cost = 6,
12 .maxHealth = 10,
13 .projectileSpeed = 4.0f,
14 .projectileType = PROJECTILE_TYPE_ARROW,
15 .hitEffect = {
16 .damage = 3.0f,
17 }
18 },
19 [TOWER_TYPE_BALLISTA] = {
20 .cooldown = 1.5f,
21 .range = 6.0f,
22 .cost = 9,
23 .maxHealth = 10,
24 .projectileSpeed = 10.0f,
25 .projectileType = PROJECTILE_TYPE_BALLISTA,
26 .hitEffect = {
27 .damage = 6.0f,
28 .pushbackPowerDistance = 0.25f,
29 }
30 },
31 [TOWER_TYPE_CATAPULT] = {
32 .cooldown = 1.7f,
33 .range = 5.0f,
34 .cost = 10,
35 .maxHealth = 10,
36 .projectileSpeed = 3.0f,
37 .projectileType = PROJECTILE_TYPE_CATAPULT,
38 .hitEffect = {
39 .damage = 2.0f,
40 .areaDamageRadius = 1.75f,
41 }
42 },
43 [TOWER_TYPE_WALL] = {
44 .cost = 2,
45 .maxHealth = 10,
46 },
47 };
48
49 Tower towers[TOWER_MAX_COUNT];
50 int towerCount = 0;
51
52 Model towerModels[TOWER_TYPE_COUNT];
53
54 // definition of our archer unit
55 SpriteUnit archerUnit = {
56 .srcRect = {0, 0, 16, 16},
57 .offset = {7, 1},
58 .frameCount = 1,
59 .frameDuration = 0.0f,
60 .srcWeaponIdleRect = {16, 0, 6, 16},
61 .srcWeaponIdleOffset = {8, 0},
62 .srcWeaponCooldownRect = {22, 0, 11, 16},
63 .srcWeaponCooldownOffset = {10, 0},
64 };
65
66 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase)
67 {
68 float xScale = flip ? -1.0f : 1.0f;
69 Camera3D camera = currentLevel->camera;
70 float size = 0.5f;
71 Vector2 offset = (Vector2){ unit.offset.x / 16.0f * size, unit.offset.y / 16.0f * size * xScale };
72 Vector2 scale = (Vector2){ unit.srcRect.width / 16.0f * size, unit.srcRect.height / 16.0f * size };
73 // we want the sprite to face the camera, so we need to calculate the up vector
74 Vector3 forward = Vector3Subtract(camera.target, camera.position);
75 Vector3 up = {0, 1, 0};
76 Vector3 right = Vector3CrossProduct(forward, up);
77 up = Vector3Normalize(Vector3CrossProduct(right, forward));
78
79 Rectangle srcRect = unit.srcRect;
80 if (unit.frameCount > 1)
81 {
82 srcRect.x += (int)(t / unit.frameDuration) % unit.frameCount * srcRect.width;
83 }
84 if (flip)
85 {
86 srcRect.x += srcRect.width;
87 srcRect.width = -srcRect.width;
88 }
89 DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
90
91 if (phase == SPRITE_UNIT_PHASE_WEAPON_COOLDOWN && unit.srcWeaponCooldownRect.width > 0)
92 {
93 offset = (Vector2){ unit.srcWeaponCooldownOffset.x / 16.0f * size, unit.srcWeaponCooldownOffset.y / 16.0f * size };
94 scale = (Vector2){ unit.srcWeaponCooldownRect.width / 16.0f * size, unit.srcWeaponCooldownRect.height / 16.0f * size };
95 srcRect = unit.srcWeaponCooldownRect;
96 if (flip)
97 {
98 // position.x = flip * scale.x * 0.5f;
99 srcRect.x += srcRect.width;
100 srcRect.width = -srcRect.width;
101 offset.x = scale.x - offset.x;
102 }
103 DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
104 }
105 else if (phase == SPRITE_UNIT_PHASE_WEAPON_IDLE && unit.srcWeaponIdleRect.width > 0)
106 {
107 offset = (Vector2){ unit.srcWeaponIdleOffset.x / 16.0f * size, unit.srcWeaponIdleOffset.y / 16.0f * size };
108 scale = (Vector2){ unit.srcWeaponIdleRect.width / 16.0f * size, unit.srcWeaponIdleRect.height / 16.0f * size };
109 srcRect = unit.srcWeaponIdleRect;
110 if (flip)
111 {
112 // position.x = flip * scale.x * 0.5f;
113 srcRect.x += srcRect.width;
114 srcRect.width = -srcRect.width;
115 offset.x = scale.x - offset.x;
116 }
117 DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
118 }
119 }
120
121 void TowerInit()
122 {
123 for (int i = 0; i < TOWER_MAX_COUNT; i++)
124 {
125 towers[i] = (Tower){0};
126 }
127 towerCount = 0;
128
129 towerModels[TOWER_TYPE_BASE] = LoadModel("data/keep.glb");
130 towerModels[TOWER_TYPE_WALL] = LoadModel("data/wall-0000.glb");
131
132 for (int i = 0; i < TOWER_TYPE_COUNT; i++)
133 {
134 if (towerModels[i].materials)
135 {
136 // assign the palette texture to the material of the model (0 is not used afaik)
137 towerModels[i].materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
138 }
139 }
140 }
141
142 static void TowerGunUpdate(Tower *tower)
143 {
144 TowerTypeConfig config = towerTypeConfigs[tower->towerType];
145 if (tower->cooldown <= 0.0f)
146 {
147 Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, config.range);
148 if (enemy)
149 {
150 tower->cooldown = config.cooldown;
151 // shoot the enemy; determine future position of the enemy
152 float bulletSpeed = config.projectileSpeed;
153 Vector2 velocity = enemy->simVelocity;
154 Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0);
155 Vector2 towerPosition = {tower->x, tower->y};
156 float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed;
157 for (int i = 0; i < 8; i++) {
158 velocity = enemy->simVelocity;
159 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0);
160 float distance = Vector2Distance(towerPosition, futurePosition);
161 float eta2 = distance / bulletSpeed;
162 if (fabs(eta - eta2) < 0.01f) {
163 break;
164 }
165 eta = (eta2 + eta) * 0.5f;
166 }
167
168 ProjectileTryAdd(config.projectileType, enemy,
169 (Vector3){towerPosition.x, 1.33f, towerPosition.y},
170 (Vector3){futurePosition.x, 0.25f, futurePosition.y},
171 bulletSpeed, config.hitEffect);
172 enemy->futureDamage += config.hitEffect.damage;
173 tower->lastTargetPosition = futurePosition;
174 }
175 }
176 else
177 {
178 tower->cooldown -= gameTime.deltaTime;
179 }
180 }
181
182 Tower *TowerGetAt(int16_t x, int16_t y)
183 {
184 for (int i = 0; i < towerCount; i++)
185 {
186 if (towers[i].x == x && towers[i].y == y && towers[i].towerType != TOWER_TYPE_NONE)
187 {
188 return &towers[i];
189 }
190 }
191 return 0;
192 }
193
194 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y)
195 {
196 if (towerCount >= TOWER_MAX_COUNT)
197 {
198 return 0;
199 }
200
201 Tower *tower = TowerGetAt(x, y);
202 if (tower)
203 {
204 return 0;
205 }
206
207 tower = &towers[towerCount++];
208 tower->x = x;
209 tower->y = y;
210 tower->towerType = towerType;
211 tower->cooldown = 0.0f;
212 tower->damage = 0.0f;
213 return tower;
214 }
215
216 Tower *GetTowerByType(uint8_t towerType)
217 {
218 for (int i = 0; i < towerCount; i++)
219 {
220 if (towers[i].towerType == towerType)
221 {
222 return &towers[i];
223 }
224 }
225 return 0;
226 }
227
228 int GetTowerCosts(uint8_t towerType)
229 {
230 return towerTypeConfigs[towerType].cost;
231 }
232
233 float TowerGetMaxHealth(Tower *tower)
234 {
235 return towerTypeConfigs[tower->towerType].maxHealth;
236 }
237
238 void TowerDraw()
239 {
240 for (int i = 0; i < towerCount; i++)
241 {
242 Tower tower = towers[i];
243 if (tower.towerType == TOWER_TYPE_NONE)
244 {
245 continue;
246 }
247
248 switch (tower.towerType)
249 {
250 case TOWER_TYPE_ARCHER:
251 {
252 Vector2 screenPosTower = GetWorldToScreen((Vector3){tower.x, 0.0f, tower.y}, currentLevel->camera);
253 Vector2 screenPosTarget = GetWorldToScreen((Vector3){tower.lastTargetPosition.x, 0.0f, tower.lastTargetPosition.y}, currentLevel->camera);
254 DrawModel(towerModels[TOWER_TYPE_WALL], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
255 DrawSpriteUnit(archerUnit, (Vector3){tower.x, 1.0f, tower.y}, 0, screenPosTarget.x > screenPosTower.x,
256 tower.cooldown > 0.2f ? SPRITE_UNIT_PHASE_WEAPON_COOLDOWN : SPRITE_UNIT_PHASE_WEAPON_IDLE);
257 }
258 break;
259 case TOWER_TYPE_BALLISTA:
260 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, BROWN);
261 break;
262 case TOWER_TYPE_CATAPULT:
263 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, DARKGRAY);
264 break;
265 default:
266 if (towerModels[tower.towerType].materials)
267 {
268 DrawModel(towerModels[tower.towerType], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
269 } else {
270 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY);
271 }
272 break;
273 }
274 }
275 }
276
277 void TowerUpdate()
278 {
279 for (int i = 0; i < towerCount; i++)
280 {
281 Tower *tower = &towers[i];
282 switch (tower->towerType)
283 {
284 case TOWER_TYPE_CATAPULT:
285 case TOWER_TYPE_BALLISTA:
286 case TOWER_TYPE_ARCHER:
287 TowerGunUpdate(tower);
288 break;
289 }
290 }
291 }
292
293 void TowerDrawHealthBars(Camera3D camera)
294 {
295 for (int i = 0; i < towerCount; i++)
296 {
297 Tower *tower = &towers[i];
298 if (tower->towerType == TOWER_TYPE_NONE || tower->damage <= 0.0f)
299 {
300 continue;
301 }
302
303 Vector3 position = (Vector3){tower->x, 0.5f, tower->y};
304 float maxHealth = TowerGetMaxHealth(tower);
305 float health = maxHealth - tower->damage;
306 float healthRatio = health / maxHealth;
307
308 DrawHealthBar(camera, position, healthRatio, GREEN, 35.0f);
309 }
310 }
1 #include "td_main.h"
2 #include <raymath.h>
3 #include <stdlib.h>
4 #include <math.h>
5
6 EnemyClassConfig enemyClassConfigs[] = {
7 [ENEMY_TYPE_MINION] = {
8 .health = 10.0f,
9 .speed = 0.6f,
10 .radius = 0.25f,
11 .maxAcceleration = 1.0f,
12 .explosionDamage = 1.0f,
13 .requiredContactTime = 0.5f,
14 .explosionRange = 1.0f,
15 .explosionPushbackPower = 0.25f,
16 .goldValue = 1,
17 },
18 };
19
20 Enemy enemies[ENEMY_MAX_COUNT];
21 int enemyCount = 0;
22
23 SpriteUnit enemySprites[] = {
24 [ENEMY_TYPE_MINION] = {
25 .srcRect = {0, 16, 16, 16},
26 .offset = {8.0f, 0.0f},
27 .frameCount = 6,
28 .frameDuration = 0.1f,
29 },
30 };
31
32 void EnemyInit()
33 {
34 for (int i = 0; i < ENEMY_MAX_COUNT; i++)
35 {
36 enemies[i] = (Enemy){0};
37 }
38 enemyCount = 0;
39 }
40
41 float EnemyGetCurrentMaxSpeed(Enemy *enemy)
42 {
43 return enemyClassConfigs[enemy->enemyType].speed;
44 }
45
46 float EnemyGetMaxHealth(Enemy *enemy)
47 {
48 return enemyClassConfigs[enemy->enemyType].health;
49 }
50
51 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY)
52 {
53 int16_t castleX = 0;
54 int16_t castleY = 0;
55 int16_t dx = castleX - currentX;
56 int16_t dy = castleY - currentY;
57 if (dx == 0 && dy == 0)
58 {
59 *nextX = currentX;
60 *nextY = currentY;
61 return 1;
62 }
63 Vector2 gradient = PathFindingGetGradient((Vector3){currentX, 0, currentY});
64
65 if (gradient.x == 0 && gradient.y == 0)
66 {
67 *nextX = currentX;
68 *nextY = currentY;
69 return 1;
70 }
71
72 if (fabsf(gradient.x) > fabsf(gradient.y))
73 {
74 *nextX = currentX + (int16_t)(gradient.x > 0.0f ? 1 : -1);
75 *nextY = currentY;
76 return 0;
77 }
78 *nextX = currentX;
79 *nextY = currentY + (int16_t)(gradient.y > 0.0f ? 1 : -1);
80 return 0;
81 }
82
83
84 // this function predicts the movement of the unit for the next deltaT seconds
85 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount)
86 {
87 const float pointReachedDistance = 0.25f;
88 const float pointReachedDistance2 = pointReachedDistance * pointReachedDistance;
89 const float maxSimStepTime = 0.015625f;
90
91 float maxAcceleration = enemyClassConfigs[enemy->enemyType].maxAcceleration;
92 float maxSpeed = EnemyGetCurrentMaxSpeed(enemy);
93 int16_t nextX = enemy->nextX;
94 int16_t nextY = enemy->nextY;
95 Vector2 position = enemy->simPosition;
96 int passedCount = 0;
97 for (float t = 0.0f; t < deltaT; t += maxSimStepTime)
98 {
99 float stepTime = fminf(deltaT - t, maxSimStepTime);
100 Vector2 target = (Vector2){nextX, nextY};
101 float speed = Vector2Length(*velocity);
102 // draw the target position for debugging
103 //DrawCubeWires((Vector3){target.x, 0.2f, target.y}, 0.1f, 0.4f, 0.1f, RED);
104 Vector2 lookForwardPos = Vector2Add(position, Vector2Scale(*velocity, speed));
105 if (Vector2DistanceSqr(target, lookForwardPos) <= pointReachedDistance2)
106 {
107 // we reached the target position, let's move to the next waypoint
108 EnemyGetNextPosition(nextX, nextY, &nextX, &nextY);
109 target = (Vector2){nextX, nextY};
110 // track how many waypoints we passed
111 passedCount++;
112 }
113
114 // acceleration towards the target
115 Vector2 unitDirection = Vector2Normalize(Vector2Subtract(target, lookForwardPos));
116 Vector2 acceleration = Vector2Scale(unitDirection, maxAcceleration * stepTime);
117 *velocity = Vector2Add(*velocity, acceleration);
118
119 // limit the speed to the maximum speed
120 if (speed > maxSpeed)
121 {
122 *velocity = Vector2Scale(*velocity, maxSpeed / speed);
123 }
124
125 // move the enemy
126 position = Vector2Add(position, Vector2Scale(*velocity, stepTime));
127 }
128
129 if (waypointPassedCount)
130 {
131 (*waypointPassedCount) = passedCount;
132 }
133
134 return position;
135 }
136
137 void EnemyDraw()
138 {
139 for (int i = 0; i < enemyCount; i++)
140 {
141 Enemy enemy = enemies[i];
142 if (enemy.enemyType == ENEMY_TYPE_NONE)
143 {
144 continue;
145 }
146
147 Vector2 position = EnemyGetPosition(&enemy, gameTime.time - enemy.startMovingTime, &enemy.simVelocity, 0);
148
149 // don't draw any trails for now; might replace this with footprints later
150 // if (enemy.movePathCount > 0)
151 // {
152 // Vector3 p = {enemy.movePath[0].x, 0.2f, enemy.movePath[0].y};
153 // DrawLine3D(p, (Vector3){position.x, 0.2f, position.y}, GREEN);
154 // }
155 // for (int j = 1; j < enemy.movePathCount; j++)
156 // {
157 // Vector3 p = {enemy.movePath[j - 1].x, 0.2f, enemy.movePath[j - 1].y};
158 // Vector3 q = {enemy.movePath[j].x, 0.2f, enemy.movePath[j].y};
159 // DrawLine3D(p, q, GREEN);
160 // }
161
162 switch (enemy.enemyType)
163 {
164 case ENEMY_TYPE_MINION:
165 DrawSpriteUnit(enemySprites[ENEMY_TYPE_MINION], (Vector3){position.x, 0.0f, position.y},
166 enemy.walkedDistance, 0, 0);
167 break;
168 }
169 }
170 }
171
172 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource)
173 {
174 // damage the tower
175 float explosionDamge = enemyClassConfigs[enemy->enemyType].explosionDamage;
176 float explosionRange = enemyClassConfigs[enemy->enemyType].explosionRange;
177 float explosionPushbackPower = enemyClassConfigs[enemy->enemyType].explosionPushbackPower;
178 float explosionRange2 = explosionRange * explosionRange;
179 tower->damage += enemyClassConfigs[enemy->enemyType].explosionDamage;
180 // explode the enemy
181 if (tower->damage >= TowerGetMaxHealth(tower))
182 {
183 tower->towerType = TOWER_TYPE_NONE;
184 }
185
186 ParticleAdd(PARTICLE_TYPE_EXPLOSION,
187 explosionSource,
188 (Vector3){0, 0.1f, 0}, (Vector3){1.0f, 1.0f, 1.0f}, 1.0f);
189
190 enemy->enemyType = ENEMY_TYPE_NONE;
191
192 // push back enemies & dealing damage
193 for (int i = 0; i < enemyCount; i++)
194 {
195 Enemy *other = &enemies[i];
196 if (other->enemyType == ENEMY_TYPE_NONE)
197 {
198 continue;
199 }
200 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, other->simPosition);
201 if (distanceSqr > 0 && distanceSqr < explosionRange2)
202 {
203 Vector2 direction = Vector2Normalize(Vector2Subtract(other->simPosition, enemy->simPosition));
204 other->simPosition = Vector2Add(other->simPosition, Vector2Scale(direction, explosionPushbackPower));
205 EnemyAddDamage(other, explosionDamge);
206 }
207 }
208 }
209
210 void EnemyUpdate()
211 {
212 const float castleX = 0;
213 const float castleY = 0;
214 const float maxPathDistance2 = 0.25f * 0.25f;
215
216 for (int i = 0; i < enemyCount; i++)
217 {
218 Enemy *enemy = &enemies[i];
219 if (enemy->enemyType == ENEMY_TYPE_NONE)
220 {
221 continue;
222 }
223
224 int waypointPassedCount = 0;
225 Vector2 prevPosition = enemy->simPosition;
226 enemy->simPosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &enemy->simVelocity, &waypointPassedCount);
227 enemy->startMovingTime = gameTime.time;
228 enemy->walkedDistance += Vector2Distance(prevPosition, enemy->simPosition);
229 // track path of unit
230 if (enemy->movePathCount == 0 || Vector2DistanceSqr(enemy->simPosition, enemy->movePath[0]) > maxPathDistance2)
231 {
232 for (int j = ENEMY_MAX_PATH_COUNT - 1; j > 0; j--)
233 {
234 enemy->movePath[j] = enemy->movePath[j - 1];
235 }
236 enemy->movePath[0] = enemy->simPosition;
237 if (++enemy->movePathCount > ENEMY_MAX_PATH_COUNT)
238 {
239 enemy->movePathCount = ENEMY_MAX_PATH_COUNT;
240 }
241 }
242
243 if (waypointPassedCount > 0)
244 {
245 enemy->currentX = enemy->nextX;
246 enemy->currentY = enemy->nextY;
247 if (EnemyGetNextPosition(enemy->currentX, enemy->currentY, &enemy->nextX, &enemy->nextY) &&
248 Vector2DistanceSqr(enemy->simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
249 {
250 // enemy reached the castle; remove it
251 enemy->enemyType = ENEMY_TYPE_NONE;
252 continue;
253 }
254 }
255 }
256
257 // handle collisions between enemies
258 for (int i = 0; i < enemyCount - 1; i++)
259 {
260 Enemy *enemyA = &enemies[i];
261 if (enemyA->enemyType == ENEMY_TYPE_NONE)
262 {
263 continue;
264 }
265 for (int j = i + 1; j < enemyCount; j++)
266 {
267 Enemy *enemyB = &enemies[j];
268 if (enemyB->enemyType == ENEMY_TYPE_NONE)
269 {
270 continue;
271 }
272 float distanceSqr = Vector2DistanceSqr(enemyA->simPosition, enemyB->simPosition);
273 float radiusA = enemyClassConfigs[enemyA->enemyType].radius;
274 float radiusB = enemyClassConfigs[enemyB->enemyType].radius;
275 float radiusSum = radiusA + radiusB;
276 if (distanceSqr < radiusSum * radiusSum && distanceSqr > 0.001f)
277 {
278 // collision
279 float distance = sqrtf(distanceSqr);
280 float overlap = radiusSum - distance;
281 // move the enemies apart, but softly; if we have a clog of enemies,
282 // moving them perfectly apart can cause them to jitter
283 float positionCorrection = overlap / 5.0f;
284 Vector2 direction = (Vector2){
285 (enemyB->simPosition.x - enemyA->simPosition.x) / distance * positionCorrection,
286 (enemyB->simPosition.y - enemyA->simPosition.y) / distance * positionCorrection};
287 enemyA->simPosition = Vector2Subtract(enemyA->simPosition, direction);
288 enemyB->simPosition = Vector2Add(enemyB->simPosition, direction);
289 }
290 }
291 }
292
293 // handle collisions between enemies and towers
294 for (int i = 0; i < enemyCount; i++)
295 {
296 Enemy *enemy = &enemies[i];
297 if (enemy->enemyType == ENEMY_TYPE_NONE)
298 {
299 continue;
300 }
301 enemy->contactTime -= gameTime.deltaTime;
302 if (enemy->contactTime < 0.0f)
303 {
304 enemy->contactTime = 0.0f;
305 }
306
307 float enemyRadius = enemyClassConfigs[enemy->enemyType].radius;
308 // linear search over towers; could be optimized by using path finding tower map,
309 // but for now, we keep it simple
310 for (int j = 0; j < towerCount; j++)
311 {
312 Tower *tower = &towers[j];
313 if (tower->towerType == TOWER_TYPE_NONE)
314 {
315 continue;
316 }
317 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){tower->x, tower->y});
318 float combinedRadius = enemyRadius + 0.708; // sqrt(0.5^2 + 0.5^2), corner-center distance of square with side length 1
319 if (distanceSqr > combinedRadius * combinedRadius)
320 {
321 continue;
322 }
323 // potential collision; square / circle intersection
324 float dx = tower->x - enemy->simPosition.x;
325 float dy = tower->y - enemy->simPosition.y;
326 float absDx = fabsf(dx);
327 float absDy = fabsf(dy);
328 Vector3 contactPoint = {0};
329 if (absDx <= 0.5f && absDx <= absDy) {
330 // vertical collision; push the enemy out horizontally
331 float overlap = enemyRadius + 0.5f - absDy;
332 if (overlap < 0.0f)
333 {
334 continue;
335 }
336 float direction = dy > 0.0f ? -1.0f : 1.0f;
337 enemy->simPosition.y += direction * overlap;
338 contactPoint = (Vector3){enemy->simPosition.x, 0.2f, tower->y + direction * 0.5f};
339 }
340 else if (absDy <= 0.5f && absDy <= absDx)
341 {
342 // horizontal collision; push the enemy out vertically
343 float overlap = enemyRadius + 0.5f - absDx;
344 if (overlap < 0.0f)
345 {
346 continue;
347 }
348 float direction = dx > 0.0f ? -1.0f : 1.0f;
349 enemy->simPosition.x += direction * overlap;
350 contactPoint = (Vector3){tower->x + direction * 0.5f, 0.2f, enemy->simPosition.y};
351 }
352 else
353 {
354 // possible collision with a corner
355 float cornerDX = dx > 0.0f ? -0.5f : 0.5f;
356 float cornerDY = dy > 0.0f ? -0.5f : 0.5f;
357 float cornerX = tower->x + cornerDX;
358 float cornerY = tower->y + cornerDY;
359 float cornerDistanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){cornerX, cornerY});
360 if (cornerDistanceSqr > enemyRadius * enemyRadius)
361 {
362 continue;
363 }
364 // push the enemy out along the diagonal
365 float cornerDistance = sqrtf(cornerDistanceSqr);
366 float overlap = enemyRadius - cornerDistance;
367 float directionX = cornerDistance > 0.0f ? (cornerX - enemy->simPosition.x) / cornerDistance : -cornerDX;
368 float directionY = cornerDistance > 0.0f ? (cornerY - enemy->simPosition.y) / cornerDistance : -cornerDY;
369 enemy->simPosition.x -= directionX * overlap;
370 enemy->simPosition.y -= directionY * overlap;
371 contactPoint = (Vector3){cornerX, 0.2f, cornerY};
372 }
373
374 if (enemyClassConfigs[enemy->enemyType].explosionDamage > 0.0f)
375 {
376 enemy->contactTime += gameTime.deltaTime * 2.0f; // * 2 to undo the subtraction above
377 if (enemy->contactTime >= enemyClassConfigs[enemy->enemyType].requiredContactTime)
378 {
379 EnemyTriggerExplode(enemy, tower, contactPoint);
380 }
381 }
382 }
383 }
384 }
385
386 EnemyId EnemyGetId(Enemy *enemy)
387 {
388 return (EnemyId){enemy - enemies, enemy->generation};
389 }
390
391 Enemy *EnemyTryResolve(EnemyId enemyId)
392 {
393 if (enemyId.index >= ENEMY_MAX_COUNT)
394 {
395 return 0;
396 }
397 Enemy *enemy = &enemies[enemyId.index];
398 if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE)
399 {
400 return 0;
401 }
402 return enemy;
403 }
404
405 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY)
406 {
407 Enemy *spawn = 0;
408 for (int i = 0; i < enemyCount; i++)
409 {
410 Enemy *enemy = &enemies[i];
411 if (enemy->enemyType == ENEMY_TYPE_NONE)
412 {
413 spawn = enemy;
414 break;
415 }
416 }
417
418 if (enemyCount < ENEMY_MAX_COUNT && !spawn)
419 {
420 spawn = &enemies[enemyCount++];
421 }
422
423 if (spawn)
424 {
425 spawn->currentX = currentX;
426 spawn->currentY = currentY;
427 spawn->nextX = currentX;
428 spawn->nextY = currentY;
429 spawn->simPosition = (Vector2){currentX, currentY};
430 spawn->simVelocity = (Vector2){0, 0};
431 spawn->enemyType = enemyType;
432 spawn->startMovingTime = gameTime.time;
433 spawn->damage = 0.0f;
434 spawn->futureDamage = 0.0f;
435 spawn->generation++;
436 spawn->movePathCount = 0;
437 spawn->walkedDistance = 0.0f;
438 }
439
440 return spawn;
441 }
442
443 int EnemyAddDamageRange(Vector2 position, float range, float damage)
444 {
445 int count = 0;
446 float range2 = range * range;
447 for (int i = 0; i < enemyCount; i++)
448 {
449 Enemy *enemy = &enemies[i];
450 if (enemy->enemyType == ENEMY_TYPE_NONE)
451 {
452 continue;
453 }
454 float distance2 = Vector2DistanceSqr(position, enemy->simPosition);
455 if (distance2 <= range2)
456 {
457 EnemyAddDamage(enemy, damage);
458 count++;
459 }
460 }
461 return count;
462 }
463
464 int EnemyAddDamage(Enemy *enemy, float damage)
465 {
466 enemy->damage += damage;
467 if (enemy->damage >= EnemyGetMaxHealth(enemy))
468 {
469 currentLevel->playerGold += enemyClassConfigs[enemy->enemyType].goldValue;
470 enemy->enemyType = ENEMY_TYPE_NONE;
471 return 1;
472 }
473
474 return 0;
475 }
476
477 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
478 {
479 int16_t castleX = 0;
480 int16_t castleY = 0;
481 Enemy* closest = 0;
482 int16_t closestDistance = 0;
483 float range2 = range * range;
484 for (int i = 0; i < enemyCount; i++)
485 {
486 Enemy* enemy = &enemies[i];
487 if (enemy->enemyType == ENEMY_TYPE_NONE)
488 {
489 continue;
490 }
491 float maxHealth = EnemyGetMaxHealth(enemy);
492 if (enemy->futureDamage >= maxHealth)
493 {
494 // ignore enemies that will die soon
495 continue;
496 }
497 int16_t dx = castleX - enemy->currentX;
498 int16_t dy = castleY - enemy->currentY;
499 int16_t distance = abs(dx) + abs(dy);
500 if (!closest || distance < closestDistance)
501 {
502 float tdx = towerX - enemy->currentX;
503 float tdy = towerY - enemy->currentY;
504 float tdistance2 = tdx * tdx + tdy * tdy;
505 if (tdistance2 <= range2)
506 {
507 closest = enemy;
508 closestDistance = distance;
509 }
510 }
511 }
512 return closest;
513 }
514
515 int EnemyCount()
516 {
517 int count = 0;
518 for (int i = 0; i < enemyCount; i++)
519 {
520 if (enemies[i].enemyType != ENEMY_TYPE_NONE)
521 {
522 count++;
523 }
524 }
525 return count;
526 }
527
528 void EnemyDrawHealthbars(Camera3D camera)
529 {
530 for (int i = 0; i < enemyCount; i++)
531 {
532 Enemy *enemy = &enemies[i];
533 if (enemy->enemyType == ENEMY_TYPE_NONE || enemy->damage == 0.0f)
534 {
535 continue;
536 }
537 Vector3 position = (Vector3){enemy->simPosition.x, 0.5f, enemy->simPosition.y};
538 float maxHealth = EnemyGetMaxHealth(enemy);
539 float health = maxHealth - enemy->damage;
540 float healthRatio = health / maxHealth;
541
542 DrawHealthBar(camera, position, healthRatio, GREEN, 15.0f);
543 }
544 }
1 #include "td_main.h"
2 #include <raymath.h>
3
4 static Projectile projectiles[PROJECTILE_MAX_COUNT];
5 static int projectileCount = 0;
6
7 typedef struct ProjectileConfig
8 {
9 float arcFactor;
10 Color color;
11 Color trailColor;
12 } ProjectileConfig;
13
14 ProjectileConfig projectileConfigs[] = {
15 [PROJECTILE_TYPE_ARROW] = {
16 .arcFactor = 0.15f,
17 .color = RED,
18 .trailColor = BROWN,
19 },
20 [PROJECTILE_TYPE_CATAPULT] = {
21 .arcFactor = 0.5f,
22 .color = RED,
23 .trailColor = GRAY,
24 },
25 [PROJECTILE_TYPE_BALLISTA] = {
26 .arcFactor = 0.025f,
27 .color = RED,
28 .trailColor = BROWN,
29 },
30 };
31
32 void ProjectileInit()
33 {
34 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
35 {
36 projectiles[i] = (Projectile){0};
37 }
38 }
39
40 void ProjectileDraw()
41 {
42 for (int i = 0; i < projectileCount; i++)
43 {
44 Projectile projectile = projectiles[i];
45 if (projectile.projectileType == PROJECTILE_TYPE_NONE)
46 {
47 continue;
48 }
49 float transition = (gameTime.time - projectile.shootTime) / (projectile.arrivalTime - projectile.shootTime);
50 if (transition >= 1.0f)
51 {
52 continue;
53 }
54
55 ProjectileConfig config = projectileConfigs[projectile.projectileType];
56 for (float transitionOffset = 0.0f; transitionOffset < 1.0f; transitionOffset += 0.1f)
57 {
58 float t = transition + transitionOffset * 0.3f;
59 if (t > 1.0f)
60 {
61 break;
62 }
63 Vector3 position = Vector3Lerp(projectile.position, projectile.target, t);
64 Color color = config.color;
65 color = ColorLerp(config.trailColor, config.color, transitionOffset * transitionOffset);
66 // fake a ballista flight path using parabola equation
67 float parabolaT = t - 0.5f;
68 parabolaT = 1.0f - 4.0f * parabolaT * parabolaT;
69 position.y += config.arcFactor * parabolaT * projectile.distance;
70
71 float size = 0.06f * (transitionOffset + 0.25f);
72 DrawCube(position, size, size, size, color);
73 }
74 }
75 }
76
77 void ProjectileUpdate()
78 {
79 for (int i = 0; i < projectileCount; i++)
80 {
81 Projectile *projectile = &projectiles[i];
82 if (projectile->projectileType == PROJECTILE_TYPE_NONE)
83 {
84 continue;
85 }
86 float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime);
87 if (transition >= 1.0f)
88 {
89 projectile->projectileType = PROJECTILE_TYPE_NONE;
90 Enemy *enemy = EnemyTryResolve(projectile->targetEnemy);
91 if (enemy && projectile->hitEffectConfig.pushbackPowerDistance > 0.0f)
92 {
93 Vector2 direction = Vector2Normalize(Vector2Subtract((Vector2){projectile->target.x, projectile->target.z}, enemy->simPosition));
94 enemy->simPosition = Vector2Add(enemy->simPosition, Vector2Scale(direction, projectile->hitEffectConfig.pushbackPowerDistance));
95 }
96
97 if (projectile->hitEffectConfig.areaDamageRadius > 0.0f)
98 {
99 EnemyAddDamageRange((Vector2){projectile->target.x, projectile->target.z}, projectile->hitEffectConfig.areaDamageRadius, projectile->hitEffectConfig.damage);
100 // pancaked sphere explosion
101 float r = projectile->hitEffectConfig.areaDamageRadius;
102 ParticleAdd(PARTICLE_TYPE_EXPLOSION, projectile->target, (Vector3){0}, (Vector3){r, r * 0.2f, r}, 0.33f);
103 }
104 else if (projectile->hitEffectConfig.damage > 0.0f && enemy)
105 {
106 EnemyAddDamage(enemy, projectile->hitEffectConfig.damage);
107 }
108 continue;
109 }
110 }
111 }
112
113 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig)
114 {
115 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
116 {
117 Projectile *projectile = &projectiles[i];
118 if (projectile->projectileType == PROJECTILE_TYPE_NONE)
119 {
120 projectile->projectileType = projectileType;
121 projectile->shootTime = gameTime.time;
122 float distance = Vector3Distance(position, target);
123 projectile->arrivalTime = gameTime.time + distance / speed;
124 projectile->position = position;
125 projectile->target = target;
126 projectile->directionNormal = Vector3Scale(Vector3Subtract(target, position), 1.0f / distance);
127 projectile->distance = distance;
128 projectile->targetEnemy = EnemyGetId(enemy);
129 projectileCount = projectileCount <= i ? i + 1 : projectileCount;
130 projectile->hitEffectConfig = hitEffectConfig;
131 return projectile;
132 }
133 }
134 return 0;
135 }
1 #include "td_main.h"
2 #include <raymath.h>
3 #include <rlgl.h>
4
5 static Particle particles[PARTICLE_MAX_COUNT];
6 static int particleCount = 0;
7
8 void ParticleInit()
9 {
10 for (int i = 0; i < PARTICLE_MAX_COUNT; i++)
11 {
12 particles[i] = (Particle){0};
13 }
14 particleCount = 0;
15 }
16
17 static void DrawExplosionParticle(Particle *particle, float transition)
18 {
19 Vector3 scale = particle->scale;
20 float size = 1.0f * (1.0f - transition);
21 Color startColor = WHITE;
22 Color endColor = RED;
23 Color color = ColorLerp(startColor, endColor, transition);
24
25 rlPushMatrix();
26 rlTranslatef(particle->position.x, particle->position.y, particle->position.z);
27 rlScalef(scale.x, scale.y, scale.z);
28 DrawSphere(Vector3Zero(), size, color);
29 rlPopMatrix();
30 }
31
32 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime)
33 {
34 if (particleCount >= PARTICLE_MAX_COUNT)
35 {
36 return;
37 }
38
39 int index = -1;
40 for (int i = 0; i < particleCount; i++)
41 {
42 if (particles[i].particleType == PARTICLE_TYPE_NONE)
43 {
44 index = i;
45 break;
46 }
47 }
48
49 if (index == -1)
50 {
51 index = particleCount++;
52 }
53
54 Particle *particle = &particles[index];
55 particle->particleType = particleType;
56 particle->spawnTime = gameTime.time;
57 particle->lifetime = lifetime;
58 particle->position = position;
59 particle->velocity = velocity;
60 particle->scale = scale;
61 }
62
63 void ParticleUpdate()
64 {
65 for (int i = 0; i < particleCount; i++)
66 {
67 Particle *particle = &particles[i];
68 if (particle->particleType == PARTICLE_TYPE_NONE)
69 {
70 continue;
71 }
72
73 float age = gameTime.time - particle->spawnTime;
74
75 if (particle->lifetime > age)
76 {
77 particle->position = Vector3Add(particle->position, Vector3Scale(particle->velocity, gameTime.deltaTime));
78 }
79 else {
80 particle->particleType = PARTICLE_TYPE_NONE;
81 }
82 }
83 }
84
85 void ParticleDraw()
86 {
87 for (int i = 0; i < particleCount; i++)
88 {
89 Particle particle = particles[i];
90 if (particle.particleType == PARTICLE_TYPE_NONE)
91 {
92 continue;
93 }
94
95 float age = gameTime.time - particle.spawnTime;
96 float transition = age / particle.lifetime;
97 switch (particle.particleType)
98 {
99 case PARTICLE_TYPE_EXPLOSION:
100 DrawExplosionParticle(&particle, transition);
101 break;
102 default:
103 DrawCube(particle.position, 0.3f, 0.5f, 0.3f, RED);
104 break;
105 }
106 }
107 }
1 #include "raylib.h"
2 #include "preferred_size.h"
3
4 // Since the canvas size is not known at compile time, we need to query it at runtime;
5 // the following platform specific code obtains the canvas size and we will use this
6 // size as the preferred size for the window at init time. We're ignoring here the
7 // possibility of the canvas size changing during runtime - this would require to
8 // poll the canvas size in the game loop or establishing a callback to be notified
9
10 #ifdef PLATFORM_WEB
11 #include <emscripten.h>
12 EMSCRIPTEN_RESULT emscripten_get_element_css_size(const char *target, double *width, double *height);
13
14 void GetPreferredSize(int *screenWidth, int *screenHeight)
15 {
16 double canvasWidth, canvasHeight;
17 emscripten_get_element_css_size("#" CANVAS_NAME, &canvasWidth, &canvasHeight);
18 *screenWidth = (int)canvasWidth;
19 *screenHeight = (int)canvasHeight;
20 TraceLog(LOG_INFO, "preferred size for %s: %d %d", CANVAS_NAME, *screenWidth, *screenHeight);
21 }
22
23 int IsPaused()
24 {
25 const char *js = "(function(){\n"
26 " var canvas = document.getElementById(\"" CANVAS_NAME "\");\n"
27 " var rect = canvas.getBoundingClientRect();\n"
28 " var isVisible = (\n"
29 " rect.top >= 0 &&\n"
30 " rect.left >= 0 &&\n"
31 " rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&\n"
32 " rect.right <= (window.innerWidth || document.documentElement.clientWidth)\n"
33 " );\n"
34 " return isVisible ? 0 : 1;\n"
35 "})()";
36 return emscripten_run_script_int(js);
37 }
38
39 #else
40 void GetPreferredSize(int *screenWidth, int *screenHeight)
41 {
42 *screenWidth = 600;
43 *screenHeight = 240;
44 }
45 int IsPaused()
46 {
47 return 0;
48 }
49 #endif
1 #ifndef PREFERRED_SIZE_H
2 #define PREFERRED_SIZE_H
3
4 void GetPreferredSize(int *screenWidth, int *screenHeight);
5 int IsPaused();
6
7 #endif
This improves the visualization when the path preview is blocked by the towers quite a bit.
One thing on my mind is now, that there could be multiple enemy entry points. Let's test how it will look like when the enemy path previews merge along the way:
1 #include "td_main.h"
2 #include <raymath.h>
3 #include <rlgl.h>
4 #include <stdlib.h>
5 #include <math.h>
6
7 //# Variables
8 GUIState guiState = {0};
9 GameTime gameTime = {0};
10
11 Model floorTileAModel = {0};
12 Model floorTileBModel = {0};
13 Model treeModel[2] = {0};
14 Model firTreeModel[2] = {0};
15 Model rockModels[5] = {0};
16 Model grassPatchModel[1] = {0};
17
18 Model pathArrowModel = {0};
19
20 Texture2D palette, spriteSheet;
21
22 Level levels[] = {
23 [0] = {
24 .state = LEVEL_STATE_BUILDING,
25 .initialGold = 20,
26 .waves[0] = {
27 .enemyType = ENEMY_TYPE_MINION,
28 .wave = 0,
29 .count = 5,
30 .interval = 2.5f,
31 .delay = 1.0f,
32 .spawnPosition = {2, 6},
33 },
34 .waves[1] = {
35 .enemyType = ENEMY_TYPE_MINION,
36 .wave = 0,
37 .count = 5,
38 .interval = 2.5f,
39 .delay = 1.0f,
40 .spawnPosition = {-2, 6},
41 },
42 .waves[2] = {
43 .enemyType = ENEMY_TYPE_MINION,
44 .wave = 1,
45 .count = 20,
46 .interval = 1.5f,
47 .delay = 1.0f,
48 .spawnPosition = {0, 6},
49 },
50 .waves[3] = {
51 .enemyType = ENEMY_TYPE_MINION,
52 .wave = 2,
53 .count = 30,
54 .interval = 1.2f,
55 .delay = 1.0f,
56 .spawnPosition = {0, 6},
57 }
58 },
59 };
60
61 Level *currentLevel = levels;
62
63 //# Game
64
65 static Model LoadGLBModel(char *filename)
66 {
67 Model model = LoadModel(TextFormat("data/%s.glb",filename));
68 for (int i = 0; i < model.materialCount; i++)
69 {
70 model.materials[i].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
71 }
72 return model;
73 }
74
75 void LoadAssets()
76 {
77 // load a sprite sheet that contains all units
78 spriteSheet = LoadTexture("data/spritesheet.png");
79 SetTextureFilter(spriteSheet, TEXTURE_FILTER_BILINEAR);
80
81 // we'll use a palette texture to colorize the all buildings and environment art
82 palette = LoadTexture("data/palette.png");
83 // The texture uses gradients on very small space, so we'll enable bilinear filtering
84 SetTextureFilter(palette, TEXTURE_FILTER_BILINEAR);
85
86 floorTileAModel = LoadGLBModel("floor-tile-a");
87 floorTileBModel = LoadGLBModel("floor-tile-b");
88 treeModel[0] = LoadGLBModel("leaftree-large-1-a");
89 treeModel[1] = LoadGLBModel("leaftree-large-1-b");
90 firTreeModel[0] = LoadGLBModel("firtree-1-a");
91 firTreeModel[1] = LoadGLBModel("firtree-1-b");
92 rockModels[0] = LoadGLBModel("rock-1");
93 rockModels[1] = LoadGLBModel("rock-2");
94 rockModels[2] = LoadGLBModel("rock-3");
95 rockModels[3] = LoadGLBModel("rock-4");
96 rockModels[4] = LoadGLBModel("rock-5");
97 grassPatchModel[0] = LoadGLBModel("grass-patch-1");
98
99 pathArrowModel = LoadGLBModel("direction-arrow-x");
100 }
101
102 void InitLevel(Level *level)
103 {
104 level->seed = (int)(GetTime() * 100.0f);
105
106 TowerInit();
107 EnemyInit();
108 ProjectileInit();
109 ParticleInit();
110 TowerTryAdd(TOWER_TYPE_BASE, 0, 0);
111
112 level->placementMode = 0;
113 level->state = LEVEL_STATE_BUILDING;
114 level->nextState = LEVEL_STATE_NONE;
115 level->playerGold = level->initialGold;
116 level->currentWave = 0;
117
118 Camera *camera = &level->camera;
119 camera->position = (Vector3){4.0f, 8.0f, 8.0f};
120 camera->target = (Vector3){0.0f, 0.0f, 0.0f};
121 camera->up = (Vector3){0.0f, 1.0f, 0.0f};
122 camera->fovy = 10.0f;
123 camera->projection = CAMERA_ORTHOGRAPHIC;
124 }
125
126 void DrawLevelHud(Level *level)
127 {
128 const char *text = TextFormat("Gold: %d", level->playerGold);
129 Font font = GetFontDefault();
130 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 120, 10}, font.baseSize * 2.0f, 2.0f, BLACK);
131 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 122, 8}, font.baseSize * 2.0f, 2.0f, YELLOW);
132 }
133
134 void DrawLevelReportLostWave(Level *level)
135 {
136 BeginMode3D(level->camera);
137 DrawLevelGround(level);
138 TowerDraw();
139 EnemyDraw();
140 ProjectileDraw();
141 ParticleDraw();
142 guiState.isBlocked = 0;
143 EndMode3D();
144
145 TowerDrawHealthBars(level->camera);
146
147 const char *text = "Wave lost";
148 int textWidth = MeasureText(text, 20);
149 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
150
151 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
152 {
153 level->nextState = LEVEL_STATE_RESET;
154 }
155 }
156
157 int HasLevelNextWave(Level *level)
158 {
159 for (int i = 0; i < 10; i++)
160 {
161 EnemyWave *wave = &level->waves[i];
162 if (wave->wave == level->currentWave)
163 {
164 return 1;
165 }
166 }
167 return 0;
168 }
169
170 void DrawLevelReportWonWave(Level *level)
171 {
172 BeginMode3D(level->camera);
173 DrawLevelGround(level);
174 TowerDraw();
175 EnemyDraw();
176 ProjectileDraw();
177 ParticleDraw();
178 guiState.isBlocked = 0;
179 EndMode3D();
180
181 TowerDrawHealthBars(level->camera);
182
183 const char *text = "Wave won";
184 int textWidth = MeasureText(text, 20);
185 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
186
187
188 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
189 {
190 level->nextState = LEVEL_STATE_RESET;
191 }
192
193 if (HasLevelNextWave(level))
194 {
195 if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
196 {
197 level->nextState = LEVEL_STATE_BUILDING;
198 }
199 }
200 else {
201 if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
202 {
203 level->nextState = LEVEL_STATE_WON_LEVEL;
204 }
205 }
206 }
207
208 void DrawBuildingBuildButton(Level *level, int x, int y, int width, int height, uint8_t towerType, const char *name)
209 {
210 static ButtonState buttonStates[8] = {0};
211 int cost = GetTowerCosts(towerType);
212 const char *text = TextFormat("%s: %d", name, cost);
213 buttonStates[towerType].isSelected = level->placementMode == towerType;
214 buttonStates[towerType].isDisabled = level->playerGold < cost;
215 if (Button(text, x, y, width, height, &buttonStates[towerType]))
216 {
217 level->placementMode = buttonStates[towerType].isSelected ? 0 : towerType;
218 }
219 }
220
221 float GetRandomFloat(float min, float max)
222 {
223 int random = GetRandomValue(0, 0xfffffff);
224 return ((float)random / (float)0xfffffff) * (max - min) + min;
225 }
226
227 void DrawLevelGround(Level *level)
228 {
229 // draw checkerboard ground pattern
230 for (int x = -5; x <= 5; x += 1)
231 {
232 for (int y = -5; y <= 5; y += 1)
233 {
234 Model *model = (x + y) % 2 == 0 ? &floorTileAModel : &floorTileBModel;
235 DrawModel(*model, (Vector3){x, 0.0f, y}, 1.0f, WHITE);
236 }
237 }
238
239 int oldSeed = GetRandomValue(0, 0xfffffff);
240 SetRandomSeed(level->seed);
241 // increase probability for trees via duplicated entries
242 Model borderModels[64];
243 int maxRockCount = GetRandomValue(2, 6);
244 int maxTreeCount = GetRandomValue(10, 20);
245 int maxFirTreeCount = GetRandomValue(5, 10);
246 int maxLeafTreeCount = maxTreeCount - maxFirTreeCount;
247 int grassPatchCount = GetRandomValue(5, 30);
248
249 int modelCount = 0;
250 for (int i = 0; i < maxRockCount && modelCount < 63; i++)
251 {
252 borderModels[modelCount++] = rockModels[GetRandomValue(0, 5)];
253 }
254 for (int i = 0; i < maxLeafTreeCount && modelCount < 63; i++)
255 {
256 borderModels[modelCount++] = treeModel[GetRandomValue(0, 1)];
257 }
258 for (int i = 0; i < maxFirTreeCount && modelCount < 63; i++)
259 {
260 borderModels[modelCount++] = firTreeModel[GetRandomValue(0, 1)];
261 }
262 for (int i = 0; i < grassPatchCount && modelCount < 63; i++)
263 {
264 borderModels[modelCount++] = grassPatchModel[0];
265 }
266
267 // draw some objects around the border of the map
268 Vector3 up = {0, 1, 0};
269 // a pseudo random number generator to get the same result every time
270 const float wiggle = 0.75f;
271 const int layerCount = 3;
272 for (int layer = 0; layer < layerCount; layer++)
273 {
274 int layerPos = 6 + layer;
275 for (int x = -6 + layer; x <= 6 + layer; x += 1)
276 {
277 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)],
278 (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, -layerPos + GetRandomFloat(0.0f, wiggle)},
279 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
280 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)],
281 (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, layerPos + GetRandomFloat(0.0f, wiggle)},
282 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
283 }
284
285 for (int z = -5 + layer; z <= 5 + layer; z += 1)
286 {
287 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)],
288 (Vector3){-layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)},
289 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
290 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)],
291 (Vector3){layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)},
292 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
293 }
294 }
295
296 SetRandomSeed(oldSeed);
297 }
298
299 void DrawEnemyPath(Level *level, Color arrowColor)
300 {
301 const int castleX = 0, castleY = 0;
302 const int maxWaypointCount = 200;
303 const float timeStep = 1.0f;
304 Vector3 arrowScale = {0.75f, 0.75f, 0.75f};
305
306 // we start with a time offset to simulate the path,
307 // this way the arrows are animated in a forward moving direction
308 // The time is wrapped around the time step to get a smooth animation
309 float timeOffset = fmodf(GetTime(), timeStep);
310
311 for (int i = 0; i < ENEMY_MAX_WAVE_COUNT; i++)
312 {
313 EnemyWave *wave = &level->waves[i];
314 if (wave->wave != level->currentWave)
315 {
316 continue;
317 }
318
319 // use this dummy enemy to simulate the path
320 Enemy dummy = {
321 .enemyType = ENEMY_TYPE_MINION,
322 .simPosition = (Vector2){wave->spawnPosition.x, wave->spawnPosition.y},
323 .nextX = wave->spawnPosition.x,
324 .nextY = wave->spawnPosition.y,
325 .currentX = wave->spawnPosition.x,
326 .currentY = wave->spawnPosition.y,
327 };
328
329 float deltaTime = timeOffset;
330 for (int j = 0; j < maxWaypointCount; j++)
331 {
332 int waypointPassedCount = 0;
333 Vector2 pos = EnemyGetPosition(&dummy, deltaTime, &dummy.simVelocity, &waypointPassedCount);
334 // after the initial variable starting offset, we use a fixed time step
335 deltaTime = timeStep;
336 dummy.simPosition = pos;
337
338 // Update the dummy's position just like we do in the regular enemy update loop
339 for (int k = 0; k < waypointPassedCount; k++)
340 {
341 dummy.currentX = dummy.nextX;
342 dummy.currentY = dummy.nextY;
343 if (EnemyGetNextPosition(dummy.currentX, dummy.currentY, &dummy.nextX, &dummy.nextY) &&
344 Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
345 {
346 break;
347 }
348 }
349 if (Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
350 {
351 break;
352 }
353
354 // get the angle we need to rotate the arrow model. The velocity is just fine for this.
355 float angle = atan2f(dummy.simVelocity.x, dummy.simVelocity.y) * RAD2DEG - 90.0f;
356 DrawModelEx(pathArrowModel, (Vector3){pos.x, 0.15f, pos.y}, (Vector3){0, 1, 0}, angle, arrowScale, arrowColor);
357 }
358 }
359 }
360
361 void DrawLevelBuildingState(Level *level)
362 {
363 BeginMode3D(level->camera);
364 DrawLevelGround(level);
365
366 int blockedCellCount = 0;
367 Vector2 blockedCells[1];
368 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera);
369 float planeDistance = ray.position.y / -ray.direction.y;
370 float planeX = ray.direction.x * planeDistance + ray.position.x;
371 float planeY = ray.direction.z * planeDistance + ray.position.z;
372 int16_t mapX = (int16_t)floorf(planeX + 0.5f);
373 int16_t mapY = (int16_t)floorf(planeY + 0.5f);
374 if (level->placementMode && !guiState.isBlocked && mapX >= -5 && mapX <= 5 && mapY >= -5 && mapY <= 5)
375 {
376 DrawCubeWires((Vector3){mapX, 0.2f, mapY}, 1.0f, 0.4f, 1.0f, RED);
377 blockedCells[blockedCellCount++] = (Vector2){mapX, mapY};
378 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
379 {
380 if (TowerTryAdd(level->placementMode, mapX, mapY))
381 {
382 level->playerGold -= GetTowerCosts(level->placementMode);
383 level->placementMode = TOWER_TYPE_NONE;
384 }
385 }
386 }
387
388 PathFindingMapUpdate(blockedCellCount, blockedCells);
389 TowerDraw();
390 EnemyDraw();
391 ProjectileDraw();
392 ParticleDraw();
393
394 // disable depth testing for the path arrows
395 // flush the 3D batch to draw the arrows on top of everything
396 rlDrawRenderBatchActive();
397 rlDisableDepthTest();
398 DrawEnemyPath(level, (Color){64, 64, 64, 160});
399
400 rlDrawRenderBatchActive();
401 rlEnableDepthTest();
402 DrawEnemyPath(level, WHITE);
403
404 guiState.isBlocked = 0;
405
406 EndMode3D();
407
408 TowerDrawHealthBars(level->camera);
409
410 static ButtonState buildWallButtonState = {0};
411 static ButtonState buildGunButtonState = {0};
412 buildWallButtonState.isSelected = level->placementMode == TOWER_TYPE_WALL;
413 buildGunButtonState.isSelected = level->placementMode == TOWER_TYPE_ARCHER;
414
415 DrawBuildingBuildButton(level, 10, 10, 110, 30, TOWER_TYPE_WALL, "Wall");
416 DrawBuildingBuildButton(level, 10, 50, 110, 30, TOWER_TYPE_ARCHER, "Archer");
417 DrawBuildingBuildButton(level, 10, 90, 110, 30, TOWER_TYPE_BALLISTA, "Ballista");
418 DrawBuildingBuildButton(level, 10, 130, 110, 30, TOWER_TYPE_CATAPULT, "Catapult");
419
420 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
421 {
422 level->nextState = LEVEL_STATE_RESET;
423 }
424
425 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0))
426 {
427 level->nextState = LEVEL_STATE_BATTLE;
428 }
429
430 const char *text = "Building phase";
431 int textWidth = MeasureText(text, 20);
432 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
433 }
434
435 void InitBattleStateConditions(Level *level)
436 {
437 level->state = LEVEL_STATE_BATTLE;
438 level->nextState = LEVEL_STATE_NONE;
439 level->waveEndTimer = 0.0f;
440 for (int i = 0; i < 10; i++)
441 {
442 EnemyWave *wave = &level->waves[i];
443 wave->spawned = 0;
444 wave->timeToSpawnNext = wave->delay;
445 }
446 }
447
448 void DrawLevelBattleState(Level *level)
449 {
450 BeginMode3D(level->camera);
451 DrawLevelGround(level);
452 TowerDraw();
453 EnemyDraw();
454 ProjectileDraw();
455 ParticleDraw();
456 guiState.isBlocked = 0;
457 EndMode3D();
458
459 EnemyDrawHealthbars(level->camera);
460 TowerDrawHealthBars(level->camera);
461
462 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
463 {
464 level->nextState = LEVEL_STATE_RESET;
465 }
466
467 int maxCount = 0;
468 int remainingCount = 0;
469 for (int i = 0; i < 10; i++)
470 {
471 EnemyWave *wave = &level->waves[i];
472 if (wave->wave != level->currentWave)
473 {
474 continue;
475 }
476 maxCount += wave->count;
477 remainingCount += wave->count - wave->spawned;
478 }
479 int aliveCount = EnemyCount();
480 remainingCount += aliveCount;
481
482 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount);
483 int textWidth = MeasureText(text, 20);
484 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
485 }
486
487 void DrawLevel(Level *level)
488 {
489 switch (level->state)
490 {
491 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break;
492 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break;
493 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break;
494 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break;
495 default: break;
496 }
497
498 DrawLevelHud(level);
499 }
500
501 void UpdateLevel(Level *level)
502 {
503 if (level->state == LEVEL_STATE_BATTLE)
504 {
505 int activeWaves = 0;
506 for (int i = 0; i < 10; i++)
507 {
508 EnemyWave *wave = &level->waves[i];
509 if (wave->spawned >= wave->count || wave->wave != level->currentWave)
510 {
511 continue;
512 }
513 activeWaves++;
514 wave->timeToSpawnNext -= gameTime.deltaTime;
515 if (wave->timeToSpawnNext <= 0.0f)
516 {
517 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y);
518 if (enemy)
519 {
520 wave->timeToSpawnNext = wave->interval;
521 wave->spawned++;
522 }
523 }
524 }
525 if (GetTowerByType(TOWER_TYPE_BASE) == 0) {
526 level->waveEndTimer += gameTime.deltaTime;
527 if (level->waveEndTimer >= 2.0f)
528 {
529 level->nextState = LEVEL_STATE_LOST_WAVE;
530 }
531 }
532 else if (activeWaves == 0 && EnemyCount() == 0)
533 {
534 level->waveEndTimer += gameTime.deltaTime;
535 if (level->waveEndTimer >= 2.0f)
536 {
537 level->nextState = LEVEL_STATE_WON_WAVE;
538 }
539 }
540 }
541
542 PathFindingMapUpdate(0, 0);
543 EnemyUpdate();
544 TowerUpdate();
545 ProjectileUpdate();
546 ParticleUpdate();
547
548 if (level->nextState == LEVEL_STATE_RESET)
549 {
550 InitLevel(level);
551 }
552
553 if (level->nextState == LEVEL_STATE_BATTLE)
554 {
555 InitBattleStateConditions(level);
556 }
557
558 if (level->nextState == LEVEL_STATE_WON_WAVE)
559 {
560 level->currentWave++;
561 level->state = LEVEL_STATE_WON_WAVE;
562 }
563
564 if (level->nextState == LEVEL_STATE_LOST_WAVE)
565 {
566 level->state = LEVEL_STATE_LOST_WAVE;
567 }
568
569 if (level->nextState == LEVEL_STATE_BUILDING)
570 {
571 level->state = LEVEL_STATE_BUILDING;
572 }
573
574 if (level->nextState == LEVEL_STATE_WON_LEVEL)
575 {
576 // make something of this later
577 InitLevel(level);
578 }
579
580 level->nextState = LEVEL_STATE_NONE;
581 }
582
583 float nextSpawnTime = 0.0f;
584
585 void ResetGame()
586 {
587 InitLevel(currentLevel);
588 }
589
590 void InitGame()
591 {
592 TowerInit();
593 EnemyInit();
594 ProjectileInit();
595 ParticleInit();
596 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f);
597
598 currentLevel = levels;
599 InitLevel(currentLevel);
600 }
601
602 //# Immediate GUI functions
603
604 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth)
605 {
606 const float healthBarHeight = 6.0f;
607 const float healthBarOffset = 15.0f;
608 const float inset = 2.0f;
609 const float innerWidth = healthBarWidth - inset * 2;
610 const float innerHeight = healthBarHeight - inset * 2;
611
612 Vector2 screenPos = GetWorldToScreen(position, camera);
613 float centerX = screenPos.x - healthBarWidth * 0.5f;
614 float topY = screenPos.y - healthBarOffset;
615 DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK);
616 float healthWidth = innerWidth * healthRatio;
617 DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor);
618 }
619
620 int Button(const char *text, int x, int y, int width, int height, ButtonState *state)
621 {
622 Rectangle bounds = {x, y, width, height};
623 int isPressed = 0;
624 int isSelected = state && state->isSelected;
625 int isDisabled = state && state->isDisabled;
626 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !isDisabled)
627 {
628 Color color = isSelected ? DARKGRAY : GRAY;
629 DrawRectangle(x, y, width, height, color);
630 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
631 {
632 isPressed = 1;
633 }
634 guiState.isBlocked = 1;
635 }
636 else
637 {
638 Color color = isSelected ? WHITE : LIGHTGRAY;
639 DrawRectangle(x, y, width, height, color);
640 }
641 Font font = GetFontDefault();
642 Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1);
643 Color textColor = isDisabled ? GRAY : BLACK;
644 DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, textColor);
645 return isPressed;
646 }
647
648 //# Main game loop
649
650 void GameUpdate()
651 {
652 float dt = GetFrameTime();
653 // cap maximum delta time to 0.1 seconds to prevent large time steps
654 if (dt > 0.1f) dt = 0.1f;
655 gameTime.time += dt;
656 gameTime.deltaTime = dt;
657
658 UpdateLevel(currentLevel);
659 }
660
661 int main(void)
662 {
663 int screenWidth, screenHeight;
664 GetPreferredSize(&screenWidth, &screenHeight);
665 InitWindow(screenWidth, screenHeight, "Tower defense");
666 SetTargetFPS(30);
667
668 LoadAssets();
669 InitGame();
670
671 while (!WindowShouldClose())
672 {
673 if (IsPaused()) {
674 // canvas is not visible in browser - do nothing
675 continue;
676 }
677
678 BeginDrawing();
679 ClearBackground((Color){0x4E, 0x63, 0x26, 0xFF});
680
681 GameUpdate();
682 DrawLevel(currentLevel);
683
684 EndDrawing();
685 }
686
687 CloseWindow();
688
689 return 0;
690 }
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 #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 #define ENEMY_MAX_PATH_COUNT 8
12 #define ENEMY_MAX_COUNT 400
13 #define ENEMY_TYPE_NONE 0
14 #define ENEMY_TYPE_MINION 1
15
16 #define PARTICLE_MAX_COUNT 400
17 #define PARTICLE_TYPE_NONE 0
18 #define PARTICLE_TYPE_EXPLOSION 1
19
20 typedef struct Particle
21 {
22 uint8_t particleType;
23 float spawnTime;
24 float lifetime;
25 Vector3 position;
26 Vector3 velocity;
27 Vector3 scale;
28 } Particle;
29
30 #define TOWER_MAX_COUNT 400
31 enum TowerType
32 {
33 TOWER_TYPE_NONE,
34 TOWER_TYPE_BASE,
35 TOWER_TYPE_ARCHER,
36 TOWER_TYPE_BALLISTA,
37 TOWER_TYPE_CATAPULT,
38 TOWER_TYPE_WALL,
39 TOWER_TYPE_COUNT
40 };
41
42 typedef struct HitEffectConfig
43 {
44 float damage;
45 float areaDamageRadius;
46 float pushbackPowerDistance;
47 } HitEffectConfig;
48
49 typedef struct TowerTypeConfig
50 {
51 float cooldown;
52 float range;
53 float projectileSpeed;
54
55 uint8_t cost;
56 uint8_t projectileType;
57 uint16_t maxHealth;
58
59 HitEffectConfig hitEffect;
60 } TowerTypeConfig;
61
62 typedef struct Tower
63 {
64 int16_t x, y;
65 uint8_t towerType;
66 Vector2 lastTargetPosition;
67 float cooldown;
68 float damage;
69 } Tower;
70
71 typedef struct GameTime
72 {
73 float time;
74 float deltaTime;
75 } GameTime;
76
77 typedef struct ButtonState {
78 char isSelected;
79 char isDisabled;
80 } ButtonState;
81
82 typedef struct GUIState {
83 int isBlocked;
84 } GUIState;
85
86 typedef enum LevelState
87 {
88 LEVEL_STATE_NONE,
89 LEVEL_STATE_BUILDING,
90 LEVEL_STATE_BATTLE,
91 LEVEL_STATE_WON_WAVE,
92 LEVEL_STATE_LOST_WAVE,
93 LEVEL_STATE_WON_LEVEL,
94 LEVEL_STATE_RESET,
95 } LevelState;
96
97 typedef struct EnemyWave {
98 uint8_t enemyType;
99 uint8_t wave;
100 uint16_t count;
101 float interval;
102 float delay;
103 Vector2 spawnPosition;
104
105 uint16_t spawned;
106 float timeToSpawnNext;
107 } EnemyWave;
108
109 #define ENEMY_MAX_WAVE_COUNT 10
110
111 typedef struct Level
112 {
113 int seed;
114 LevelState state;
115 LevelState nextState;
116 Camera3D camera;
117 int placementMode;
118
119 int initialGold;
120 int playerGold;
121
122 EnemyWave waves[ENEMY_MAX_WAVE_COUNT];
123 int currentWave;
124 float waveEndTimer;
125 } Level;
126
127 typedef struct DeltaSrc
128 {
129 char x, y;
130 } DeltaSrc;
131
132 typedef struct PathfindingMap
133 {
134 int width, height;
135 float scale;
136 float *distances;
137 long *towerIndex;
138 DeltaSrc *deltaSrc;
139 float maxDistance;
140 Matrix toMapSpace;
141 Matrix toWorldSpace;
142 } PathfindingMap;
143
144 // when we execute the pathfinding algorithm, we need to store the active nodes
145 // in a queue. Each node has a position, a distance from the start, and the
146 // position of the node that we came from.
147 typedef struct PathfindingNode
148 {
149 int16_t x, y, fromX, fromY;
150 float distance;
151 } PathfindingNode;
152
153 typedef struct EnemyId
154 {
155 uint16_t index;
156 uint16_t generation;
157 } EnemyId;
158
159 typedef struct EnemyClassConfig
160 {
161 float speed;
162 float health;
163 float radius;
164 float maxAcceleration;
165 float requiredContactTime;
166 float explosionDamage;
167 float explosionRange;
168 float explosionPushbackPower;
169 int goldValue;
170 } EnemyClassConfig;
171
172 typedef struct Enemy
173 {
174 int16_t currentX, currentY;
175 int16_t nextX, nextY;
176 Vector2 simPosition;
177 Vector2 simVelocity;
178 uint16_t generation;
179 float walkedDistance;
180 float startMovingTime;
181 float damage, futureDamage;
182 float contactTime;
183 uint8_t enemyType;
184 uint8_t movePathCount;
185 Vector2 movePath[ENEMY_MAX_PATH_COUNT];
186 } Enemy;
187
188 // a unit that uses sprites to be drawn
189 #define SPRITE_UNIT_PHASE_WEAPON_IDLE 0
190 #define SPRITE_UNIT_PHASE_WEAPON_COOLDOWN 1
191 typedef struct SpriteUnit
192 {
193 Rectangle srcRect;
194 Vector2 offset;
195 int frameCount;
196 float frameDuration;
197 Rectangle srcWeaponIdleRect;
198 Vector2 srcWeaponIdleOffset;
199 Rectangle srcWeaponCooldownRect;
200 Vector2 srcWeaponCooldownOffset;
201 } SpriteUnit;
202
203 #define PROJECTILE_MAX_COUNT 1200
204 #define PROJECTILE_TYPE_NONE 0
205 #define PROJECTILE_TYPE_ARROW 1
206 #define PROJECTILE_TYPE_CATAPULT 2
207 #define PROJECTILE_TYPE_BALLISTA 3
208
209 typedef struct Projectile
210 {
211 uint8_t projectileType;
212 float shootTime;
213 float arrivalTime;
214 float distance;
215 Vector3 position;
216 Vector3 target;
217 Vector3 directionNormal;
218 EnemyId targetEnemy;
219 HitEffectConfig hitEffectConfig;
220 } Projectile;
221
222 //# Function declarations
223 float TowerGetMaxHealth(Tower *tower);
224 int Button(const char *text, int x, int y, int width, int height, ButtonState *state);
225 int EnemyAddDamageRange(Vector2 position, float range, float damage);
226 int EnemyAddDamage(Enemy *enemy, float damage);
227
228 //# Enemy functions
229 void EnemyInit();
230 void EnemyDraw();
231 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource);
232 void EnemyUpdate();
233 float EnemyGetCurrentMaxSpeed(Enemy *enemy);
234 float EnemyGetMaxHealth(Enemy *enemy);
235 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY);
236 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount);
237 EnemyId EnemyGetId(Enemy *enemy);
238 Enemy *EnemyTryResolve(EnemyId enemyId);
239 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY);
240 int EnemyAddDamage(Enemy *enemy, float damage);
241 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range);
242 int EnemyCount();
243 void EnemyDrawHealthbars(Camera3D camera);
244
245 //# Tower functions
246 void TowerInit();
247 Tower *TowerGetAt(int16_t x, int16_t y);
248 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y);
249 Tower *GetTowerByType(uint8_t towerType);
250 int GetTowerCosts(uint8_t towerType);
251 float TowerGetMaxHealth(Tower *tower);
252 void TowerDraw();
253 void TowerUpdate();
254 void TowerDrawHealthBars(Camera3D camera);
255 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase);
256
257 //# Particles
258 void ParticleInit();
259 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime);
260 void ParticleUpdate();
261 void ParticleDraw();
262
263 //# Projectiles
264 void ProjectileInit();
265 void ProjectileDraw();
266 void ProjectileUpdate();
267 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig);
268
269 //# Pathfinding map
270 void PathfindingMapInit(int width, int height, Vector3 translate, float scale);
271 float PathFindingGetDistance(int mapX, int mapY);
272 Vector2 PathFindingGetGradient(Vector3 world);
273 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY);
274 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells);
275 void PathFindingMapDraw();
276
277 //# UI
278 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth);
279
280 //# Level
281 void DrawLevelGround(Level *level);
282 void DrawEnemyPath(Level *level, Color arrowColor);
283
284 //# variables
285 extern Level *currentLevel;
286 extern Enemy enemies[ENEMY_MAX_COUNT];
287 extern int enemyCount;
288 extern EnemyClassConfig enemyClassConfigs[];
289
290 extern GUIState guiState;
291 extern GameTime gameTime;
292 extern Tower towers[TOWER_MAX_COUNT];
293 extern int towerCount;
294
295 extern Texture2D palette, spriteSheet;
296
297 #endif
1 #include "td_main.h"
2 #include <raymath.h>
3
4 static TowerTypeConfig towerTypeConfigs[TOWER_TYPE_COUNT] = {
5 [TOWER_TYPE_BASE] = {
6 .maxHealth = 10,
7 },
8 [TOWER_TYPE_ARCHER] = {
9 .cooldown = 0.5f,
10 .range = 3.0f,
11 .cost = 6,
12 .maxHealth = 10,
13 .projectileSpeed = 4.0f,
14 .projectileType = PROJECTILE_TYPE_ARROW,
15 .hitEffect = {
16 .damage = 3.0f,
17 }
18 },
19 [TOWER_TYPE_BALLISTA] = {
20 .cooldown = 1.5f,
21 .range = 6.0f,
22 .cost = 9,
23 .maxHealth = 10,
24 .projectileSpeed = 10.0f,
25 .projectileType = PROJECTILE_TYPE_BALLISTA,
26 .hitEffect = {
27 .damage = 6.0f,
28 .pushbackPowerDistance = 0.25f,
29 }
30 },
31 [TOWER_TYPE_CATAPULT] = {
32 .cooldown = 1.7f,
33 .range = 5.0f,
34 .cost = 10,
35 .maxHealth = 10,
36 .projectileSpeed = 3.0f,
37 .projectileType = PROJECTILE_TYPE_CATAPULT,
38 .hitEffect = {
39 .damage = 2.0f,
40 .areaDamageRadius = 1.75f,
41 }
42 },
43 [TOWER_TYPE_WALL] = {
44 .cost = 2,
45 .maxHealth = 10,
46 },
47 };
48
49 Tower towers[TOWER_MAX_COUNT];
50 int towerCount = 0;
51
52 Model towerModels[TOWER_TYPE_COUNT];
53
54 // definition of our archer unit
55 SpriteUnit archerUnit = {
56 .srcRect = {0, 0, 16, 16},
57 .offset = {7, 1},
58 .frameCount = 1,
59 .frameDuration = 0.0f,
60 .srcWeaponIdleRect = {16, 0, 6, 16},
61 .srcWeaponIdleOffset = {8, 0},
62 .srcWeaponCooldownRect = {22, 0, 11, 16},
63 .srcWeaponCooldownOffset = {10, 0},
64 };
65
66 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase)
67 {
68 float xScale = flip ? -1.0f : 1.0f;
69 Camera3D camera = currentLevel->camera;
70 float size = 0.5f;
71 Vector2 offset = (Vector2){ unit.offset.x / 16.0f * size, unit.offset.y / 16.0f * size * xScale };
72 Vector2 scale = (Vector2){ unit.srcRect.width / 16.0f * size, unit.srcRect.height / 16.0f * size };
73 // we want the sprite to face the camera, so we need to calculate the up vector
74 Vector3 forward = Vector3Subtract(camera.target, camera.position);
75 Vector3 up = {0, 1, 0};
76 Vector3 right = Vector3CrossProduct(forward, up);
77 up = Vector3Normalize(Vector3CrossProduct(right, forward));
78
79 Rectangle srcRect = unit.srcRect;
80 if (unit.frameCount > 1)
81 {
82 srcRect.x += (int)(t / unit.frameDuration) % unit.frameCount * srcRect.width;
83 }
84 if (flip)
85 {
86 srcRect.x += srcRect.width;
87 srcRect.width = -srcRect.width;
88 }
89 DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
90
91 if (phase == SPRITE_UNIT_PHASE_WEAPON_COOLDOWN && unit.srcWeaponCooldownRect.width > 0)
92 {
93 offset = (Vector2){ unit.srcWeaponCooldownOffset.x / 16.0f * size, unit.srcWeaponCooldownOffset.y / 16.0f * size };
94 scale = (Vector2){ unit.srcWeaponCooldownRect.width / 16.0f * size, unit.srcWeaponCooldownRect.height / 16.0f * size };
95 srcRect = unit.srcWeaponCooldownRect;
96 if (flip)
97 {
98 // position.x = flip * scale.x * 0.5f;
99 srcRect.x += srcRect.width;
100 srcRect.width = -srcRect.width;
101 offset.x = scale.x - offset.x;
102 }
103 DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
104 }
105 else if (phase == SPRITE_UNIT_PHASE_WEAPON_IDLE && unit.srcWeaponIdleRect.width > 0)
106 {
107 offset = (Vector2){ unit.srcWeaponIdleOffset.x / 16.0f * size, unit.srcWeaponIdleOffset.y / 16.0f * size };
108 scale = (Vector2){ unit.srcWeaponIdleRect.width / 16.0f * size, unit.srcWeaponIdleRect.height / 16.0f * size };
109 srcRect = unit.srcWeaponIdleRect;
110 if (flip)
111 {
112 // position.x = flip * scale.x * 0.5f;
113 srcRect.x += srcRect.width;
114 srcRect.width = -srcRect.width;
115 offset.x = scale.x - offset.x;
116 }
117 DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
118 }
119 }
120
121 void TowerInit()
122 {
123 for (int i = 0; i < TOWER_MAX_COUNT; i++)
124 {
125 towers[i] = (Tower){0};
126 }
127 towerCount = 0;
128
129 towerModels[TOWER_TYPE_BASE] = LoadModel("data/keep.glb");
130 towerModels[TOWER_TYPE_WALL] = LoadModel("data/wall-0000.glb");
131
132 for (int i = 0; i < TOWER_TYPE_COUNT; i++)
133 {
134 if (towerModels[i].materials)
135 {
136 // assign the palette texture to the material of the model (0 is not used afaik)
137 towerModels[i].materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
138 }
139 }
140 }
141
142 static void TowerGunUpdate(Tower *tower)
143 {
144 TowerTypeConfig config = towerTypeConfigs[tower->towerType];
145 if (tower->cooldown <= 0.0f)
146 {
147 Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, config.range);
148 if (enemy)
149 {
150 tower->cooldown = config.cooldown;
151 // shoot the enemy; determine future position of the enemy
152 float bulletSpeed = config.projectileSpeed;
153 Vector2 velocity = enemy->simVelocity;
154 Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0);
155 Vector2 towerPosition = {tower->x, tower->y};
156 float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed;
157 for (int i = 0; i < 8; i++) {
158 velocity = enemy->simVelocity;
159 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0);
160 float distance = Vector2Distance(towerPosition, futurePosition);
161 float eta2 = distance / bulletSpeed;
162 if (fabs(eta - eta2) < 0.01f) {
163 break;
164 }
165 eta = (eta2 + eta) * 0.5f;
166 }
167
168 ProjectileTryAdd(config.projectileType, enemy,
169 (Vector3){towerPosition.x, 1.33f, towerPosition.y},
170 (Vector3){futurePosition.x, 0.25f, futurePosition.y},
171 bulletSpeed, config.hitEffect);
172 enemy->futureDamage += config.hitEffect.damage;
173 tower->lastTargetPosition = futurePosition;
174 }
175 }
176 else
177 {
178 tower->cooldown -= gameTime.deltaTime;
179 }
180 }
181
182 Tower *TowerGetAt(int16_t x, int16_t y)
183 {
184 for (int i = 0; i < towerCount; i++)
185 {
186 if (towers[i].x == x && towers[i].y == y && towers[i].towerType != TOWER_TYPE_NONE)
187 {
188 return &towers[i];
189 }
190 }
191 return 0;
192 }
193
194 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y)
195 {
196 if (towerCount >= TOWER_MAX_COUNT)
197 {
198 return 0;
199 }
200
201 Tower *tower = TowerGetAt(x, y);
202 if (tower)
203 {
204 return 0;
205 }
206
207 tower = &towers[towerCount++];
208 tower->x = x;
209 tower->y = y;
210 tower->towerType = towerType;
211 tower->cooldown = 0.0f;
212 tower->damage = 0.0f;
213 return tower;
214 }
215
216 Tower *GetTowerByType(uint8_t towerType)
217 {
218 for (int i = 0; i < towerCount; i++)
219 {
220 if (towers[i].towerType == towerType)
221 {
222 return &towers[i];
223 }
224 }
225 return 0;
226 }
227
228 int GetTowerCosts(uint8_t towerType)
229 {
230 return towerTypeConfigs[towerType].cost;
231 }
232
233 float TowerGetMaxHealth(Tower *tower)
234 {
235 return towerTypeConfigs[tower->towerType].maxHealth;
236 }
237
238 void TowerDraw()
239 {
240 for (int i = 0; i < towerCount; i++)
241 {
242 Tower tower = towers[i];
243 if (tower.towerType == TOWER_TYPE_NONE)
244 {
245 continue;
246 }
247
248 switch (tower.towerType)
249 {
250 case TOWER_TYPE_ARCHER:
251 {
252 Vector2 screenPosTower = GetWorldToScreen((Vector3){tower.x, 0.0f, tower.y}, currentLevel->camera);
253 Vector2 screenPosTarget = GetWorldToScreen((Vector3){tower.lastTargetPosition.x, 0.0f, tower.lastTargetPosition.y}, currentLevel->camera);
254 DrawModel(towerModels[TOWER_TYPE_WALL], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
255 DrawSpriteUnit(archerUnit, (Vector3){tower.x, 1.0f, tower.y}, 0, screenPosTarget.x > screenPosTower.x,
256 tower.cooldown > 0.2f ? SPRITE_UNIT_PHASE_WEAPON_COOLDOWN : SPRITE_UNIT_PHASE_WEAPON_IDLE);
257 }
258 break;
259 case TOWER_TYPE_BALLISTA:
260 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, BROWN);
261 break;
262 case TOWER_TYPE_CATAPULT:
263 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, DARKGRAY);
264 break;
265 default:
266 if (towerModels[tower.towerType].materials)
267 {
268 DrawModel(towerModels[tower.towerType], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
269 } else {
270 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY);
271 }
272 break;
273 }
274 }
275 }
276
277 void TowerUpdate()
278 {
279 for (int i = 0; i < towerCount; i++)
280 {
281 Tower *tower = &towers[i];
282 switch (tower->towerType)
283 {
284 case TOWER_TYPE_CATAPULT:
285 case TOWER_TYPE_BALLISTA:
286 case TOWER_TYPE_ARCHER:
287 TowerGunUpdate(tower);
288 break;
289 }
290 }
291 }
292
293 void TowerDrawHealthBars(Camera3D camera)
294 {
295 for (int i = 0; i < towerCount; i++)
296 {
297 Tower *tower = &towers[i];
298 if (tower->towerType == TOWER_TYPE_NONE || tower->damage <= 0.0f)
299 {
300 continue;
301 }
302
303 Vector3 position = (Vector3){tower->x, 0.5f, tower->y};
304 float maxHealth = TowerGetMaxHealth(tower);
305 float health = maxHealth - tower->damage;
306 float healthRatio = health / maxHealth;
307
308 DrawHealthBar(camera, position, healthRatio, GREEN, 35.0f);
309 }
310 }
1 #include "td_main.h"
2 #include <raymath.h>
3 #include <stdlib.h>
4 #include <math.h>
5
6 EnemyClassConfig enemyClassConfigs[] = {
7 [ENEMY_TYPE_MINION] = {
8 .health = 10.0f,
9 .speed = 0.6f,
10 .radius = 0.25f,
11 .maxAcceleration = 1.0f,
12 .explosionDamage = 1.0f,
13 .requiredContactTime = 0.5f,
14 .explosionRange = 1.0f,
15 .explosionPushbackPower = 0.25f,
16 .goldValue = 1,
17 },
18 };
19
20 Enemy enemies[ENEMY_MAX_COUNT];
21 int enemyCount = 0;
22
23 SpriteUnit enemySprites[] = {
24 [ENEMY_TYPE_MINION] = {
25 .srcRect = {0, 16, 16, 16},
26 .offset = {8.0f, 0.0f},
27 .frameCount = 6,
28 .frameDuration = 0.1f,
29 },
30 };
31
32 void EnemyInit()
33 {
34 for (int i = 0; i < ENEMY_MAX_COUNT; i++)
35 {
36 enemies[i] = (Enemy){0};
37 }
38 enemyCount = 0;
39 }
40
41 float EnemyGetCurrentMaxSpeed(Enemy *enemy)
42 {
43 return enemyClassConfigs[enemy->enemyType].speed;
44 }
45
46 float EnemyGetMaxHealth(Enemy *enemy)
47 {
48 return enemyClassConfigs[enemy->enemyType].health;
49 }
50
51 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY)
52 {
53 int16_t castleX = 0;
54 int16_t castleY = 0;
55 int16_t dx = castleX - currentX;
56 int16_t dy = castleY - currentY;
57 if (dx == 0 && dy == 0)
58 {
59 *nextX = currentX;
60 *nextY = currentY;
61 return 1;
62 }
63 Vector2 gradient = PathFindingGetGradient((Vector3){currentX, 0, currentY});
64
65 if (gradient.x == 0 && gradient.y == 0)
66 {
67 *nextX = currentX;
68 *nextY = currentY;
69 return 1;
70 }
71
72 if (fabsf(gradient.x) > fabsf(gradient.y))
73 {
74 *nextX = currentX + (int16_t)(gradient.x > 0.0f ? 1 : -1);
75 *nextY = currentY;
76 return 0;
77 }
78 *nextX = currentX;
79 *nextY = currentY + (int16_t)(gradient.y > 0.0f ? 1 : -1);
80 return 0;
81 }
82
83
84 // this function predicts the movement of the unit for the next deltaT seconds
85 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount)
86 {
87 const float pointReachedDistance = 0.25f;
88 const float pointReachedDistance2 = pointReachedDistance * pointReachedDistance;
89 const float maxSimStepTime = 0.015625f;
90
91 float maxAcceleration = enemyClassConfigs[enemy->enemyType].maxAcceleration;
92 float maxSpeed = EnemyGetCurrentMaxSpeed(enemy);
93 int16_t nextX = enemy->nextX;
94 int16_t nextY = enemy->nextY;
95 Vector2 position = enemy->simPosition;
96 int passedCount = 0;
97 for (float t = 0.0f; t < deltaT; t += maxSimStepTime)
98 {
99 float stepTime = fminf(deltaT - t, maxSimStepTime);
100 Vector2 target = (Vector2){nextX, nextY};
101 float speed = Vector2Length(*velocity);
102 // draw the target position for debugging
103 //DrawCubeWires((Vector3){target.x, 0.2f, target.y}, 0.1f, 0.4f, 0.1f, RED);
104 Vector2 lookForwardPos = Vector2Add(position, Vector2Scale(*velocity, speed));
105 if (Vector2DistanceSqr(target, lookForwardPos) <= pointReachedDistance2)
106 {
107 // we reached the target position, let's move to the next waypoint
108 EnemyGetNextPosition(nextX, nextY, &nextX, &nextY);
109 target = (Vector2){nextX, nextY};
110 // track how many waypoints we passed
111 passedCount++;
112 }
113
114 // acceleration towards the target
115 Vector2 unitDirection = Vector2Normalize(Vector2Subtract(target, lookForwardPos));
116 Vector2 acceleration = Vector2Scale(unitDirection, maxAcceleration * stepTime);
117 *velocity = Vector2Add(*velocity, acceleration);
118
119 // limit the speed to the maximum speed
120 if (speed > maxSpeed)
121 {
122 *velocity = Vector2Scale(*velocity, maxSpeed / speed);
123 }
124
125 // move the enemy
126 position = Vector2Add(position, Vector2Scale(*velocity, stepTime));
127 }
128
129 if (waypointPassedCount)
130 {
131 (*waypointPassedCount) = passedCount;
132 }
133
134 return position;
135 }
136
137 void EnemyDraw()
138 {
139 for (int i = 0; i < enemyCount; i++)
140 {
141 Enemy enemy = enemies[i];
142 if (enemy.enemyType == ENEMY_TYPE_NONE)
143 {
144 continue;
145 }
146
147 Vector2 position = EnemyGetPosition(&enemy, gameTime.time - enemy.startMovingTime, &enemy.simVelocity, 0);
148
149 // don't draw any trails for now; might replace this with footprints later
150 // if (enemy.movePathCount > 0)
151 // {
152 // Vector3 p = {enemy.movePath[0].x, 0.2f, enemy.movePath[0].y};
153 // DrawLine3D(p, (Vector3){position.x, 0.2f, position.y}, GREEN);
154 // }
155 // for (int j = 1; j < enemy.movePathCount; j++)
156 // {
157 // Vector3 p = {enemy.movePath[j - 1].x, 0.2f, enemy.movePath[j - 1].y};
158 // Vector3 q = {enemy.movePath[j].x, 0.2f, enemy.movePath[j].y};
159 // DrawLine3D(p, q, GREEN);
160 // }
161
162 switch (enemy.enemyType)
163 {
164 case ENEMY_TYPE_MINION:
165 DrawSpriteUnit(enemySprites[ENEMY_TYPE_MINION], (Vector3){position.x, 0.0f, position.y},
166 enemy.walkedDistance, 0, 0);
167 break;
168 }
169 }
170 }
171
172 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource)
173 {
174 // damage the tower
175 float explosionDamge = enemyClassConfigs[enemy->enemyType].explosionDamage;
176 float explosionRange = enemyClassConfigs[enemy->enemyType].explosionRange;
177 float explosionPushbackPower = enemyClassConfigs[enemy->enemyType].explosionPushbackPower;
178 float explosionRange2 = explosionRange * explosionRange;
179 tower->damage += enemyClassConfigs[enemy->enemyType].explosionDamage;
180 // explode the enemy
181 if (tower->damage >= TowerGetMaxHealth(tower))
182 {
183 tower->towerType = TOWER_TYPE_NONE;
184 }
185
186 ParticleAdd(PARTICLE_TYPE_EXPLOSION,
187 explosionSource,
188 (Vector3){0, 0.1f, 0}, (Vector3){1.0f, 1.0f, 1.0f}, 1.0f);
189
190 enemy->enemyType = ENEMY_TYPE_NONE;
191
192 // push back enemies & dealing damage
193 for (int i = 0; i < enemyCount; i++)
194 {
195 Enemy *other = &enemies[i];
196 if (other->enemyType == ENEMY_TYPE_NONE)
197 {
198 continue;
199 }
200 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, other->simPosition);
201 if (distanceSqr > 0 && distanceSqr < explosionRange2)
202 {
203 Vector2 direction = Vector2Normalize(Vector2Subtract(other->simPosition, enemy->simPosition));
204 other->simPosition = Vector2Add(other->simPosition, Vector2Scale(direction, explosionPushbackPower));
205 EnemyAddDamage(other, explosionDamge);
206 }
207 }
208 }
209
210 void EnemyUpdate()
211 {
212 const float castleX = 0;
213 const float castleY = 0;
214 const float maxPathDistance2 = 0.25f * 0.25f;
215
216 for (int i = 0; i < enemyCount; i++)
217 {
218 Enemy *enemy = &enemies[i];
219 if (enemy->enemyType == ENEMY_TYPE_NONE)
220 {
221 continue;
222 }
223
224 int waypointPassedCount = 0;
225 Vector2 prevPosition = enemy->simPosition;
226 enemy->simPosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &enemy->simVelocity, &waypointPassedCount);
227 enemy->startMovingTime = gameTime.time;
228 enemy->walkedDistance += Vector2Distance(prevPosition, enemy->simPosition);
229 // track path of unit
230 if (enemy->movePathCount == 0 || Vector2DistanceSqr(enemy->simPosition, enemy->movePath[0]) > maxPathDistance2)
231 {
232 for (int j = ENEMY_MAX_PATH_COUNT - 1; j > 0; j--)
233 {
234 enemy->movePath[j] = enemy->movePath[j - 1];
235 }
236 enemy->movePath[0] = enemy->simPosition;
237 if (++enemy->movePathCount > ENEMY_MAX_PATH_COUNT)
238 {
239 enemy->movePathCount = ENEMY_MAX_PATH_COUNT;
240 }
241 }
242
243 if (waypointPassedCount > 0)
244 {
245 enemy->currentX = enemy->nextX;
246 enemy->currentY = enemy->nextY;
247 if (EnemyGetNextPosition(enemy->currentX, enemy->currentY, &enemy->nextX, &enemy->nextY) &&
248 Vector2DistanceSqr(enemy->simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
249 {
250 // enemy reached the castle; remove it
251 enemy->enemyType = ENEMY_TYPE_NONE;
252 continue;
253 }
254 }
255 }
256
257 // handle collisions between enemies
258 for (int i = 0; i < enemyCount - 1; i++)
259 {
260 Enemy *enemyA = &enemies[i];
261 if (enemyA->enemyType == ENEMY_TYPE_NONE)
262 {
263 continue;
264 }
265 for (int j = i + 1; j < enemyCount; j++)
266 {
267 Enemy *enemyB = &enemies[j];
268 if (enemyB->enemyType == ENEMY_TYPE_NONE)
269 {
270 continue;
271 }
272 float distanceSqr = Vector2DistanceSqr(enemyA->simPosition, enemyB->simPosition);
273 float radiusA = enemyClassConfigs[enemyA->enemyType].radius;
274 float radiusB = enemyClassConfigs[enemyB->enemyType].radius;
275 float radiusSum = radiusA + radiusB;
276 if (distanceSqr < radiusSum * radiusSum && distanceSqr > 0.001f)
277 {
278 // collision
279 float distance = sqrtf(distanceSqr);
280 float overlap = radiusSum - distance;
281 // move the enemies apart, but softly; if we have a clog of enemies,
282 // moving them perfectly apart can cause them to jitter
283 float positionCorrection = overlap / 5.0f;
284 Vector2 direction = (Vector2){
285 (enemyB->simPosition.x - enemyA->simPosition.x) / distance * positionCorrection,
286 (enemyB->simPosition.y - enemyA->simPosition.y) / distance * positionCorrection};
287 enemyA->simPosition = Vector2Subtract(enemyA->simPosition, direction);
288 enemyB->simPosition = Vector2Add(enemyB->simPosition, direction);
289 }
290 }
291 }
292
293 // handle collisions between enemies and towers
294 for (int i = 0; i < enemyCount; i++)
295 {
296 Enemy *enemy = &enemies[i];
297 if (enemy->enemyType == ENEMY_TYPE_NONE)
298 {
299 continue;
300 }
301 enemy->contactTime -= gameTime.deltaTime;
302 if (enemy->contactTime < 0.0f)
303 {
304 enemy->contactTime = 0.0f;
305 }
306
307 float enemyRadius = enemyClassConfigs[enemy->enemyType].radius;
308 // linear search over towers; could be optimized by using path finding tower map,
309 // but for now, we keep it simple
310 for (int j = 0; j < towerCount; j++)
311 {
312 Tower *tower = &towers[j];
313 if (tower->towerType == TOWER_TYPE_NONE)
314 {
315 continue;
316 }
317 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){tower->x, tower->y});
318 float combinedRadius = enemyRadius + 0.708; // sqrt(0.5^2 + 0.5^2), corner-center distance of square with side length 1
319 if (distanceSqr > combinedRadius * combinedRadius)
320 {
321 continue;
322 }
323 // potential collision; square / circle intersection
324 float dx = tower->x - enemy->simPosition.x;
325 float dy = tower->y - enemy->simPosition.y;
326 float absDx = fabsf(dx);
327 float absDy = fabsf(dy);
328 Vector3 contactPoint = {0};
329 if (absDx <= 0.5f && absDx <= absDy) {
330 // vertical collision; push the enemy out horizontally
331 float overlap = enemyRadius + 0.5f - absDy;
332 if (overlap < 0.0f)
333 {
334 continue;
335 }
336 float direction = dy > 0.0f ? -1.0f : 1.0f;
337 enemy->simPosition.y += direction * overlap;
338 contactPoint = (Vector3){enemy->simPosition.x, 0.2f, tower->y + direction * 0.5f};
339 }
340 else if (absDy <= 0.5f && absDy <= absDx)
341 {
342 // horizontal collision; push the enemy out vertically
343 float overlap = enemyRadius + 0.5f - absDx;
344 if (overlap < 0.0f)
345 {
346 continue;
347 }
348 float direction = dx > 0.0f ? -1.0f : 1.0f;
349 enemy->simPosition.x += direction * overlap;
350 contactPoint = (Vector3){tower->x + direction * 0.5f, 0.2f, enemy->simPosition.y};
351 }
352 else
353 {
354 // possible collision with a corner
355 float cornerDX = dx > 0.0f ? -0.5f : 0.5f;
356 float cornerDY = dy > 0.0f ? -0.5f : 0.5f;
357 float cornerX = tower->x + cornerDX;
358 float cornerY = tower->y + cornerDY;
359 float cornerDistanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){cornerX, cornerY});
360 if (cornerDistanceSqr > enemyRadius * enemyRadius)
361 {
362 continue;
363 }
364 // push the enemy out along the diagonal
365 float cornerDistance = sqrtf(cornerDistanceSqr);
366 float overlap = enemyRadius - cornerDistance;
367 float directionX = cornerDistance > 0.0f ? (cornerX - enemy->simPosition.x) / cornerDistance : -cornerDX;
368 float directionY = cornerDistance > 0.0f ? (cornerY - enemy->simPosition.y) / cornerDistance : -cornerDY;
369 enemy->simPosition.x -= directionX * overlap;
370 enemy->simPosition.y -= directionY * overlap;
371 contactPoint = (Vector3){cornerX, 0.2f, cornerY};
372 }
373
374 if (enemyClassConfigs[enemy->enemyType].explosionDamage > 0.0f)
375 {
376 enemy->contactTime += gameTime.deltaTime * 2.0f; // * 2 to undo the subtraction above
377 if (enemy->contactTime >= enemyClassConfigs[enemy->enemyType].requiredContactTime)
378 {
379 EnemyTriggerExplode(enemy, tower, contactPoint);
380 }
381 }
382 }
383 }
384 }
385
386 EnemyId EnemyGetId(Enemy *enemy)
387 {
388 return (EnemyId){enemy - enemies, enemy->generation};
389 }
390
391 Enemy *EnemyTryResolve(EnemyId enemyId)
392 {
393 if (enemyId.index >= ENEMY_MAX_COUNT)
394 {
395 return 0;
396 }
397 Enemy *enemy = &enemies[enemyId.index];
398 if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE)
399 {
400 return 0;
401 }
402 return enemy;
403 }
404
405 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY)
406 {
407 Enemy *spawn = 0;
408 for (int i = 0; i < enemyCount; i++)
409 {
410 Enemy *enemy = &enemies[i];
411 if (enemy->enemyType == ENEMY_TYPE_NONE)
412 {
413 spawn = enemy;
414 break;
415 }
416 }
417
418 if (enemyCount < ENEMY_MAX_COUNT && !spawn)
419 {
420 spawn = &enemies[enemyCount++];
421 }
422
423 if (spawn)
424 {
425 spawn->currentX = currentX;
426 spawn->currentY = currentY;
427 spawn->nextX = currentX;
428 spawn->nextY = currentY;
429 spawn->simPosition = (Vector2){currentX, currentY};
430 spawn->simVelocity = (Vector2){0, 0};
431 spawn->enemyType = enemyType;
432 spawn->startMovingTime = gameTime.time;
433 spawn->damage = 0.0f;
434 spawn->futureDamage = 0.0f;
435 spawn->generation++;
436 spawn->movePathCount = 0;
437 spawn->walkedDistance = 0.0f;
438 }
439
440 return spawn;
441 }
442
443 int EnemyAddDamageRange(Vector2 position, float range, float damage)
444 {
445 int count = 0;
446 float range2 = range * range;
447 for (int i = 0; i < enemyCount; i++)
448 {
449 Enemy *enemy = &enemies[i];
450 if (enemy->enemyType == ENEMY_TYPE_NONE)
451 {
452 continue;
453 }
454 float distance2 = Vector2DistanceSqr(position, enemy->simPosition);
455 if (distance2 <= range2)
456 {
457 EnemyAddDamage(enemy, damage);
458 count++;
459 }
460 }
461 return count;
462 }
463
464 int EnemyAddDamage(Enemy *enemy, float damage)
465 {
466 enemy->damage += damage;
467 if (enemy->damage >= EnemyGetMaxHealth(enemy))
468 {
469 currentLevel->playerGold += enemyClassConfigs[enemy->enemyType].goldValue;
470 enemy->enemyType = ENEMY_TYPE_NONE;
471 return 1;
472 }
473
474 return 0;
475 }
476
477 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
478 {
479 int16_t castleX = 0;
480 int16_t castleY = 0;
481 Enemy* closest = 0;
482 int16_t closestDistance = 0;
483 float range2 = range * range;
484 for (int i = 0; i < enemyCount; i++)
485 {
486 Enemy* enemy = &enemies[i];
487 if (enemy->enemyType == ENEMY_TYPE_NONE)
488 {
489 continue;
490 }
491 float maxHealth = EnemyGetMaxHealth(enemy);
492 if (enemy->futureDamage >= maxHealth)
493 {
494 // ignore enemies that will die soon
495 continue;
496 }
497 int16_t dx = castleX - enemy->currentX;
498 int16_t dy = castleY - enemy->currentY;
499 int16_t distance = abs(dx) + abs(dy);
500 if (!closest || distance < closestDistance)
501 {
502 float tdx = towerX - enemy->currentX;
503 float tdy = towerY - enemy->currentY;
504 float tdistance2 = tdx * tdx + tdy * tdy;
505 if (tdistance2 <= range2)
506 {
507 closest = enemy;
508 closestDistance = distance;
509 }
510 }
511 }
512 return closest;
513 }
514
515 int EnemyCount()
516 {
517 int count = 0;
518 for (int i = 0; i < enemyCount; i++)
519 {
520 if (enemies[i].enemyType != ENEMY_TYPE_NONE)
521 {
522 count++;
523 }
524 }
525 return count;
526 }
527
528 void EnemyDrawHealthbars(Camera3D camera)
529 {
530 for (int i = 0; i < enemyCount; i++)
531 {
532 Enemy *enemy = &enemies[i];
533 if (enemy->enemyType == ENEMY_TYPE_NONE || enemy->damage == 0.0f)
534 {
535 continue;
536 }
537 Vector3 position = (Vector3){enemy->simPosition.x, 0.5f, enemy->simPosition.y};
538 float maxHealth = EnemyGetMaxHealth(enemy);
539 float health = maxHealth - enemy->damage;
540 float healthRatio = health / maxHealth;
541
542 DrawHealthBar(camera, position, healthRatio, GREEN, 15.0f);
543 }
544 }
1 #include "td_main.h"
2 #include <raymath.h>
3
4 static Projectile projectiles[PROJECTILE_MAX_COUNT];
5 static int projectileCount = 0;
6
7 typedef struct ProjectileConfig
8 {
9 float arcFactor;
10 Color color;
11 Color trailColor;
12 } ProjectileConfig;
13
14 ProjectileConfig projectileConfigs[] = {
15 [PROJECTILE_TYPE_ARROW] = {
16 .arcFactor = 0.15f,
17 .color = RED,
18 .trailColor = BROWN,
19 },
20 [PROJECTILE_TYPE_CATAPULT] = {
21 .arcFactor = 0.5f,
22 .color = RED,
23 .trailColor = GRAY,
24 },
25 [PROJECTILE_TYPE_BALLISTA] = {
26 .arcFactor = 0.025f,
27 .color = RED,
28 .trailColor = BROWN,
29 },
30 };
31
32 void ProjectileInit()
33 {
34 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
35 {
36 projectiles[i] = (Projectile){0};
37 }
38 }
39
40 void ProjectileDraw()
41 {
42 for (int i = 0; i < projectileCount; i++)
43 {
44 Projectile projectile = projectiles[i];
45 if (projectile.projectileType == PROJECTILE_TYPE_NONE)
46 {
47 continue;
48 }
49 float transition = (gameTime.time - projectile.shootTime) / (projectile.arrivalTime - projectile.shootTime);
50 if (transition >= 1.0f)
51 {
52 continue;
53 }
54
55 ProjectileConfig config = projectileConfigs[projectile.projectileType];
56 for (float transitionOffset = 0.0f; transitionOffset < 1.0f; transitionOffset += 0.1f)
57 {
58 float t = transition + transitionOffset * 0.3f;
59 if (t > 1.0f)
60 {
61 break;
62 }
63 Vector3 position = Vector3Lerp(projectile.position, projectile.target, t);
64 Color color = config.color;
65 color = ColorLerp(config.trailColor, config.color, transitionOffset * transitionOffset);
66 // fake a ballista flight path using parabola equation
67 float parabolaT = t - 0.5f;
68 parabolaT = 1.0f - 4.0f * parabolaT * parabolaT;
69 position.y += config.arcFactor * parabolaT * projectile.distance;
70
71 float size = 0.06f * (transitionOffset + 0.25f);
72 DrawCube(position, size, size, size, color);
73 }
74 }
75 }
76
77 void ProjectileUpdate()
78 {
79 for (int i = 0; i < projectileCount; i++)
80 {
81 Projectile *projectile = &projectiles[i];
82 if (projectile->projectileType == PROJECTILE_TYPE_NONE)
83 {
84 continue;
85 }
86 float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime);
87 if (transition >= 1.0f)
88 {
89 projectile->projectileType = PROJECTILE_TYPE_NONE;
90 Enemy *enemy = EnemyTryResolve(projectile->targetEnemy);
91 if (enemy && projectile->hitEffectConfig.pushbackPowerDistance > 0.0f)
92 {
93 Vector2 direction = Vector2Normalize(Vector2Subtract((Vector2){projectile->target.x, projectile->target.z}, enemy->simPosition));
94 enemy->simPosition = Vector2Add(enemy->simPosition, Vector2Scale(direction, projectile->hitEffectConfig.pushbackPowerDistance));
95 }
96
97 if (projectile->hitEffectConfig.areaDamageRadius > 0.0f)
98 {
99 EnemyAddDamageRange((Vector2){projectile->target.x, projectile->target.z}, projectile->hitEffectConfig.areaDamageRadius, projectile->hitEffectConfig.damage);
100 // pancaked sphere explosion
101 float r = projectile->hitEffectConfig.areaDamageRadius;
102 ParticleAdd(PARTICLE_TYPE_EXPLOSION, projectile->target, (Vector3){0}, (Vector3){r, r * 0.2f, r}, 0.33f);
103 }
104 else if (projectile->hitEffectConfig.damage > 0.0f && enemy)
105 {
106 EnemyAddDamage(enemy, projectile->hitEffectConfig.damage);
107 }
108 continue;
109 }
110 }
111 }
112
113 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig)
114 {
115 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
116 {
117 Projectile *projectile = &projectiles[i];
118 if (projectile->projectileType == PROJECTILE_TYPE_NONE)
119 {
120 projectile->projectileType = projectileType;
121 projectile->shootTime = gameTime.time;
122 float distance = Vector3Distance(position, target);
123 projectile->arrivalTime = gameTime.time + distance / speed;
124 projectile->position = position;
125 projectile->target = target;
126 projectile->directionNormal = Vector3Scale(Vector3Subtract(target, position), 1.0f / distance);
127 projectile->distance = distance;
128 projectile->targetEnemy = EnemyGetId(enemy);
129 projectileCount = projectileCount <= i ? i + 1 : projectileCount;
130 projectile->hitEffectConfig = hitEffectConfig;
131 return projectile;
132 }
133 }
134 return 0;
135 }
1 #include "td_main.h"
2 #include <raymath.h>
3 #include <rlgl.h>
4
5 static Particle particles[PARTICLE_MAX_COUNT];
6 static int particleCount = 0;
7
8 void ParticleInit()
9 {
10 for (int i = 0; i < PARTICLE_MAX_COUNT; i++)
11 {
12 particles[i] = (Particle){0};
13 }
14 particleCount = 0;
15 }
16
17 static void DrawExplosionParticle(Particle *particle, float transition)
18 {
19 Vector3 scale = particle->scale;
20 float size = 1.0f * (1.0f - transition);
21 Color startColor = WHITE;
22 Color endColor = RED;
23 Color color = ColorLerp(startColor, endColor, transition);
24
25 rlPushMatrix();
26 rlTranslatef(particle->position.x, particle->position.y, particle->position.z);
27 rlScalef(scale.x, scale.y, scale.z);
28 DrawSphere(Vector3Zero(), size, color);
29 rlPopMatrix();
30 }
31
32 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime)
33 {
34 if (particleCount >= PARTICLE_MAX_COUNT)
35 {
36 return;
37 }
38
39 int index = -1;
40 for (int i = 0; i < particleCount; i++)
41 {
42 if (particles[i].particleType == PARTICLE_TYPE_NONE)
43 {
44 index = i;
45 break;
46 }
47 }
48
49 if (index == -1)
50 {
51 index = particleCount++;
52 }
53
54 Particle *particle = &particles[index];
55 particle->particleType = particleType;
56 particle->spawnTime = gameTime.time;
57 particle->lifetime = lifetime;
58 particle->position = position;
59 particle->velocity = velocity;
60 particle->scale = scale;
61 }
62
63 void ParticleUpdate()
64 {
65 for (int i = 0; i < particleCount; i++)
66 {
67 Particle *particle = &particles[i];
68 if (particle->particleType == PARTICLE_TYPE_NONE)
69 {
70 continue;
71 }
72
73 float age = gameTime.time - particle->spawnTime;
74
75 if (particle->lifetime > age)
76 {
77 particle->position = Vector3Add(particle->position, Vector3Scale(particle->velocity, gameTime.deltaTime));
78 }
79 else {
80 particle->particleType = PARTICLE_TYPE_NONE;
81 }
82 }
83 }
84
85 void ParticleDraw()
86 {
87 for (int i = 0; i < particleCount; i++)
88 {
89 Particle particle = particles[i];
90 if (particle.particleType == PARTICLE_TYPE_NONE)
91 {
92 continue;
93 }
94
95 float age = gameTime.time - particle.spawnTime;
96 float transition = age / particle.lifetime;
97 switch (particle.particleType)
98 {
99 case PARTICLE_TYPE_EXPLOSION:
100 DrawExplosionParticle(&particle, transition);
101 break;
102 default:
103 DrawCube(particle.position, 0.3f, 0.5f, 0.3f, RED);
104 break;
105 }
106 }
107 }
1 #include "raylib.h"
2 #include "preferred_size.h"
3
4 // Since the canvas size is not known at compile time, we need to query it at runtime;
5 // the following platform specific code obtains the canvas size and we will use this
6 // size as the preferred size for the window at init time. We're ignoring here the
7 // possibility of the canvas size changing during runtime - this would require to
8 // poll the canvas size in the game loop or establishing a callback to be notified
9
10 #ifdef PLATFORM_WEB
11 #include <emscripten.h>
12 EMSCRIPTEN_RESULT emscripten_get_element_css_size(const char *target, double *width, double *height);
13
14 void GetPreferredSize(int *screenWidth, int *screenHeight)
15 {
16 double canvasWidth, canvasHeight;
17 emscripten_get_element_css_size("#" CANVAS_NAME, &canvasWidth, &canvasHeight);
18 *screenWidth = (int)canvasWidth;
19 *screenHeight = (int)canvasHeight;
20 TraceLog(LOG_INFO, "preferred size for %s: %d %d", CANVAS_NAME, *screenWidth, *screenHeight);
21 }
22
23 int IsPaused()
24 {
25 const char *js = "(function(){\n"
26 " var canvas = document.getElementById(\"" CANVAS_NAME "\");\n"
27 " var rect = canvas.getBoundingClientRect();\n"
28 " var isVisible = (\n"
29 " rect.top >= 0 &&\n"
30 " rect.left >= 0 &&\n"
31 " rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&\n"
32 " rect.right <= (window.innerWidth || document.documentElement.clientWidth)\n"
33 " );\n"
34 " return isVisible ? 0 : 1;\n"
35 "})()";
36 return emscripten_run_script_int(js);
37 }
38
39 #else
40 void GetPreferredSize(int *screenWidth, int *screenHeight)
41 {
42 *screenWidth = 600;
43 *screenHeight = 240;
44 }
45 int IsPaused()
46 {
47 return 0;
48 }
49 #endif
1 #ifndef PREFERRED_SIZE_H
2 #define PREFERRED_SIZE_H
3
4 void GetPreferredSize(int *screenWidth, int *screenHeight);
5 int IsPaused();
6
7 #endif
It kinda works, but there are some render artifacts called Z-fights. This happens when two objects are rendered at the same depth and floating point inaccuracies cause flickering pixels. There are two ways to handle this: Avoiding the occurrence of Z-fights or making them invisible by rendering the z-fighting triangles with the same color.
My favorite solution is usually the latter: Making sure that the Z-fighting triangles are rendered with the same color is quite easy in a situation without lighting effects. This is not possible when using textured geometry, but for flat colored geometry, it's very effective. This is also how I came to choose this art style for making prototypes and simple games: It's very easy to work with and some nasty technical problems become non-issues.
Let's look at the arrow mesh how it is right now:
Now let's modify this mesh by just moving the vertices of the outline a bit lower, also known as beveling:
It's barely visible, but let's see how it looks in the game:
1 #include "td_main.h"
2 #include <raymath.h>
3 #include <rlgl.h>
4 #include <stdlib.h>
5 #include <math.h>
6
7 //# Variables
8 GUIState guiState = {0};
9 GameTime gameTime = {0};
10
11 Model floorTileAModel = {0};
12 Model floorTileBModel = {0};
13 Model treeModel[2] = {0};
14 Model firTreeModel[2] = {0};
15 Model rockModels[5] = {0};
16 Model grassPatchModel[1] = {0};
17
18 Model pathArrowModel = {0};
19
20 Texture2D palette, spriteSheet;
21
22 Level levels[] = {
23 [0] = {
24 .state = LEVEL_STATE_BUILDING,
25 .initialGold = 20,
26 .waves[0] = {
27 .enemyType = ENEMY_TYPE_MINION,
28 .wave = 0,
29 .count = 5,
30 .interval = 2.5f,
31 .delay = 1.0f,
32 .spawnPosition = {2, 6},
33 },
34 .waves[1] = {
35 .enemyType = ENEMY_TYPE_MINION,
36 .wave = 0,
37 .count = 5,
38 .interval = 2.5f,
39 .delay = 1.0f,
40 .spawnPosition = {-2, 6},
41 },
42 .waves[2] = {
43 .enemyType = ENEMY_TYPE_MINION,
44 .wave = 1,
45 .count = 20,
46 .interval = 1.5f,
47 .delay = 1.0f,
48 .spawnPosition = {0, 6},
49 },
50 .waves[3] = {
51 .enemyType = ENEMY_TYPE_MINION,
52 .wave = 2,
53 .count = 30,
54 .interval = 1.2f,
55 .delay = 1.0f,
56 .spawnPosition = {0, 6},
57 }
58 },
59 };
60
61 Level *currentLevel = levels;
62
63 //# Game
64
65 static Model LoadGLBModel(char *filename)
66 {
67 Model model = LoadModel(TextFormat("data/%s.glb",filename));
68 for (int i = 0; i < model.materialCount; i++)
69 {
70 model.materials[i].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
71 }
72 return model;
73 }
74
75 void LoadAssets()
76 {
77 // load a sprite sheet that contains all units
78 spriteSheet = LoadTexture("data/spritesheet.png");
79 SetTextureFilter(spriteSheet, TEXTURE_FILTER_BILINEAR);
80
81 // we'll use a palette texture to colorize the all buildings and environment art
82 palette = LoadTexture("data/palette.png");
83 // The texture uses gradients on very small space, so we'll enable bilinear filtering
84 SetTextureFilter(palette, TEXTURE_FILTER_BILINEAR);
85
86 floorTileAModel = LoadGLBModel("floor-tile-a");
87 floorTileBModel = LoadGLBModel("floor-tile-b");
88 treeModel[0] = LoadGLBModel("leaftree-large-1-a");
89 treeModel[1] = LoadGLBModel("leaftree-large-1-b");
90 firTreeModel[0] = LoadGLBModel("firtree-1-a");
91 firTreeModel[1] = LoadGLBModel("firtree-1-b");
92 rockModels[0] = LoadGLBModel("rock-1");
93 rockModels[1] = LoadGLBModel("rock-2");
94 rockModels[2] = LoadGLBModel("rock-3");
95 rockModels[3] = LoadGLBModel("rock-4");
96 rockModels[4] = LoadGLBModel("rock-5");
97 grassPatchModel[0] = LoadGLBModel("grass-patch-1");
98
99 pathArrowModel = LoadGLBModel("direction-arrow-x");
100 }
101
102 void InitLevel(Level *level)
103 {
104 level->seed = (int)(GetTime() * 100.0f);
105
106 TowerInit();
107 EnemyInit();
108 ProjectileInit();
109 ParticleInit();
110 TowerTryAdd(TOWER_TYPE_BASE, 0, 0);
111
112 level->placementMode = 0;
113 level->state = LEVEL_STATE_BUILDING;
114 level->nextState = LEVEL_STATE_NONE;
115 level->playerGold = level->initialGold;
116 level->currentWave = 0;
117
118 Camera *camera = &level->camera;
119 camera->position = (Vector3){4.0f, 8.0f, 8.0f};
120 camera->target = (Vector3){0.0f, 0.0f, 0.0f};
121 camera->up = (Vector3){0.0f, 1.0f, 0.0f};
122 camera->fovy = 10.0f;
123 camera->projection = CAMERA_ORTHOGRAPHIC;
124 }
125
126 void DrawLevelHud(Level *level)
127 {
128 const char *text = TextFormat("Gold: %d", level->playerGold);
129 Font font = GetFontDefault();
130 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 120, 10}, font.baseSize * 2.0f, 2.0f, BLACK);
131 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 122, 8}, font.baseSize * 2.0f, 2.0f, YELLOW);
132 }
133
134 void DrawLevelReportLostWave(Level *level)
135 {
136 BeginMode3D(level->camera);
137 DrawLevelGround(level);
138 TowerDraw();
139 EnemyDraw();
140 ProjectileDraw();
141 ParticleDraw();
142 guiState.isBlocked = 0;
143 EndMode3D();
144
145 TowerDrawHealthBars(level->camera);
146
147 const char *text = "Wave lost";
148 int textWidth = MeasureText(text, 20);
149 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
150
151 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
152 {
153 level->nextState = LEVEL_STATE_RESET;
154 }
155 }
156
157 int HasLevelNextWave(Level *level)
158 {
159 for (int i = 0; i < 10; i++)
160 {
161 EnemyWave *wave = &level->waves[i];
162 if (wave->wave == level->currentWave)
163 {
164 return 1;
165 }
166 }
167 return 0;
168 }
169
170 void DrawLevelReportWonWave(Level *level)
171 {
172 BeginMode3D(level->camera);
173 DrawLevelGround(level);
174 TowerDraw();
175 EnemyDraw();
176 ProjectileDraw();
177 ParticleDraw();
178 guiState.isBlocked = 0;
179 EndMode3D();
180
181 TowerDrawHealthBars(level->camera);
182
183 const char *text = "Wave won";
184 int textWidth = MeasureText(text, 20);
185 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
186
187
188 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
189 {
190 level->nextState = LEVEL_STATE_RESET;
191 }
192
193 if (HasLevelNextWave(level))
194 {
195 if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
196 {
197 level->nextState = LEVEL_STATE_BUILDING;
198 }
199 }
200 else {
201 if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
202 {
203 level->nextState = LEVEL_STATE_WON_LEVEL;
204 }
205 }
206 }
207
208 void DrawBuildingBuildButton(Level *level, int x, int y, int width, int height, uint8_t towerType, const char *name)
209 {
210 static ButtonState buttonStates[8] = {0};
211 int cost = GetTowerCosts(towerType);
212 const char *text = TextFormat("%s: %d", name, cost);
213 buttonStates[towerType].isSelected = level->placementMode == towerType;
214 buttonStates[towerType].isDisabled = level->playerGold < cost;
215 if (Button(text, x, y, width, height, &buttonStates[towerType]))
216 {
217 level->placementMode = buttonStates[towerType].isSelected ? 0 : towerType;
218 }
219 }
220
221 float GetRandomFloat(float min, float max)
222 {
223 int random = GetRandomValue(0, 0xfffffff);
224 return ((float)random / (float)0xfffffff) * (max - min) + min;
225 }
226
227 void DrawLevelGround(Level *level)
228 {
229 // draw checkerboard ground pattern
230 for (int x = -5; x <= 5; x += 1)
231 {
232 for (int y = -5; y <= 5; y += 1)
233 {
234 Model *model = (x + y) % 2 == 0 ? &floorTileAModel : &floorTileBModel;
235 DrawModel(*model, (Vector3){x, 0.0f, y}, 1.0f, WHITE);
236 }
237 }
238
239 int oldSeed = GetRandomValue(0, 0xfffffff);
240 SetRandomSeed(level->seed);
241 // increase probability for trees via duplicated entries
242 Model borderModels[64];
243 int maxRockCount = GetRandomValue(2, 6);
244 int maxTreeCount = GetRandomValue(10, 20);
245 int maxFirTreeCount = GetRandomValue(5, 10);
246 int maxLeafTreeCount = maxTreeCount - maxFirTreeCount;
247 int grassPatchCount = GetRandomValue(5, 30);
248
249 int modelCount = 0;
250 for (int i = 0; i < maxRockCount && modelCount < 63; i++)
251 {
252 borderModels[modelCount++] = rockModels[GetRandomValue(0, 5)];
253 }
254 for (int i = 0; i < maxLeafTreeCount && modelCount < 63; i++)
255 {
256 borderModels[modelCount++] = treeModel[GetRandomValue(0, 1)];
257 }
258 for (int i = 0; i < maxFirTreeCount && modelCount < 63; i++)
259 {
260 borderModels[modelCount++] = firTreeModel[GetRandomValue(0, 1)];
261 }
262 for (int i = 0; i < grassPatchCount && modelCount < 63; i++)
263 {
264 borderModels[modelCount++] = grassPatchModel[0];
265 }
266
267 // draw some objects around the border of the map
268 Vector3 up = {0, 1, 0};
269 // a pseudo random number generator to get the same result every time
270 const float wiggle = 0.75f;
271 const int layerCount = 3;
272 for (int layer = 0; layer < layerCount; layer++)
273 {
274 int layerPos = 6 + layer;
275 for (int x = -6 + layer; x <= 6 + layer; x += 1)
276 {
277 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)],
278 (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, -layerPos + GetRandomFloat(0.0f, wiggle)},
279 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
280 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)],
281 (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, layerPos + GetRandomFloat(0.0f, wiggle)},
282 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
283 }
284
285 for (int z = -5 + layer; z <= 5 + layer; z += 1)
286 {
287 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)],
288 (Vector3){-layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)},
289 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
290 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)],
291 (Vector3){layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)},
292 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
293 }
294 }
295
296 SetRandomSeed(oldSeed);
297 }
298
299 void DrawEnemyPath(Level *level, Color arrowColor)
300 {
301 const int castleX = 0, castleY = 0;
302 const int maxWaypointCount = 200;
303 const float timeStep = 1.0f;
304 Vector3 arrowScale = {0.75f, 0.75f, 0.75f};
305
306 // we start with a time offset to simulate the path,
307 // this way the arrows are animated in a forward moving direction
308 // The time is wrapped around the time step to get a smooth animation
309 float timeOffset = fmodf(GetTime(), timeStep);
310
311 for (int i = 0; i < ENEMY_MAX_WAVE_COUNT; i++)
312 {
313 EnemyWave *wave = &level->waves[i];
314 if (wave->wave != level->currentWave)
315 {
316 continue;
317 }
318
319 // use this dummy enemy to simulate the path
320 Enemy dummy = {
321 .enemyType = ENEMY_TYPE_MINION,
322 .simPosition = (Vector2){wave->spawnPosition.x, wave->spawnPosition.y},
323 .nextX = wave->spawnPosition.x,
324 .nextY = wave->spawnPosition.y,
325 .currentX = wave->spawnPosition.x,
326 .currentY = wave->spawnPosition.y,
327 };
328
329 float deltaTime = timeOffset;
330 for (int j = 0; j < maxWaypointCount; j++)
331 {
332 int waypointPassedCount = 0;
333 Vector2 pos = EnemyGetPosition(&dummy, deltaTime, &dummy.simVelocity, &waypointPassedCount);
334 // after the initial variable starting offset, we use a fixed time step
335 deltaTime = timeStep;
336 dummy.simPosition = pos;
337
338 // Update the dummy's position just like we do in the regular enemy update loop
339 for (int k = 0; k < waypointPassedCount; k++)
340 {
341 dummy.currentX = dummy.nextX;
342 dummy.currentY = dummy.nextY;
343 if (EnemyGetNextPosition(dummy.currentX, dummy.currentY, &dummy.nextX, &dummy.nextY) &&
344 Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
345 {
346 break;
347 }
348 }
349 if (Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
350 {
351 break;
352 }
353
354 // get the angle we need to rotate the arrow model. The velocity is just fine for this.
355 float angle = atan2f(dummy.simVelocity.x, dummy.simVelocity.y) * RAD2DEG - 90.0f;
356 DrawModelEx(pathArrowModel, (Vector3){pos.x, 0.15f, pos.y}, (Vector3){0, 1, 0}, angle, arrowScale, arrowColor);
357 }
358 }
359 }
360
361 void DrawLevelBuildingState(Level *level)
362 {
363 BeginMode3D(level->camera);
364 DrawLevelGround(level);
365
366 int blockedCellCount = 0;
367 Vector2 blockedCells[1];
368 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera);
369 float planeDistance = ray.position.y / -ray.direction.y;
370 float planeX = ray.direction.x * planeDistance + ray.position.x;
371 float planeY = ray.direction.z * planeDistance + ray.position.z;
372 int16_t mapX = (int16_t)floorf(planeX + 0.5f);
373 int16_t mapY = (int16_t)floorf(planeY + 0.5f);
374 if (level->placementMode && !guiState.isBlocked && mapX >= -5 && mapX <= 5 && mapY >= -5 && mapY <= 5)
375 {
376 DrawCubeWires((Vector3){mapX, 0.2f, mapY}, 1.0f, 0.4f, 1.0f, RED);
377 blockedCells[blockedCellCount++] = (Vector2){mapX, mapY};
378 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
379 {
380 if (TowerTryAdd(level->placementMode, mapX, mapY))
381 {
382 level->playerGold -= GetTowerCosts(level->placementMode);
383 level->placementMode = TOWER_TYPE_NONE;
384 }
385 }
386 }
387
388 PathFindingMapUpdate(blockedCellCount, blockedCells);
389 TowerDraw();
390 EnemyDraw();
391 ProjectileDraw();
392 ParticleDraw();
393
394 // disable depth testing for the path arrows
395 // flush the 3D batch to draw the arrows on top of everything
396 rlDrawRenderBatchActive();
397 rlDisableDepthTest();
398 DrawEnemyPath(level, (Color){64, 64, 64, 160});
399
400 rlDrawRenderBatchActive();
401 rlEnableDepthTest();
402 DrawEnemyPath(level, WHITE);
403
404 guiState.isBlocked = 0;
405
406 EndMode3D();
407
408 TowerDrawHealthBars(level->camera);
409
410 static ButtonState buildWallButtonState = {0};
411 static ButtonState buildGunButtonState = {0};
412 buildWallButtonState.isSelected = level->placementMode == TOWER_TYPE_WALL;
413 buildGunButtonState.isSelected = level->placementMode == TOWER_TYPE_ARCHER;
414
415 DrawBuildingBuildButton(level, 10, 10, 110, 30, TOWER_TYPE_WALL, "Wall");
416 DrawBuildingBuildButton(level, 10, 50, 110, 30, TOWER_TYPE_ARCHER, "Archer");
417 DrawBuildingBuildButton(level, 10, 90, 110, 30, TOWER_TYPE_BALLISTA, "Ballista");
418 DrawBuildingBuildButton(level, 10, 130, 110, 30, TOWER_TYPE_CATAPULT, "Catapult");
419
420 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
421 {
422 level->nextState = LEVEL_STATE_RESET;
423 }
424
425 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0))
426 {
427 level->nextState = LEVEL_STATE_BATTLE;
428 }
429
430 const char *text = "Building phase";
431 int textWidth = MeasureText(text, 20);
432 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
433 }
434
435 void InitBattleStateConditions(Level *level)
436 {
437 level->state = LEVEL_STATE_BATTLE;
438 level->nextState = LEVEL_STATE_NONE;
439 level->waveEndTimer = 0.0f;
440 for (int i = 0; i < 10; i++)
441 {
442 EnemyWave *wave = &level->waves[i];
443 wave->spawned = 0;
444 wave->timeToSpawnNext = wave->delay;
445 }
446 }
447
448 void DrawLevelBattleState(Level *level)
449 {
450 BeginMode3D(level->camera);
451 DrawLevelGround(level);
452 TowerDraw();
453 EnemyDraw();
454 ProjectileDraw();
455 ParticleDraw();
456 guiState.isBlocked = 0;
457 EndMode3D();
458
459 EnemyDrawHealthbars(level->camera);
460 TowerDrawHealthBars(level->camera);
461
462 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
463 {
464 level->nextState = LEVEL_STATE_RESET;
465 }
466
467 int maxCount = 0;
468 int remainingCount = 0;
469 for (int i = 0; i < 10; i++)
470 {
471 EnemyWave *wave = &level->waves[i];
472 if (wave->wave != level->currentWave)
473 {
474 continue;
475 }
476 maxCount += wave->count;
477 remainingCount += wave->count - wave->spawned;
478 }
479 int aliveCount = EnemyCount();
480 remainingCount += aliveCount;
481
482 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount);
483 int textWidth = MeasureText(text, 20);
484 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
485 }
486
487 void DrawLevel(Level *level)
488 {
489 switch (level->state)
490 {
491 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break;
492 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break;
493 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break;
494 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break;
495 default: break;
496 }
497
498 DrawLevelHud(level);
499 }
500
501 void UpdateLevel(Level *level)
502 {
503 if (level->state == LEVEL_STATE_BATTLE)
504 {
505 int activeWaves = 0;
506 for (int i = 0; i < 10; i++)
507 {
508 EnemyWave *wave = &level->waves[i];
509 if (wave->spawned >= wave->count || wave->wave != level->currentWave)
510 {
511 continue;
512 }
513 activeWaves++;
514 wave->timeToSpawnNext -= gameTime.deltaTime;
515 if (wave->timeToSpawnNext <= 0.0f)
516 {
517 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y);
518 if (enemy)
519 {
520 wave->timeToSpawnNext = wave->interval;
521 wave->spawned++;
522 }
523 }
524 }
525 if (GetTowerByType(TOWER_TYPE_BASE) == 0) {
526 level->waveEndTimer += gameTime.deltaTime;
527 if (level->waveEndTimer >= 2.0f)
528 {
529 level->nextState = LEVEL_STATE_LOST_WAVE;
530 }
531 }
532 else if (activeWaves == 0 && EnemyCount() == 0)
533 {
534 level->waveEndTimer += gameTime.deltaTime;
535 if (level->waveEndTimer >= 2.0f)
536 {
537 level->nextState = LEVEL_STATE_WON_WAVE;
538 }
539 }
540 }
541
542 PathFindingMapUpdate(0, 0);
543 EnemyUpdate();
544 TowerUpdate();
545 ProjectileUpdate();
546 ParticleUpdate();
547
548 if (level->nextState == LEVEL_STATE_RESET)
549 {
550 InitLevel(level);
551 }
552
553 if (level->nextState == LEVEL_STATE_BATTLE)
554 {
555 InitBattleStateConditions(level);
556 }
557
558 if (level->nextState == LEVEL_STATE_WON_WAVE)
559 {
560 level->currentWave++;
561 level->state = LEVEL_STATE_WON_WAVE;
562 }
563
564 if (level->nextState == LEVEL_STATE_LOST_WAVE)
565 {
566 level->state = LEVEL_STATE_LOST_WAVE;
567 }
568
569 if (level->nextState == LEVEL_STATE_BUILDING)
570 {
571 level->state = LEVEL_STATE_BUILDING;
572 }
573
574 if (level->nextState == LEVEL_STATE_WON_LEVEL)
575 {
576 // make something of this later
577 InitLevel(level);
578 }
579
580 level->nextState = LEVEL_STATE_NONE;
581 }
582
583 float nextSpawnTime = 0.0f;
584
585 void ResetGame()
586 {
587 InitLevel(currentLevel);
588 }
589
590 void InitGame()
591 {
592 TowerInit();
593 EnemyInit();
594 ProjectileInit();
595 ParticleInit();
596 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f);
597
598 currentLevel = levels;
599 InitLevel(currentLevel);
600 }
601
602 //# Immediate GUI functions
603
604 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth)
605 {
606 const float healthBarHeight = 6.0f;
607 const float healthBarOffset = 15.0f;
608 const float inset = 2.0f;
609 const float innerWidth = healthBarWidth - inset * 2;
610 const float innerHeight = healthBarHeight - inset * 2;
611
612 Vector2 screenPos = GetWorldToScreen(position, camera);
613 float centerX = screenPos.x - healthBarWidth * 0.5f;
614 float topY = screenPos.y - healthBarOffset;
615 DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK);
616 float healthWidth = innerWidth * healthRatio;
617 DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor);
618 }
619
620 int Button(const char *text, int x, int y, int width, int height, ButtonState *state)
621 {
622 Rectangle bounds = {x, y, width, height};
623 int isPressed = 0;
624 int isSelected = state && state->isSelected;
625 int isDisabled = state && state->isDisabled;
626 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !isDisabled)
627 {
628 Color color = isSelected ? DARKGRAY : GRAY;
629 DrawRectangle(x, y, width, height, color);
630 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
631 {
632 isPressed = 1;
633 }
634 guiState.isBlocked = 1;
635 }
636 else
637 {
638 Color color = isSelected ? WHITE : LIGHTGRAY;
639 DrawRectangle(x, y, width, height, color);
640 }
641 Font font = GetFontDefault();
642 Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1);
643 Color textColor = isDisabled ? GRAY : BLACK;
644 DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, textColor);
645 return isPressed;
646 }
647
648 //# Main game loop
649
650 void GameUpdate()
651 {
652 float dt = GetFrameTime();
653 // cap maximum delta time to 0.1 seconds to prevent large time steps
654 if (dt > 0.1f) dt = 0.1f;
655 gameTime.time += dt;
656 gameTime.deltaTime = dt;
657
658 UpdateLevel(currentLevel);
659 }
660
661 int main(void)
662 {
663 int screenWidth, screenHeight;
664 GetPreferredSize(&screenWidth, &screenHeight);
665 InitWindow(screenWidth, screenHeight, "Tower defense");
666 SetTargetFPS(30);
667
668 LoadAssets();
669 InitGame();
670
671 while (!WindowShouldClose())
672 {
673 if (IsPaused()) {
674 // canvas is not visible in browser - do nothing
675 continue;
676 }
677
678 BeginDrawing();
679 ClearBackground((Color){0x4E, 0x63, 0x26, 0xFF});
680
681 GameUpdate();
682 DrawLevel(currentLevel);
683
684 EndDrawing();
685 }
686
687 CloseWindow();
688
689 return 0;
690 }
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 #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 #define ENEMY_MAX_PATH_COUNT 8
12 #define ENEMY_MAX_COUNT 400
13 #define ENEMY_TYPE_NONE 0
14 #define ENEMY_TYPE_MINION 1
15
16 #define PARTICLE_MAX_COUNT 400
17 #define PARTICLE_TYPE_NONE 0
18 #define PARTICLE_TYPE_EXPLOSION 1
19
20 typedef struct Particle
21 {
22 uint8_t particleType;
23 float spawnTime;
24 float lifetime;
25 Vector3 position;
26 Vector3 velocity;
27 Vector3 scale;
28 } Particle;
29
30 #define TOWER_MAX_COUNT 400
31 enum TowerType
32 {
33 TOWER_TYPE_NONE,
34 TOWER_TYPE_BASE,
35 TOWER_TYPE_ARCHER,
36 TOWER_TYPE_BALLISTA,
37 TOWER_TYPE_CATAPULT,
38 TOWER_TYPE_WALL,
39 TOWER_TYPE_COUNT
40 };
41
42 typedef struct HitEffectConfig
43 {
44 float damage;
45 float areaDamageRadius;
46 float pushbackPowerDistance;
47 } HitEffectConfig;
48
49 typedef struct TowerTypeConfig
50 {
51 float cooldown;
52 float range;
53 float projectileSpeed;
54
55 uint8_t cost;
56 uint8_t projectileType;
57 uint16_t maxHealth;
58
59 HitEffectConfig hitEffect;
60 } TowerTypeConfig;
61
62 typedef struct Tower
63 {
64 int16_t x, y;
65 uint8_t towerType;
66 Vector2 lastTargetPosition;
67 float cooldown;
68 float damage;
69 } Tower;
70
71 typedef struct GameTime
72 {
73 float time;
74 float deltaTime;
75 } GameTime;
76
77 typedef struct ButtonState {
78 char isSelected;
79 char isDisabled;
80 } ButtonState;
81
82 typedef struct GUIState {
83 int isBlocked;
84 } GUIState;
85
86 typedef enum LevelState
87 {
88 LEVEL_STATE_NONE,
89 LEVEL_STATE_BUILDING,
90 LEVEL_STATE_BATTLE,
91 LEVEL_STATE_WON_WAVE,
92 LEVEL_STATE_LOST_WAVE,
93 LEVEL_STATE_WON_LEVEL,
94 LEVEL_STATE_RESET,
95 } LevelState;
96
97 typedef struct EnemyWave {
98 uint8_t enemyType;
99 uint8_t wave;
100 uint16_t count;
101 float interval;
102 float delay;
103 Vector2 spawnPosition;
104
105 uint16_t spawned;
106 float timeToSpawnNext;
107 } EnemyWave;
108
109 #define ENEMY_MAX_WAVE_COUNT 10
110
111 typedef struct Level
112 {
113 int seed;
114 LevelState state;
115 LevelState nextState;
116 Camera3D camera;
117 int placementMode;
118
119 int initialGold;
120 int playerGold;
121
122 EnemyWave waves[ENEMY_MAX_WAVE_COUNT];
123 int currentWave;
124 float waveEndTimer;
125 } Level;
126
127 typedef struct DeltaSrc
128 {
129 char x, y;
130 } DeltaSrc;
131
132 typedef struct PathfindingMap
133 {
134 int width, height;
135 float scale;
136 float *distances;
137 long *towerIndex;
138 DeltaSrc *deltaSrc;
139 float maxDistance;
140 Matrix toMapSpace;
141 Matrix toWorldSpace;
142 } PathfindingMap;
143
144 // when we execute the pathfinding algorithm, we need to store the active nodes
145 // in a queue. Each node has a position, a distance from the start, and the
146 // position of the node that we came from.
147 typedef struct PathfindingNode
148 {
149 int16_t x, y, fromX, fromY;
150 float distance;
151 } PathfindingNode;
152
153 typedef struct EnemyId
154 {
155 uint16_t index;
156 uint16_t generation;
157 } EnemyId;
158
159 typedef struct EnemyClassConfig
160 {
161 float speed;
162 float health;
163 float radius;
164 float maxAcceleration;
165 float requiredContactTime;
166 float explosionDamage;
167 float explosionRange;
168 float explosionPushbackPower;
169 int goldValue;
170 } EnemyClassConfig;
171
172 typedef struct Enemy
173 {
174 int16_t currentX, currentY;
175 int16_t nextX, nextY;
176 Vector2 simPosition;
177 Vector2 simVelocity;
178 uint16_t generation;
179 float walkedDistance;
180 float startMovingTime;
181 float damage, futureDamage;
182 float contactTime;
183 uint8_t enemyType;
184 uint8_t movePathCount;
185 Vector2 movePath[ENEMY_MAX_PATH_COUNT];
186 } Enemy;
187
188 // a unit that uses sprites to be drawn
189 #define SPRITE_UNIT_PHASE_WEAPON_IDLE 0
190 #define SPRITE_UNIT_PHASE_WEAPON_COOLDOWN 1
191 typedef struct SpriteUnit
192 {
193 Rectangle srcRect;
194 Vector2 offset;
195 int frameCount;
196 float frameDuration;
197 Rectangle srcWeaponIdleRect;
198 Vector2 srcWeaponIdleOffset;
199 Rectangle srcWeaponCooldownRect;
200 Vector2 srcWeaponCooldownOffset;
201 } SpriteUnit;
202
203 #define PROJECTILE_MAX_COUNT 1200
204 #define PROJECTILE_TYPE_NONE 0
205 #define PROJECTILE_TYPE_ARROW 1
206 #define PROJECTILE_TYPE_CATAPULT 2
207 #define PROJECTILE_TYPE_BALLISTA 3
208
209 typedef struct Projectile
210 {
211 uint8_t projectileType;
212 float shootTime;
213 float arrivalTime;
214 float distance;
215 Vector3 position;
216 Vector3 target;
217 Vector3 directionNormal;
218 EnemyId targetEnemy;
219 HitEffectConfig hitEffectConfig;
220 } Projectile;
221
222 //# Function declarations
223 float TowerGetMaxHealth(Tower *tower);
224 int Button(const char *text, int x, int y, int width, int height, ButtonState *state);
225 int EnemyAddDamageRange(Vector2 position, float range, float damage);
226 int EnemyAddDamage(Enemy *enemy, float damage);
227
228 //# Enemy functions
229 void EnemyInit();
230 void EnemyDraw();
231 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource);
232 void EnemyUpdate();
233 float EnemyGetCurrentMaxSpeed(Enemy *enemy);
234 float EnemyGetMaxHealth(Enemy *enemy);
235 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY);
236 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount);
237 EnemyId EnemyGetId(Enemy *enemy);
238 Enemy *EnemyTryResolve(EnemyId enemyId);
239 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY);
240 int EnemyAddDamage(Enemy *enemy, float damage);
241 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range);
242 int EnemyCount();
243 void EnemyDrawHealthbars(Camera3D camera);
244
245 //# Tower functions
246 void TowerInit();
247 Tower *TowerGetAt(int16_t x, int16_t y);
248 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y);
249 Tower *GetTowerByType(uint8_t towerType);
250 int GetTowerCosts(uint8_t towerType);
251 float TowerGetMaxHealth(Tower *tower);
252 void TowerDraw();
253 void TowerUpdate();
254 void TowerDrawHealthBars(Camera3D camera);
255 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase);
256
257 //# Particles
258 void ParticleInit();
259 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime);
260 void ParticleUpdate();
261 void ParticleDraw();
262
263 //# Projectiles
264 void ProjectileInit();
265 void ProjectileDraw();
266 void ProjectileUpdate();
267 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig);
268
269 //# Pathfinding map
270 void PathfindingMapInit(int width, int height, Vector3 translate, float scale);
271 float PathFindingGetDistance(int mapX, int mapY);
272 Vector2 PathFindingGetGradient(Vector3 world);
273 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY);
274 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells);
275 void PathFindingMapDraw();
276
277 //# UI
278 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth);
279
280 //# Level
281 void DrawLevelGround(Level *level);
282 void DrawEnemyPath(Level *level, Color arrowColor);
283
284 //# variables
285 extern Level *currentLevel;
286 extern Enemy enemies[ENEMY_MAX_COUNT];
287 extern int enemyCount;
288 extern EnemyClassConfig enemyClassConfigs[];
289
290 extern GUIState guiState;
291 extern GameTime gameTime;
292 extern Tower towers[TOWER_MAX_COUNT];
293 extern int towerCount;
294
295 extern Texture2D palette, spriteSheet;
296
297 #endif
1 #include "td_main.h"
2 #include <raymath.h>
3
4 static TowerTypeConfig towerTypeConfigs[TOWER_TYPE_COUNT] = {
5 [TOWER_TYPE_BASE] = {
6 .maxHealth = 10,
7 },
8 [TOWER_TYPE_ARCHER] = {
9 .cooldown = 0.5f,
10 .range = 3.0f,
11 .cost = 6,
12 .maxHealth = 10,
13 .projectileSpeed = 4.0f,
14 .projectileType = PROJECTILE_TYPE_ARROW,
15 .hitEffect = {
16 .damage = 3.0f,
17 }
18 },
19 [TOWER_TYPE_BALLISTA] = {
20 .cooldown = 1.5f,
21 .range = 6.0f,
22 .cost = 9,
23 .maxHealth = 10,
24 .projectileSpeed = 10.0f,
25 .projectileType = PROJECTILE_TYPE_BALLISTA,
26 .hitEffect = {
27 .damage = 6.0f,
28 .pushbackPowerDistance = 0.25f,
29 }
30 },
31 [TOWER_TYPE_CATAPULT] = {
32 .cooldown = 1.7f,
33 .range = 5.0f,
34 .cost = 10,
35 .maxHealth = 10,
36 .projectileSpeed = 3.0f,
37 .projectileType = PROJECTILE_TYPE_CATAPULT,
38 .hitEffect = {
39 .damage = 2.0f,
40 .areaDamageRadius = 1.75f,
41 }
42 },
43 [TOWER_TYPE_WALL] = {
44 .cost = 2,
45 .maxHealth = 10,
46 },
47 };
48
49 Tower towers[TOWER_MAX_COUNT];
50 int towerCount = 0;
51
52 Model towerModels[TOWER_TYPE_COUNT];
53
54 // definition of our archer unit
55 SpriteUnit archerUnit = {
56 .srcRect = {0, 0, 16, 16},
57 .offset = {7, 1},
58 .frameCount = 1,
59 .frameDuration = 0.0f,
60 .srcWeaponIdleRect = {16, 0, 6, 16},
61 .srcWeaponIdleOffset = {8, 0},
62 .srcWeaponCooldownRect = {22, 0, 11, 16},
63 .srcWeaponCooldownOffset = {10, 0},
64 };
65
66 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase)
67 {
68 float xScale = flip ? -1.0f : 1.0f;
69 Camera3D camera = currentLevel->camera;
70 float size = 0.5f;
71 Vector2 offset = (Vector2){ unit.offset.x / 16.0f * size, unit.offset.y / 16.0f * size * xScale };
72 Vector2 scale = (Vector2){ unit.srcRect.width / 16.0f * size, unit.srcRect.height / 16.0f * size };
73 // we want the sprite to face the camera, so we need to calculate the up vector
74 Vector3 forward = Vector3Subtract(camera.target, camera.position);
75 Vector3 up = {0, 1, 0};
76 Vector3 right = Vector3CrossProduct(forward, up);
77 up = Vector3Normalize(Vector3CrossProduct(right, forward));
78
79 Rectangle srcRect = unit.srcRect;
80 if (unit.frameCount > 1)
81 {
82 srcRect.x += (int)(t / unit.frameDuration) % unit.frameCount * srcRect.width;
83 }
84 if (flip)
85 {
86 srcRect.x += srcRect.width;
87 srcRect.width = -srcRect.width;
88 }
89 DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
90
91 if (phase == SPRITE_UNIT_PHASE_WEAPON_COOLDOWN && unit.srcWeaponCooldownRect.width > 0)
92 {
93 offset = (Vector2){ unit.srcWeaponCooldownOffset.x / 16.0f * size, unit.srcWeaponCooldownOffset.y / 16.0f * size };
94 scale = (Vector2){ unit.srcWeaponCooldownRect.width / 16.0f * size, unit.srcWeaponCooldownRect.height / 16.0f * size };
95 srcRect = unit.srcWeaponCooldownRect;
96 if (flip)
97 {
98 // position.x = flip * scale.x * 0.5f;
99 srcRect.x += srcRect.width;
100 srcRect.width = -srcRect.width;
101 offset.x = scale.x - offset.x;
102 }
103 DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
104 }
105 else if (phase == SPRITE_UNIT_PHASE_WEAPON_IDLE && unit.srcWeaponIdleRect.width > 0)
106 {
107 offset = (Vector2){ unit.srcWeaponIdleOffset.x / 16.0f * size, unit.srcWeaponIdleOffset.y / 16.0f * size };
108 scale = (Vector2){ unit.srcWeaponIdleRect.width / 16.0f * size, unit.srcWeaponIdleRect.height / 16.0f * size };
109 srcRect = unit.srcWeaponIdleRect;
110 if (flip)
111 {
112 // position.x = flip * scale.x * 0.5f;
113 srcRect.x += srcRect.width;
114 srcRect.width = -srcRect.width;
115 offset.x = scale.x - offset.x;
116 }
117 DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
118 }
119 }
120
121 void TowerInit()
122 {
123 for (int i = 0; i < TOWER_MAX_COUNT; i++)
124 {
125 towers[i] = (Tower){0};
126 }
127 towerCount = 0;
128
129 towerModels[TOWER_TYPE_BASE] = LoadModel("data/keep.glb");
130 towerModels[TOWER_TYPE_WALL] = LoadModel("data/wall-0000.glb");
131
132 for (int i = 0; i < TOWER_TYPE_COUNT; i++)
133 {
134 if (towerModels[i].materials)
135 {
136 // assign the palette texture to the material of the model (0 is not used afaik)
137 towerModels[i].materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
138 }
139 }
140 }
141
142 static void TowerGunUpdate(Tower *tower)
143 {
144 TowerTypeConfig config = towerTypeConfigs[tower->towerType];
145 if (tower->cooldown <= 0.0f)
146 {
147 Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, config.range);
148 if (enemy)
149 {
150 tower->cooldown = config.cooldown;
151 // shoot the enemy; determine future position of the enemy
152 float bulletSpeed = config.projectileSpeed;
153 Vector2 velocity = enemy->simVelocity;
154 Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0);
155 Vector2 towerPosition = {tower->x, tower->y};
156 float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed;
157 for (int i = 0; i < 8; i++) {
158 velocity = enemy->simVelocity;
159 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0);
160 float distance = Vector2Distance(towerPosition, futurePosition);
161 float eta2 = distance / bulletSpeed;
162 if (fabs(eta - eta2) < 0.01f) {
163 break;
164 }
165 eta = (eta2 + eta) * 0.5f;
166 }
167
168 ProjectileTryAdd(config.projectileType, enemy,
169 (Vector3){towerPosition.x, 1.33f, towerPosition.y},
170 (Vector3){futurePosition.x, 0.25f, futurePosition.y},
171 bulletSpeed, config.hitEffect);
172 enemy->futureDamage += config.hitEffect.damage;
173 tower->lastTargetPosition = futurePosition;
174 }
175 }
176 else
177 {
178 tower->cooldown -= gameTime.deltaTime;
179 }
180 }
181
182 Tower *TowerGetAt(int16_t x, int16_t y)
183 {
184 for (int i = 0; i < towerCount; i++)
185 {
186 if (towers[i].x == x && towers[i].y == y && towers[i].towerType != TOWER_TYPE_NONE)
187 {
188 return &towers[i];
189 }
190 }
191 return 0;
192 }
193
194 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y)
195 {
196 if (towerCount >= TOWER_MAX_COUNT)
197 {
198 return 0;
199 }
200
201 Tower *tower = TowerGetAt(x, y);
202 if (tower)
203 {
204 return 0;
205 }
206
207 tower = &towers[towerCount++];
208 tower->x = x;
209 tower->y = y;
210 tower->towerType = towerType;
211 tower->cooldown = 0.0f;
212 tower->damage = 0.0f;
213 return tower;
214 }
215
216 Tower *GetTowerByType(uint8_t towerType)
217 {
218 for (int i = 0; i < towerCount; i++)
219 {
220 if (towers[i].towerType == towerType)
221 {
222 return &towers[i];
223 }
224 }
225 return 0;
226 }
227
228 int GetTowerCosts(uint8_t towerType)
229 {
230 return towerTypeConfigs[towerType].cost;
231 }
232
233 float TowerGetMaxHealth(Tower *tower)
234 {
235 return towerTypeConfigs[tower->towerType].maxHealth;
236 }
237
238 void TowerDraw()
239 {
240 for (int i = 0; i < towerCount; i++)
241 {
242 Tower tower = towers[i];
243 if (tower.towerType == TOWER_TYPE_NONE)
244 {
245 continue;
246 }
247
248 switch (tower.towerType)
249 {
250 case TOWER_TYPE_ARCHER:
251 {
252 Vector2 screenPosTower = GetWorldToScreen((Vector3){tower.x, 0.0f, tower.y}, currentLevel->camera);
253 Vector2 screenPosTarget = GetWorldToScreen((Vector3){tower.lastTargetPosition.x, 0.0f, tower.lastTargetPosition.y}, currentLevel->camera);
254 DrawModel(towerModels[TOWER_TYPE_WALL], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
255 DrawSpriteUnit(archerUnit, (Vector3){tower.x, 1.0f, tower.y}, 0, screenPosTarget.x > screenPosTower.x,
256 tower.cooldown > 0.2f ? SPRITE_UNIT_PHASE_WEAPON_COOLDOWN : SPRITE_UNIT_PHASE_WEAPON_IDLE);
257 }
258 break;
259 case TOWER_TYPE_BALLISTA:
260 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, BROWN);
261 break;
262 case TOWER_TYPE_CATAPULT:
263 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, DARKGRAY);
264 break;
265 default:
266 if (towerModels[tower.towerType].materials)
267 {
268 DrawModel(towerModels[tower.towerType], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
269 } else {
270 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY);
271 }
272 break;
273 }
274 }
275 }
276
277 void TowerUpdate()
278 {
279 for (int i = 0; i < towerCount; i++)
280 {
281 Tower *tower = &towers[i];
282 switch (tower->towerType)
283 {
284 case TOWER_TYPE_CATAPULT:
285 case TOWER_TYPE_BALLISTA:
286 case TOWER_TYPE_ARCHER:
287 TowerGunUpdate(tower);
288 break;
289 }
290 }
291 }
292
293 void TowerDrawHealthBars(Camera3D camera)
294 {
295 for (int i = 0; i < towerCount; i++)
296 {
297 Tower *tower = &towers[i];
298 if (tower->towerType == TOWER_TYPE_NONE || tower->damage <= 0.0f)
299 {
300 continue;
301 }
302
303 Vector3 position = (Vector3){tower->x, 0.5f, tower->y};
304 float maxHealth = TowerGetMaxHealth(tower);
305 float health = maxHealth - tower->damage;
306 float healthRatio = health / maxHealth;
307
308 DrawHealthBar(camera, position, healthRatio, GREEN, 35.0f);
309 }
310 }
1 #include "td_main.h"
2 #include <raymath.h>
3 #include <stdlib.h>
4 #include <math.h>
5
6 EnemyClassConfig enemyClassConfigs[] = {
7 [ENEMY_TYPE_MINION] = {
8 .health = 10.0f,
9 .speed = 0.6f,
10 .radius = 0.25f,
11 .maxAcceleration = 1.0f,
12 .explosionDamage = 1.0f,
13 .requiredContactTime = 0.5f,
14 .explosionRange = 1.0f,
15 .explosionPushbackPower = 0.25f,
16 .goldValue = 1,
17 },
18 };
19
20 Enemy enemies[ENEMY_MAX_COUNT];
21 int enemyCount = 0;
22
23 SpriteUnit enemySprites[] = {
24 [ENEMY_TYPE_MINION] = {
25 .srcRect = {0, 16, 16, 16},
26 .offset = {8.0f, 0.0f},
27 .frameCount = 6,
28 .frameDuration = 0.1f,
29 },
30 };
31
32 void EnemyInit()
33 {
34 for (int i = 0; i < ENEMY_MAX_COUNT; i++)
35 {
36 enemies[i] = (Enemy){0};
37 }
38 enemyCount = 0;
39 }
40
41 float EnemyGetCurrentMaxSpeed(Enemy *enemy)
42 {
43 return enemyClassConfigs[enemy->enemyType].speed;
44 }
45
46 float EnemyGetMaxHealth(Enemy *enemy)
47 {
48 return enemyClassConfigs[enemy->enemyType].health;
49 }
50
51 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY)
52 {
53 int16_t castleX = 0;
54 int16_t castleY = 0;
55 int16_t dx = castleX - currentX;
56 int16_t dy = castleY - currentY;
57 if (dx == 0 && dy == 0)
58 {
59 *nextX = currentX;
60 *nextY = currentY;
61 return 1;
62 }
63 Vector2 gradient = PathFindingGetGradient((Vector3){currentX, 0, currentY});
64
65 if (gradient.x == 0 && gradient.y == 0)
66 {
67 *nextX = currentX;
68 *nextY = currentY;
69 return 1;
70 }
71
72 if (fabsf(gradient.x) > fabsf(gradient.y))
73 {
74 *nextX = currentX + (int16_t)(gradient.x > 0.0f ? 1 : -1);
75 *nextY = currentY;
76 return 0;
77 }
78 *nextX = currentX;
79 *nextY = currentY + (int16_t)(gradient.y > 0.0f ? 1 : -1);
80 return 0;
81 }
82
83
84 // this function predicts the movement of the unit for the next deltaT seconds
85 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount)
86 {
87 const float pointReachedDistance = 0.25f;
88 const float pointReachedDistance2 = pointReachedDistance * pointReachedDistance;
89 const float maxSimStepTime = 0.015625f;
90
91 float maxAcceleration = enemyClassConfigs[enemy->enemyType].maxAcceleration;
92 float maxSpeed = EnemyGetCurrentMaxSpeed(enemy);
93 int16_t nextX = enemy->nextX;
94 int16_t nextY = enemy->nextY;
95 Vector2 position = enemy->simPosition;
96 int passedCount = 0;
97 for (float t = 0.0f; t < deltaT; t += maxSimStepTime)
98 {
99 float stepTime = fminf(deltaT - t, maxSimStepTime);
100 Vector2 target = (Vector2){nextX, nextY};
101 float speed = Vector2Length(*velocity);
102 // draw the target position for debugging
103 //DrawCubeWires((Vector3){target.x, 0.2f, target.y}, 0.1f, 0.4f, 0.1f, RED);
104 Vector2 lookForwardPos = Vector2Add(position, Vector2Scale(*velocity, speed));
105 if (Vector2DistanceSqr(target, lookForwardPos) <= pointReachedDistance2)
106 {
107 // we reached the target position, let's move to the next waypoint
108 EnemyGetNextPosition(nextX, nextY, &nextX, &nextY);
109 target = (Vector2){nextX, nextY};
110 // track how many waypoints we passed
111 passedCount++;
112 }
113
114 // acceleration towards the target
115 Vector2 unitDirection = Vector2Normalize(Vector2Subtract(target, lookForwardPos));
116 Vector2 acceleration = Vector2Scale(unitDirection, maxAcceleration * stepTime);
117 *velocity = Vector2Add(*velocity, acceleration);
118
119 // limit the speed to the maximum speed
120 if (speed > maxSpeed)
121 {
122 *velocity = Vector2Scale(*velocity, maxSpeed / speed);
123 }
124
125 // move the enemy
126 position = Vector2Add(position, Vector2Scale(*velocity, stepTime));
127 }
128
129 if (waypointPassedCount)
130 {
131 (*waypointPassedCount) = passedCount;
132 }
133
134 return position;
135 }
136
137 void EnemyDraw()
138 {
139 for (int i = 0; i < enemyCount; i++)
140 {
141 Enemy enemy = enemies[i];
142 if (enemy.enemyType == ENEMY_TYPE_NONE)
143 {
144 continue;
145 }
146
147 Vector2 position = EnemyGetPosition(&enemy, gameTime.time - enemy.startMovingTime, &enemy.simVelocity, 0);
148
149 // don't draw any trails for now; might replace this with footprints later
150 // if (enemy.movePathCount > 0)
151 // {
152 // Vector3 p = {enemy.movePath[0].x, 0.2f, enemy.movePath[0].y};
153 // DrawLine3D(p, (Vector3){position.x, 0.2f, position.y}, GREEN);
154 // }
155 // for (int j = 1; j < enemy.movePathCount; j++)
156 // {
157 // Vector3 p = {enemy.movePath[j - 1].x, 0.2f, enemy.movePath[j - 1].y};
158 // Vector3 q = {enemy.movePath[j].x, 0.2f, enemy.movePath[j].y};
159 // DrawLine3D(p, q, GREEN);
160 // }
161
162 switch (enemy.enemyType)
163 {
164 case ENEMY_TYPE_MINION:
165 DrawSpriteUnit(enemySprites[ENEMY_TYPE_MINION], (Vector3){position.x, 0.0f, position.y},
166 enemy.walkedDistance, 0, 0);
167 break;
168 }
169 }
170 }
171
172 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource)
173 {
174 // damage the tower
175 float explosionDamge = enemyClassConfigs[enemy->enemyType].explosionDamage;
176 float explosionRange = enemyClassConfigs[enemy->enemyType].explosionRange;
177 float explosionPushbackPower = enemyClassConfigs[enemy->enemyType].explosionPushbackPower;
178 float explosionRange2 = explosionRange * explosionRange;
179 tower->damage += enemyClassConfigs[enemy->enemyType].explosionDamage;
180 // explode the enemy
181 if (tower->damage >= TowerGetMaxHealth(tower))
182 {
183 tower->towerType = TOWER_TYPE_NONE;
184 }
185
186 ParticleAdd(PARTICLE_TYPE_EXPLOSION,
187 explosionSource,
188 (Vector3){0, 0.1f, 0}, (Vector3){1.0f, 1.0f, 1.0f}, 1.0f);
189
190 enemy->enemyType = ENEMY_TYPE_NONE;
191
192 // push back enemies & dealing damage
193 for (int i = 0; i < enemyCount; i++)
194 {
195 Enemy *other = &enemies[i];
196 if (other->enemyType == ENEMY_TYPE_NONE)
197 {
198 continue;
199 }
200 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, other->simPosition);
201 if (distanceSqr > 0 && distanceSqr < explosionRange2)
202 {
203 Vector2 direction = Vector2Normalize(Vector2Subtract(other->simPosition, enemy->simPosition));
204 other->simPosition = Vector2Add(other->simPosition, Vector2Scale(direction, explosionPushbackPower));
205 EnemyAddDamage(other, explosionDamge);
206 }
207 }
208 }
209
210 void EnemyUpdate()
211 {
212 const float castleX = 0;
213 const float castleY = 0;
214 const float maxPathDistance2 = 0.25f * 0.25f;
215
216 for (int i = 0; i < enemyCount; i++)
217 {
218 Enemy *enemy = &enemies[i];
219 if (enemy->enemyType == ENEMY_TYPE_NONE)
220 {
221 continue;
222 }
223
224 int waypointPassedCount = 0;
225 Vector2 prevPosition = enemy->simPosition;
226 enemy->simPosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &enemy->simVelocity, &waypointPassedCount);
227 enemy->startMovingTime = gameTime.time;
228 enemy->walkedDistance += Vector2Distance(prevPosition, enemy->simPosition);
229 // track path of unit
230 if (enemy->movePathCount == 0 || Vector2DistanceSqr(enemy->simPosition, enemy->movePath[0]) > maxPathDistance2)
231 {
232 for (int j = ENEMY_MAX_PATH_COUNT - 1; j > 0; j--)
233 {
234 enemy->movePath[j] = enemy->movePath[j - 1];
235 }
236 enemy->movePath[0] = enemy->simPosition;
237 if (++enemy->movePathCount > ENEMY_MAX_PATH_COUNT)
238 {
239 enemy->movePathCount = ENEMY_MAX_PATH_COUNT;
240 }
241 }
242
243 if (waypointPassedCount > 0)
244 {
245 enemy->currentX = enemy->nextX;
246 enemy->currentY = enemy->nextY;
247 if (EnemyGetNextPosition(enemy->currentX, enemy->currentY, &enemy->nextX, &enemy->nextY) &&
248 Vector2DistanceSqr(enemy->simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
249 {
250 // enemy reached the castle; remove it
251 enemy->enemyType = ENEMY_TYPE_NONE;
252 continue;
253 }
254 }
255 }
256
257 // handle collisions between enemies
258 for (int i = 0; i < enemyCount - 1; i++)
259 {
260 Enemy *enemyA = &enemies[i];
261 if (enemyA->enemyType == ENEMY_TYPE_NONE)
262 {
263 continue;
264 }
265 for (int j = i + 1; j < enemyCount; j++)
266 {
267 Enemy *enemyB = &enemies[j];
268 if (enemyB->enemyType == ENEMY_TYPE_NONE)
269 {
270 continue;
271 }
272 float distanceSqr = Vector2DistanceSqr(enemyA->simPosition, enemyB->simPosition);
273 float radiusA = enemyClassConfigs[enemyA->enemyType].radius;
274 float radiusB = enemyClassConfigs[enemyB->enemyType].radius;
275 float radiusSum = radiusA + radiusB;
276 if (distanceSqr < radiusSum * radiusSum && distanceSqr > 0.001f)
277 {
278 // collision
279 float distance = sqrtf(distanceSqr);
280 float overlap = radiusSum - distance;
281 // move the enemies apart, but softly; if we have a clog of enemies,
282 // moving them perfectly apart can cause them to jitter
283 float positionCorrection = overlap / 5.0f;
284 Vector2 direction = (Vector2){
285 (enemyB->simPosition.x - enemyA->simPosition.x) / distance * positionCorrection,
286 (enemyB->simPosition.y - enemyA->simPosition.y) / distance * positionCorrection};
287 enemyA->simPosition = Vector2Subtract(enemyA->simPosition, direction);
288 enemyB->simPosition = Vector2Add(enemyB->simPosition, direction);
289 }
290 }
291 }
292
293 // handle collisions between enemies and towers
294 for (int i = 0; i < enemyCount; i++)
295 {
296 Enemy *enemy = &enemies[i];
297 if (enemy->enemyType == ENEMY_TYPE_NONE)
298 {
299 continue;
300 }
301 enemy->contactTime -= gameTime.deltaTime;
302 if (enemy->contactTime < 0.0f)
303 {
304 enemy->contactTime = 0.0f;
305 }
306
307 float enemyRadius = enemyClassConfigs[enemy->enemyType].radius;
308 // linear search over towers; could be optimized by using path finding tower map,
309 // but for now, we keep it simple
310 for (int j = 0; j < towerCount; j++)
311 {
312 Tower *tower = &towers[j];
313 if (tower->towerType == TOWER_TYPE_NONE)
314 {
315 continue;
316 }
317 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){tower->x, tower->y});
318 float combinedRadius = enemyRadius + 0.708; // sqrt(0.5^2 + 0.5^2), corner-center distance of square with side length 1
319 if (distanceSqr > combinedRadius * combinedRadius)
320 {
321 continue;
322 }
323 // potential collision; square / circle intersection
324 float dx = tower->x - enemy->simPosition.x;
325 float dy = tower->y - enemy->simPosition.y;
326 float absDx = fabsf(dx);
327 float absDy = fabsf(dy);
328 Vector3 contactPoint = {0};
329 if (absDx <= 0.5f && absDx <= absDy) {
330 // vertical collision; push the enemy out horizontally
331 float overlap = enemyRadius + 0.5f - absDy;
332 if (overlap < 0.0f)
333 {
334 continue;
335 }
336 float direction = dy > 0.0f ? -1.0f : 1.0f;
337 enemy->simPosition.y += direction * overlap;
338 contactPoint = (Vector3){enemy->simPosition.x, 0.2f, tower->y + direction * 0.5f};
339 }
340 else if (absDy <= 0.5f && absDy <= absDx)
341 {
342 // horizontal collision; push the enemy out vertically
343 float overlap = enemyRadius + 0.5f - absDx;
344 if (overlap < 0.0f)
345 {
346 continue;
347 }
348 float direction = dx > 0.0f ? -1.0f : 1.0f;
349 enemy->simPosition.x += direction * overlap;
350 contactPoint = (Vector3){tower->x + direction * 0.5f, 0.2f, enemy->simPosition.y};
351 }
352 else
353 {
354 // possible collision with a corner
355 float cornerDX = dx > 0.0f ? -0.5f : 0.5f;
356 float cornerDY = dy > 0.0f ? -0.5f : 0.5f;
357 float cornerX = tower->x + cornerDX;
358 float cornerY = tower->y + cornerDY;
359 float cornerDistanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){cornerX, cornerY});
360 if (cornerDistanceSqr > enemyRadius * enemyRadius)
361 {
362 continue;
363 }
364 // push the enemy out along the diagonal
365 float cornerDistance = sqrtf(cornerDistanceSqr);
366 float overlap = enemyRadius - cornerDistance;
367 float directionX = cornerDistance > 0.0f ? (cornerX - enemy->simPosition.x) / cornerDistance : -cornerDX;
368 float directionY = cornerDistance > 0.0f ? (cornerY - enemy->simPosition.y) / cornerDistance : -cornerDY;
369 enemy->simPosition.x -= directionX * overlap;
370 enemy->simPosition.y -= directionY * overlap;
371 contactPoint = (Vector3){cornerX, 0.2f, cornerY};
372 }
373
374 if (enemyClassConfigs[enemy->enemyType].explosionDamage > 0.0f)
375 {
376 enemy->contactTime += gameTime.deltaTime * 2.0f; // * 2 to undo the subtraction above
377 if (enemy->contactTime >= enemyClassConfigs[enemy->enemyType].requiredContactTime)
378 {
379 EnemyTriggerExplode(enemy, tower, contactPoint);
380 }
381 }
382 }
383 }
384 }
385
386 EnemyId EnemyGetId(Enemy *enemy)
387 {
388 return (EnemyId){enemy - enemies, enemy->generation};
389 }
390
391 Enemy *EnemyTryResolve(EnemyId enemyId)
392 {
393 if (enemyId.index >= ENEMY_MAX_COUNT)
394 {
395 return 0;
396 }
397 Enemy *enemy = &enemies[enemyId.index];
398 if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE)
399 {
400 return 0;
401 }
402 return enemy;
403 }
404
405 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY)
406 {
407 Enemy *spawn = 0;
408 for (int i = 0; i < enemyCount; i++)
409 {
410 Enemy *enemy = &enemies[i];
411 if (enemy->enemyType == ENEMY_TYPE_NONE)
412 {
413 spawn = enemy;
414 break;
415 }
416 }
417
418 if (enemyCount < ENEMY_MAX_COUNT && !spawn)
419 {
420 spawn = &enemies[enemyCount++];
421 }
422
423 if (spawn)
424 {
425 spawn->currentX = currentX;
426 spawn->currentY = currentY;
427 spawn->nextX = currentX;
428 spawn->nextY = currentY;
429 spawn->simPosition = (Vector2){currentX, currentY};
430 spawn->simVelocity = (Vector2){0, 0};
431 spawn->enemyType = enemyType;
432 spawn->startMovingTime = gameTime.time;
433 spawn->damage = 0.0f;
434 spawn->futureDamage = 0.0f;
435 spawn->generation++;
436 spawn->movePathCount = 0;
437 spawn->walkedDistance = 0.0f;
438 }
439
440 return spawn;
441 }
442
443 int EnemyAddDamageRange(Vector2 position, float range, float damage)
444 {
445 int count = 0;
446 float range2 = range * range;
447 for (int i = 0; i < enemyCount; i++)
448 {
449 Enemy *enemy = &enemies[i];
450 if (enemy->enemyType == ENEMY_TYPE_NONE)
451 {
452 continue;
453 }
454 float distance2 = Vector2DistanceSqr(position, enemy->simPosition);
455 if (distance2 <= range2)
456 {
457 EnemyAddDamage(enemy, damage);
458 count++;
459 }
460 }
461 return count;
462 }
463
464 int EnemyAddDamage(Enemy *enemy, float damage)
465 {
466 enemy->damage += damage;
467 if (enemy->damage >= EnemyGetMaxHealth(enemy))
468 {
469 currentLevel->playerGold += enemyClassConfigs[enemy->enemyType].goldValue;
470 enemy->enemyType = ENEMY_TYPE_NONE;
471 return 1;
472 }
473
474 return 0;
475 }
476
477 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
478 {
479 int16_t castleX = 0;
480 int16_t castleY = 0;
481 Enemy* closest = 0;
482 int16_t closestDistance = 0;
483 float range2 = range * range;
484 for (int i = 0; i < enemyCount; i++)
485 {
486 Enemy* enemy = &enemies[i];
487 if (enemy->enemyType == ENEMY_TYPE_NONE)
488 {
489 continue;
490 }
491 float maxHealth = EnemyGetMaxHealth(enemy);
492 if (enemy->futureDamage >= maxHealth)
493 {
494 // ignore enemies that will die soon
495 continue;
496 }
497 int16_t dx = castleX - enemy->currentX;
498 int16_t dy = castleY - enemy->currentY;
499 int16_t distance = abs(dx) + abs(dy);
500 if (!closest || distance < closestDistance)
501 {
502 float tdx = towerX - enemy->currentX;
503 float tdy = towerY - enemy->currentY;
504 float tdistance2 = tdx * tdx + tdy * tdy;
505 if (tdistance2 <= range2)
506 {
507 closest = enemy;
508 closestDistance = distance;
509 }
510 }
511 }
512 return closest;
513 }
514
515 int EnemyCount()
516 {
517 int count = 0;
518 for (int i = 0; i < enemyCount; i++)
519 {
520 if (enemies[i].enemyType != ENEMY_TYPE_NONE)
521 {
522 count++;
523 }
524 }
525 return count;
526 }
527
528 void EnemyDrawHealthbars(Camera3D camera)
529 {
530 for (int i = 0; i < enemyCount; i++)
531 {
532 Enemy *enemy = &enemies[i];
533 if (enemy->enemyType == ENEMY_TYPE_NONE || enemy->damage == 0.0f)
534 {
535 continue;
536 }
537 Vector3 position = (Vector3){enemy->simPosition.x, 0.5f, enemy->simPosition.y};
538 float maxHealth = EnemyGetMaxHealth(enemy);
539 float health = maxHealth - enemy->damage;
540 float healthRatio = health / maxHealth;
541
542 DrawHealthBar(camera, position, healthRatio, GREEN, 15.0f);
543 }
544 }
1 #include "td_main.h"
2 #include <raymath.h>
3
4 static Projectile projectiles[PROJECTILE_MAX_COUNT];
5 static int projectileCount = 0;
6
7 typedef struct ProjectileConfig
8 {
9 float arcFactor;
10 Color color;
11 Color trailColor;
12 } ProjectileConfig;
13
14 ProjectileConfig projectileConfigs[] = {
15 [PROJECTILE_TYPE_ARROW] = {
16 .arcFactor = 0.15f,
17 .color = RED,
18 .trailColor = BROWN,
19 },
20 [PROJECTILE_TYPE_CATAPULT] = {
21 .arcFactor = 0.5f,
22 .color = RED,
23 .trailColor = GRAY,
24 },
25 [PROJECTILE_TYPE_BALLISTA] = {
26 .arcFactor = 0.025f,
27 .color = RED,
28 .trailColor = BROWN,
29 },
30 };
31
32 void ProjectileInit()
33 {
34 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
35 {
36 projectiles[i] = (Projectile){0};
37 }
38 }
39
40 void ProjectileDraw()
41 {
42 for (int i = 0; i < projectileCount; i++)
43 {
44 Projectile projectile = projectiles[i];
45 if (projectile.projectileType == PROJECTILE_TYPE_NONE)
46 {
47 continue;
48 }
49 float transition = (gameTime.time - projectile.shootTime) / (projectile.arrivalTime - projectile.shootTime);
50 if (transition >= 1.0f)
51 {
52 continue;
53 }
54
55 ProjectileConfig config = projectileConfigs[projectile.projectileType];
56 for (float transitionOffset = 0.0f; transitionOffset < 1.0f; transitionOffset += 0.1f)
57 {
58 float t = transition + transitionOffset * 0.3f;
59 if (t > 1.0f)
60 {
61 break;
62 }
63 Vector3 position = Vector3Lerp(projectile.position, projectile.target, t);
64 Color color = config.color;
65 color = ColorLerp(config.trailColor, config.color, transitionOffset * transitionOffset);
66 // fake a ballista flight path using parabola equation
67 float parabolaT = t - 0.5f;
68 parabolaT = 1.0f - 4.0f * parabolaT * parabolaT;
69 position.y += config.arcFactor * parabolaT * projectile.distance;
70
71 float size = 0.06f * (transitionOffset + 0.25f);
72 DrawCube(position, size, size, size, color);
73 }
74 }
75 }
76
77 void ProjectileUpdate()
78 {
79 for (int i = 0; i < projectileCount; i++)
80 {
81 Projectile *projectile = &projectiles[i];
82 if (projectile->projectileType == PROJECTILE_TYPE_NONE)
83 {
84 continue;
85 }
86 float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime);
87 if (transition >= 1.0f)
88 {
89 projectile->projectileType = PROJECTILE_TYPE_NONE;
90 Enemy *enemy = EnemyTryResolve(projectile->targetEnemy);
91 if (enemy && projectile->hitEffectConfig.pushbackPowerDistance > 0.0f)
92 {
93 Vector2 direction = Vector2Normalize(Vector2Subtract((Vector2){projectile->target.x, projectile->target.z}, enemy->simPosition));
94 enemy->simPosition = Vector2Add(enemy->simPosition, Vector2Scale(direction, projectile->hitEffectConfig.pushbackPowerDistance));
95 }
96
97 if (projectile->hitEffectConfig.areaDamageRadius > 0.0f)
98 {
99 EnemyAddDamageRange((Vector2){projectile->target.x, projectile->target.z}, projectile->hitEffectConfig.areaDamageRadius, projectile->hitEffectConfig.damage);
100 // pancaked sphere explosion
101 float r = projectile->hitEffectConfig.areaDamageRadius;
102 ParticleAdd(PARTICLE_TYPE_EXPLOSION, projectile->target, (Vector3){0}, (Vector3){r, r * 0.2f, r}, 0.33f);
103 }
104 else if (projectile->hitEffectConfig.damage > 0.0f && enemy)
105 {
106 EnemyAddDamage(enemy, projectile->hitEffectConfig.damage);
107 }
108 continue;
109 }
110 }
111 }
112
113 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig)
114 {
115 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
116 {
117 Projectile *projectile = &projectiles[i];
118 if (projectile->projectileType == PROJECTILE_TYPE_NONE)
119 {
120 projectile->projectileType = projectileType;
121 projectile->shootTime = gameTime.time;
122 float distance = Vector3Distance(position, target);
123 projectile->arrivalTime = gameTime.time + distance / speed;
124 projectile->position = position;
125 projectile->target = target;
126 projectile->directionNormal = Vector3Scale(Vector3Subtract(target, position), 1.0f / distance);
127 projectile->distance = distance;
128 projectile->targetEnemy = EnemyGetId(enemy);
129 projectileCount = projectileCount <= i ? i + 1 : projectileCount;
130 projectile->hitEffectConfig = hitEffectConfig;
131 return projectile;
132 }
133 }
134 return 0;
135 }
1 #include "td_main.h"
2 #include <raymath.h>
3 #include <rlgl.h>
4
5 static Particle particles[PARTICLE_MAX_COUNT];
6 static int particleCount = 0;
7
8 void ParticleInit()
9 {
10 for (int i = 0; i < PARTICLE_MAX_COUNT; i++)
11 {
12 particles[i] = (Particle){0};
13 }
14 particleCount = 0;
15 }
16
17 static void DrawExplosionParticle(Particle *particle, float transition)
18 {
19 Vector3 scale = particle->scale;
20 float size = 1.0f * (1.0f - transition);
21 Color startColor = WHITE;
22 Color endColor = RED;
23 Color color = ColorLerp(startColor, endColor, transition);
24
25 rlPushMatrix();
26 rlTranslatef(particle->position.x, particle->position.y, particle->position.z);
27 rlScalef(scale.x, scale.y, scale.z);
28 DrawSphere(Vector3Zero(), size, color);
29 rlPopMatrix();
30 }
31
32 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime)
33 {
34 if (particleCount >= PARTICLE_MAX_COUNT)
35 {
36 return;
37 }
38
39 int index = -1;
40 for (int i = 0; i < particleCount; i++)
41 {
42 if (particles[i].particleType == PARTICLE_TYPE_NONE)
43 {
44 index = i;
45 break;
46 }
47 }
48
49 if (index == -1)
50 {
51 index = particleCount++;
52 }
53
54 Particle *particle = &particles[index];
55 particle->particleType = particleType;
56 particle->spawnTime = gameTime.time;
57 particle->lifetime = lifetime;
58 particle->position = position;
59 particle->velocity = velocity;
60 particle->scale = scale;
61 }
62
63 void ParticleUpdate()
64 {
65 for (int i = 0; i < particleCount; i++)
66 {
67 Particle *particle = &particles[i];
68 if (particle->particleType == PARTICLE_TYPE_NONE)
69 {
70 continue;
71 }
72
73 float age = gameTime.time - particle->spawnTime;
74
75 if (particle->lifetime > age)
76 {
77 particle->position = Vector3Add(particle->position, Vector3Scale(particle->velocity, gameTime.deltaTime));
78 }
79 else {
80 particle->particleType = PARTICLE_TYPE_NONE;
81 }
82 }
83 }
84
85 void ParticleDraw()
86 {
87 for (int i = 0; i < particleCount; i++)
88 {
89 Particle particle = particles[i];
90 if (particle.particleType == PARTICLE_TYPE_NONE)
91 {
92 continue;
93 }
94
95 float age = gameTime.time - particle.spawnTime;
96 float transition = age / particle.lifetime;
97 switch (particle.particleType)
98 {
99 case PARTICLE_TYPE_EXPLOSION:
100 DrawExplosionParticle(&particle, transition);
101 break;
102 default:
103 DrawCube(particle.position, 0.3f, 0.5f, 0.3f, RED);
104 break;
105 }
106 }
107 }
1 #include "raylib.h"
2 #include "preferred_size.h"
3
4 // Since the canvas size is not known at compile time, we need to query it at runtime;
5 // the following platform specific code obtains the canvas size and we will use this
6 // size as the preferred size for the window at init time. We're ignoring here the
7 // possibility of the canvas size changing during runtime - this would require to
8 // poll the canvas size in the game loop or establishing a callback to be notified
9
10 #ifdef PLATFORM_WEB
11 #include <emscripten.h>
12 EMSCRIPTEN_RESULT emscripten_get_element_css_size(const char *target, double *width, double *height);
13
14 void GetPreferredSize(int *screenWidth, int *screenHeight)
15 {
16 double canvasWidth, canvasHeight;
17 emscripten_get_element_css_size("#" CANVAS_NAME, &canvasWidth, &canvasHeight);
18 *screenWidth = (int)canvasWidth;
19 *screenHeight = (int)canvasHeight;
20 TraceLog(LOG_INFO, "preferred size for %s: %d %d", CANVAS_NAME, *screenWidth, *screenHeight);
21 }
22
23 int IsPaused()
24 {
25 const char *js = "(function(){\n"
26 " var canvas = document.getElementById(\"" CANVAS_NAME "\");\n"
27 " var rect = canvas.getBoundingClientRect();\n"
28 " var isVisible = (\n"
29 " rect.top >= 0 &&\n"
30 " rect.left >= 0 &&\n"
31 " rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&\n"
32 " rect.right <= (window.innerWidth || document.documentElement.clientWidth)\n"
33 " );\n"
34 " return isVisible ? 0 : 1;\n"
35 "})()";
36 return emscripten_run_script_int(js);
37 }
38
39 #else
40 void GetPreferredSize(int *screenWidth, int *screenHeight)
41 {
42 *screenWidth = 600;
43 *screenHeight = 240;
44 }
45 int IsPaused()
46 {
47 return 0;
48 }
49 #endif
1 #ifndef PREFERRED_SIZE_H
2 #define PREFERRED_SIZE_H
3
4 void GetPreferredSize(int *screenWidth, int *screenHeight);
5 int IsPaused();
6
7 #endif
The result looks good! There's no z-fighting anymore - though ... you have to be aware that this only works when the precision of the depth buffer is high enough. Most modern GPUs use a 24 or 32 bit depth buffer, which is more than enough for this kind of scene. But especially on mobile devices and web browsers, the depth buffer can be as low as 16 bit where this technique may not work well and z-fighting will still be visible. For an effect like this where the z-fights are not very noticeable, it's an acceptable trade-off. And a lot of times, the camera clipping planes can be optimized to avoid this issue: The smaller the near and far clipping planes of the camera are, the more bits of the depth buffer are used during rendering, increasing the overall precision.
Summary
We have now a nice looking path preview that shows the path the enemies will take and the path preview is updated when hovering over a tower and when placing new towers.
Visually and gameplay-wise, this is quite a good improvement, making it easier to anticipate how the enemies will move through the level.
On touch only devices, the building placement is however almost broken and needs to be fixed. This will be the next topic to tackle.