Simple tower defense tutorial, part 14: Cartoonish tower movement (1/2)
While the movement looks now smooth, the motion is still static since the tower is staying upright all the time. When watching cartoons, motion is highly exaggerated and playful. Squash and stretch and anticipation are common techniques to make the motion more lively. There is a good youtube series on the 12 principles of animation that is worth watching if you are interested in this topic.
Applying these principles to the behavior of a game is what people often refer to as "juicing" the game.
I want to excercise this here by making the tower placement mode more lively and showing what it involves. The fundamental idea of juicing up gameplay elements is to make every action a joyful interaction with the game. While it won't make a boring game more fun, it can carry the player over more tedious parts of a game.
To make this blog post a little less dry, I will give a glimpse on what we are going to achieve at the end of the next part - this part is covering the math for the simulation, the next part how to apply it when drawing the tower model:

I hope this is a good motivation to follow through this post that is going to be more math heavy. In the previous part, I pointed out that animating via math functions can be quite difficult, but learning how to animate via code and math is going to be crucial for what we want to achieve in this part:
Like we've seen for the animated arrows on the tower placement, these animation principles can be expressed as formulas, which enables us to let the calculations interact with the game state and making them respond to the player's actions. Since the player can interact with the game at any time, having adaptable formulas that describe the animation is practically the only way how to achieve the goal of making the behavior of the tower placement cartoonish.
This constant uncertainty is a great challenge and the reason why using formulas is so powerful. Premade animations or tweens won't cut it here, because they are not flexible enough to adapt to the player's actions.
The solution I will use here a spring based simulation to make the tower wobble around when it is moved but also let it squash and stretch when it is accelerated or decelerated. Imagine it as a bouncy spring that bounces when you move it around.
When using an engine like Unity, you could use the physics engine to achieve a similar effect, though doing the calculations ourselves will give us more control over the behavior and make it easier to adapt to the game state.
Here's the plan: When we place the tower, we initialize a struct that holds two values: A position of a spring tip and its velocity. The spring has a rest length and we will use a function to synchronize the spring tip in an upright position with the tower position.
The effect will be this: When the tower is moved, the spring tip will rest in its previous position for a moment before the momentum is transported through the spring to the tip. This will induce a wobbling effect. We use the spring tip to rotate the tower around its base so it looks like we grabbed it at its base and pull it to a new location. Furthermore, when the spring gets stretched or compressed, we will scale the tower to make it look like it is squashed or stretched. This should provide us with a nice cartoonish effect that makes the tower look more lively.
Let's start with displaying an abstraction of the spring simulation:
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 Model greenArrowModel = {0};
20
21 Texture2D palette, spriteSheet;
22
23 Level levels[] = {
24 [0] = {
25 .state = LEVEL_STATE_BUILDING,
26 .initialGold = 20,
27 .waves[0] = {
28 .enemyType = ENEMY_TYPE_MINION,
29 .wave = 0,
30 .count = 5,
31 .interval = 2.5f,
32 .delay = 1.0f,
33 .spawnPosition = {2, 6},
34 },
35 .waves[1] = {
36 .enemyType = ENEMY_TYPE_MINION,
37 .wave = 0,
38 .count = 5,
39 .interval = 2.5f,
40 .delay = 1.0f,
41 .spawnPosition = {-2, 6},
42 },
43 .waves[2] = {
44 .enemyType = ENEMY_TYPE_MINION,
45 .wave = 1,
46 .count = 20,
47 .interval = 1.5f,
48 .delay = 1.0f,
49 .spawnPosition = {0, 6},
50 },
51 .waves[3] = {
52 .enemyType = ENEMY_TYPE_MINION,
53 .wave = 2,
54 .count = 30,
55 .interval = 1.2f,
56 .delay = 1.0f,
57 .spawnPosition = {0, 6},
58 }
59 },
60 };
61
62 Level *currentLevel = levels;
63
64 //# Game
65
66 static Model LoadGLBModel(char *filename)
67 {
68 Model model = LoadModel(TextFormat("data/%s.glb",filename));
69 for (int i = 0; i < model.materialCount; i++)
70 {
71 model.materials[i].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
72 }
73 return model;
74 }
75
76 void LoadAssets()
77 {
78 // load a sprite sheet that contains all units
79 spriteSheet = LoadTexture("data/spritesheet.png");
80 SetTextureFilter(spriteSheet, TEXTURE_FILTER_BILINEAR);
81
82 // we'll use a palette texture to colorize the all buildings and environment art
83 palette = LoadTexture("data/palette.png");
84 // The texture uses gradients on very small space, so we'll enable bilinear filtering
85 SetTextureFilter(palette, TEXTURE_FILTER_BILINEAR);
86
87 floorTileAModel = LoadGLBModel("floor-tile-a");
88 floorTileBModel = LoadGLBModel("floor-tile-b");
89 treeModel[0] = LoadGLBModel("leaftree-large-1-a");
90 treeModel[1] = LoadGLBModel("leaftree-large-1-b");
91 firTreeModel[0] = LoadGLBModel("firtree-1-a");
92 firTreeModel[1] = LoadGLBModel("firtree-1-b");
93 rockModels[0] = LoadGLBModel("rock-1");
94 rockModels[1] = LoadGLBModel("rock-2");
95 rockModels[2] = LoadGLBModel("rock-3");
96 rockModels[3] = LoadGLBModel("rock-4");
97 rockModels[4] = LoadGLBModel("rock-5");
98 grassPatchModel[0] = LoadGLBModel("grass-patch-1");
99
100 pathArrowModel = LoadGLBModel("direction-arrow-x");
101 greenArrowModel = LoadGLBModel("green-arrow");
102 }
103
104 void InitLevel(Level *level)
105 {
106 level->seed = (int)(GetTime() * 100.0f);
107
108 TowerInit();
109 EnemyInit();
110 ProjectileInit();
111 ParticleInit();
112 TowerTryAdd(TOWER_TYPE_BASE, 0, 0);
113
114 level->placementMode = 0;
115 level->state = LEVEL_STATE_BUILDING;
116 level->nextState = LEVEL_STATE_NONE;
117 level->playerGold = level->initialGold;
118 level->currentWave = 0;
119 level->placementX = -1;
120 level->placementY = 0;
121
122 Camera *camera = &level->camera;
123 camera->position = (Vector3){4.0f, 8.0f, 8.0f};
124 camera->target = (Vector3){0.0f, 0.0f, 0.0f};
125 camera->up = (Vector3){0.0f, 1.0f, 0.0f};
126 camera->fovy = 10.0f;
127 camera->projection = CAMERA_ORTHOGRAPHIC;
128 }
129
130 void DrawLevelHud(Level *level)
131 {
132 const char *text = TextFormat("Gold: %d", level->playerGold);
133 Font font = GetFontDefault();
134 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 120, 10}, font.baseSize * 2.0f, 2.0f, BLACK);
135 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 122, 8}, font.baseSize * 2.0f, 2.0f, YELLOW);
136 }
137
138 void DrawLevelReportLostWave(Level *level)
139 {
140 BeginMode3D(level->camera);
141 DrawLevelGround(level);
142 TowerDraw();
143 EnemyDraw();
144 ProjectileDraw();
145 ParticleDraw();
146 guiState.isBlocked = 0;
147 EndMode3D();
148
149 TowerDrawHealthBars(level->camera);
150
151 const char *text = "Wave lost";
152 int textWidth = MeasureText(text, 20);
153 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
154
155 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
156 {
157 level->nextState = LEVEL_STATE_RESET;
158 }
159 }
160
161 int HasLevelNextWave(Level *level)
162 {
163 for (int i = 0; i < 10; i++)
164 {
165 EnemyWave *wave = &level->waves[i];
166 if (wave->wave == level->currentWave)
167 {
168 return 1;
169 }
170 }
171 return 0;
172 }
173
174 void DrawLevelReportWonWave(Level *level)
175 {
176 BeginMode3D(level->camera);
177 DrawLevelGround(level);
178 TowerDraw();
179 EnemyDraw();
180 ProjectileDraw();
181 ParticleDraw();
182 guiState.isBlocked = 0;
183 EndMode3D();
184
185 TowerDrawHealthBars(level->camera);
186
187 const char *text = "Wave won";
188 int textWidth = MeasureText(text, 20);
189 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
190
191
192 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
193 {
194 level->nextState = LEVEL_STATE_RESET;
195 }
196
197 if (HasLevelNextWave(level))
198 {
199 if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
200 {
201 level->nextState = LEVEL_STATE_BUILDING;
202 }
203 }
204 else {
205 if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
206 {
207 level->nextState = LEVEL_STATE_WON_LEVEL;
208 }
209 }
210 }
211
212 void DrawBuildingBuildButton(Level *level, int x, int y, int width, int height, uint8_t towerType, const char *name)
213 {
214 static ButtonState buttonStates[8] = {0};
215 int cost = GetTowerCosts(towerType);
216 const char *text = TextFormat("%s: %d", name, cost);
217 buttonStates[towerType].isSelected = level->placementMode == towerType;
218 buttonStates[towerType].isDisabled = level->playerGold < cost;
219 if (Button(text, x, y, width, height, &buttonStates[towerType]))
220 {
221 level->placementMode = buttonStates[towerType].isSelected ? 0 : towerType;
222 level->nextState = LEVEL_STATE_BUILDING_PLACEMENT;
223 }
224 }
225
226 float GetRandomFloat(float min, float max)
227 {
228 int random = GetRandomValue(0, 0xfffffff);
229 return ((float)random / (float)0xfffffff) * (max - min) + min;
230 }
231
232 void DrawLevelGround(Level *level)
233 {
234 // draw checkerboard ground pattern
235 for (int x = -5; x <= 5; x += 1)
236 {
237 for (int y = -5; y <= 5; y += 1)
238 {
239 Model *model = (x + y) % 2 == 0 ? &floorTileAModel : &floorTileBModel;
240 DrawModel(*model, (Vector3){x, 0.0f, y}, 1.0f, WHITE);
241 }
242 }
243
244 int oldSeed = GetRandomValue(0, 0xfffffff);
245 SetRandomSeed(level->seed);
246 // increase probability for trees via duplicated entries
247 Model borderModels[64];
248 int maxRockCount = GetRandomValue(2, 6);
249 int maxTreeCount = GetRandomValue(10, 20);
250 int maxFirTreeCount = GetRandomValue(5, 10);
251 int maxLeafTreeCount = maxTreeCount - maxFirTreeCount;
252 int grassPatchCount = GetRandomValue(5, 30);
253
254 int modelCount = 0;
255 for (int i = 0; i < maxRockCount && modelCount < 63; i++)
256 {
257 borderModels[modelCount++] = rockModels[GetRandomValue(0, 5)];
258 }
259 for (int i = 0; i < maxLeafTreeCount && modelCount < 63; i++)
260 {
261 borderModels[modelCount++] = treeModel[GetRandomValue(0, 1)];
262 }
263 for (int i = 0; i < maxFirTreeCount && modelCount < 63; i++)
264 {
265 borderModels[modelCount++] = firTreeModel[GetRandomValue(0, 1)];
266 }
267 for (int i = 0; i < grassPatchCount && modelCount < 63; i++)
268 {
269 borderModels[modelCount++] = grassPatchModel[0];
270 }
271
272 // draw some objects around the border of the map
273 Vector3 up = {0, 1, 0};
274 // a pseudo random number generator to get the same result every time
275 const float wiggle = 0.75f;
276 const int layerCount = 3;
277 for (int layer = 0; layer < layerCount; layer++)
278 {
279 int layerPos = 6 + layer;
280 for (int x = -6 + layer; x <= 6 + layer; x += 1)
281 {
282 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)],
283 (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, -layerPos + GetRandomFloat(0.0f, wiggle)},
284 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
285 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)],
286 (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, layerPos + GetRandomFloat(0.0f, wiggle)},
287 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
288 }
289
290 for (int z = -5 + layer; z <= 5 + layer; z += 1)
291 {
292 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)],
293 (Vector3){-layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)},
294 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
295 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)],
296 (Vector3){layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)},
297 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
298 }
299 }
300
301 SetRandomSeed(oldSeed);
302 }
303
304 void DrawEnemyPath(Level *level, Color arrowColor)
305 {
306 const int castleX = 0, castleY = 0;
307 const int maxWaypointCount = 200;
308 const float timeStep = 1.0f;
309 Vector3 arrowScale = {0.75f, 0.75f, 0.75f};
310
311 // we start with a time offset to simulate the path,
312 // this way the arrows are animated in a forward moving direction
313 // The time is wrapped around the time step to get a smooth animation
314 float timeOffset = fmodf(GetTime(), timeStep);
315
316 for (int i = 0; i < ENEMY_MAX_WAVE_COUNT; i++)
317 {
318 EnemyWave *wave = &level->waves[i];
319 if (wave->wave != level->currentWave)
320 {
321 continue;
322 }
323
324 // use this dummy enemy to simulate the path
325 Enemy dummy = {
326 .enemyType = ENEMY_TYPE_MINION,
327 .simPosition = (Vector2){wave->spawnPosition.x, wave->spawnPosition.y},
328 .nextX = wave->spawnPosition.x,
329 .nextY = wave->spawnPosition.y,
330 .currentX = wave->spawnPosition.x,
331 .currentY = wave->spawnPosition.y,
332 };
333
334 float deltaTime = timeOffset;
335 for (int j = 0; j < maxWaypointCount; j++)
336 {
337 int waypointPassedCount = 0;
338 Vector2 pos = EnemyGetPosition(&dummy, deltaTime, &dummy.simVelocity, &waypointPassedCount);
339 // after the initial variable starting offset, we use a fixed time step
340 deltaTime = timeStep;
341 dummy.simPosition = pos;
342
343 // Update the dummy's position just like we do in the regular enemy update loop
344 for (int k = 0; k < waypointPassedCount; k++)
345 {
346 dummy.currentX = dummy.nextX;
347 dummy.currentY = dummy.nextY;
348 if (EnemyGetNextPosition(dummy.currentX, dummy.currentY, &dummy.nextX, &dummy.nextY) &&
349 Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
350 {
351 break;
352 }
353 }
354 if (Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
355 {
356 break;
357 }
358
359 // get the angle we need to rotate the arrow model. The velocity is just fine for this.
360 float angle = atan2f(dummy.simVelocity.x, dummy.simVelocity.y) * RAD2DEG - 90.0f;
361 DrawModelEx(pathArrowModel, (Vector3){pos.x, 0.15f, pos.y}, (Vector3){0, 1, 0}, angle, arrowScale, arrowColor);
362 }
363 }
364 }
365
366 void DrawEnemyPaths(Level *level)
367 {
368 // disable depth testing for the path arrows
369 // flush the 3D batch to draw the arrows on top of everything
370 rlDrawRenderBatchActive();
371 rlDisableDepthTest();
372 DrawEnemyPath(level, (Color){64, 64, 64, 160});
373
374 rlDrawRenderBatchActive();
375 rlEnableDepthTest();
376 DrawEnemyPath(level, WHITE);
377 }
378
379 void DrawLevelBuildingPlacementState(Level *level)
380 {
381 BeginMode3D(level->camera);
382 DrawLevelGround(level);
383
384 int blockedCellCount = 0;
385 Vector2 blockedCells[1];
386 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera);
387 float planeDistance = ray.position.y / -ray.direction.y;
388 float planeX = ray.direction.x * planeDistance + ray.position.x;
389 float planeY = ray.direction.z * planeDistance + ray.position.z;
390 int16_t mapX = (int16_t)floorf(planeX + 0.5f);
391 int16_t mapY = (int16_t)floorf(planeY + 0.5f);
392 if (level->placementMode && !guiState.isBlocked && mapX >= -5 && mapX <= 5 && mapY >= -5 && mapY <= 5 && IsMouseButtonDown(MOUSE_LEFT_BUTTON))
393 {
394 level->placementX = mapX;
395 level->placementY = mapY;
396 }
397 else
398 {
399 mapX = level->placementX;
400 mapY = level->placementY;
401 }
402 blockedCells[blockedCellCount++] = (Vector2){mapX, mapY};
403 PathFindingMapUpdate(blockedCellCount, blockedCells);
404
405 TowerDraw();
406 EnemyDraw();
407 ProjectileDraw();
408 ParticleDraw();
409 DrawEnemyPaths(level);
410
411 // smooth transition for the placement position using exponential decay
412 const float lambda = 15.0f;
413 const float deltaTime = fmin(GetFrameTime(), 0.3f);
414 float factor = 1.0f - expf(-lambda * deltaTime);
415
416 level->placementTransitionPosition =
417 Vector2Lerp(
418 level->placementTransitionPosition,
419 (Vector2){mapX, mapY}, factor);
420
421 // draw the spring position for debugging the spring simulation
422 // first step: stiff spring, no simulation
423 Vector3 worldPlacementPosition = (Vector3){
424 level->placementTransitionPosition.x, 0.0f, level->placementTransitionPosition.y};
425 level->placementTowerSpring.position = (Vector3){
426 worldPlacementPosition.x, 1.0f, worldPlacementPosition.z};
427 DrawCube(level->placementTowerSpring.position, 0.1f, 0.1f, 0.1f, RED);
428 DrawLine3D(level->placementTowerSpring.position, worldPlacementPosition, YELLOW);
429
430 rlPushMatrix();
431 rlTranslatef(level->placementTransitionPosition.x, 0, level->placementTransitionPosition.y);
432 DrawCubeWires((Vector3){0.0f, 0.0f, 0.0f}, 1.0f, 0.0f, 1.0f, RED);
433
434
435 // deactivated for now to debug the spring simulation
436 // Tower dummy = {
437 // .towerType = level->placementMode,
438 // };
439 // TowerDrawSingle(dummy);
440
441 float bounce = sinf(GetTime() * 8.0f) * 0.5f + 0.5f;
442 float offset = fmaxf(0.0f, bounce - 0.4f) * 0.35f + 0.7f;
443 float squeeze = -fminf(0.0f, bounce - 0.4f) * 0.7f + 0.7f;
444 float stretch = fmaxf(0.0f, bounce - 0.3f) * 0.5f + 0.8f;
445
446 DrawModelEx(greenArrowModel, (Vector3){ offset, 0.0f, 0.0f}, (Vector3){0, 1, 0}, 90, (Vector3){squeeze, 1.0f, stretch}, WHITE);
447 DrawModelEx(greenArrowModel, (Vector3){-offset, 0.0f, 0.0f}, (Vector3){0, 1, 0}, 270, (Vector3){squeeze, 1.0f, stretch}, WHITE);
448 DrawModelEx(greenArrowModel, (Vector3){ 0.0f, 0.0f, offset}, (Vector3){0, 1, 0}, 0, (Vector3){squeeze, 1.0f, stretch}, WHITE);
449 DrawModelEx(greenArrowModel, (Vector3){ 0.0f, 0.0f, -offset}, (Vector3){0, 1, 0}, 180, (Vector3){squeeze, 1.0f, stretch}, WHITE);
450 rlPopMatrix();
451
452 guiState.isBlocked = 0;
453
454 EndMode3D();
455
456 TowerDrawHealthBars(level->camera);
457
458 if (Button("Cancel", 20, GetScreenHeight() - 40, 160, 30, 0))
459 {
460 level->nextState = LEVEL_STATE_BUILDING;
461 level->placementMode = TOWER_TYPE_NONE;
462 TraceLog(LOG_INFO, "Cancel building");
463 }
464
465 if (Button("Build", GetScreenWidth() - 180, GetScreenHeight() - 40, 160, 30, 0))
466 {
467 level->nextState = LEVEL_STATE_BUILDING;
468 if (TowerTryAdd(level->placementMode, mapX, mapY))
469 {
470 level->playerGold -= GetTowerCosts(level->placementMode);
471 level->placementMode = TOWER_TYPE_NONE;
472 }
473 }
474 }
475
476 void DrawLevelBuildingState(Level *level)
477 {
478 BeginMode3D(level->camera);
479 DrawLevelGround(level);
480
481 PathFindingMapUpdate(0, 0);
482 TowerDraw();
483 EnemyDraw();
484 ProjectileDraw();
485 ParticleDraw();
486 DrawEnemyPaths(level);
487
488 guiState.isBlocked = 0;
489
490 EndMode3D();
491
492 TowerDrawHealthBars(level->camera);
493
494 DrawBuildingBuildButton(level, 10, 10, 110, 30, TOWER_TYPE_WALL, "Wall");
495 DrawBuildingBuildButton(level, 10, 50, 110, 30, TOWER_TYPE_ARCHER, "Archer");
496 DrawBuildingBuildButton(level, 10, 90, 110, 30, TOWER_TYPE_BALLISTA, "Ballista");
497 DrawBuildingBuildButton(level, 10, 130, 110, 30, TOWER_TYPE_CATAPULT, "Catapult");
498
499 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
500 {
501 level->nextState = LEVEL_STATE_RESET;
502 }
503
504 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0))
505 {
506 level->nextState = LEVEL_STATE_BATTLE;
507 }
508
509 const char *text = "Building phase";
510 int textWidth = MeasureText(text, 20);
511 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
512 }
513
514 void InitBattleStateConditions(Level *level)
515 {
516 level->state = LEVEL_STATE_BATTLE;
517 level->nextState = LEVEL_STATE_NONE;
518 level->waveEndTimer = 0.0f;
519 for (int i = 0; i < 10; i++)
520 {
521 EnemyWave *wave = &level->waves[i];
522 wave->spawned = 0;
523 wave->timeToSpawnNext = wave->delay;
524 }
525 }
526
527 void DrawLevelBattleState(Level *level)
528 {
529 BeginMode3D(level->camera);
530 DrawLevelGround(level);
531 TowerDraw();
532 EnemyDraw();
533 ProjectileDraw();
534 ParticleDraw();
535 guiState.isBlocked = 0;
536 EndMode3D();
537
538 EnemyDrawHealthbars(level->camera);
539 TowerDrawHealthBars(level->camera);
540
541 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
542 {
543 level->nextState = LEVEL_STATE_RESET;
544 }
545
546 int maxCount = 0;
547 int remainingCount = 0;
548 for (int i = 0; i < 10; i++)
549 {
550 EnemyWave *wave = &level->waves[i];
551 if (wave->wave != level->currentWave)
552 {
553 continue;
554 }
555 maxCount += wave->count;
556 remainingCount += wave->count - wave->spawned;
557 }
558 int aliveCount = EnemyCount();
559 remainingCount += aliveCount;
560
561 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount);
562 int textWidth = MeasureText(text, 20);
563 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
564 }
565
566 void DrawLevel(Level *level)
567 {
568 switch (level->state)
569 {
570 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break;
571 case LEVEL_STATE_BUILDING_PLACEMENT: DrawLevelBuildingPlacementState(level); break;
572 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break;
573 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break;
574 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break;
575 default: break;
576 }
577
578 DrawLevelHud(level);
579 }
580
581 void UpdateLevel(Level *level)
582 {
583 if (level->state == LEVEL_STATE_BATTLE)
584 {
585 int activeWaves = 0;
586 for (int i = 0; i < 10; i++)
587 {
588 EnemyWave *wave = &level->waves[i];
589 if (wave->spawned >= wave->count || wave->wave != level->currentWave)
590 {
591 continue;
592 }
593 activeWaves++;
594 wave->timeToSpawnNext -= gameTime.deltaTime;
595 if (wave->timeToSpawnNext <= 0.0f)
596 {
597 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y);
598 if (enemy)
599 {
600 wave->timeToSpawnNext = wave->interval;
601 wave->spawned++;
602 }
603 }
604 }
605 if (GetTowerByType(TOWER_TYPE_BASE) == 0) {
606 level->waveEndTimer += gameTime.deltaTime;
607 if (level->waveEndTimer >= 2.0f)
608 {
609 level->nextState = LEVEL_STATE_LOST_WAVE;
610 }
611 }
612 else if (activeWaves == 0 && EnemyCount() == 0)
613 {
614 level->waveEndTimer += gameTime.deltaTime;
615 if (level->waveEndTimer >= 2.0f)
616 {
617 level->nextState = LEVEL_STATE_WON_WAVE;
618 }
619 }
620 }
621
622 PathFindingMapUpdate(0, 0);
623 EnemyUpdate();
624 TowerUpdate();
625 ProjectileUpdate();
626 ParticleUpdate();
627
628 if (level->nextState == LEVEL_STATE_RESET)
629 {
630 InitLevel(level);
631 }
632
633 if (level->nextState == LEVEL_STATE_BATTLE)
634 {
635 InitBattleStateConditions(level);
636 }
637
638 if (level->nextState == LEVEL_STATE_WON_WAVE)
639 {
640 level->currentWave++;
641 level->state = LEVEL_STATE_WON_WAVE;
642 }
643
644 if (level->nextState == LEVEL_STATE_LOST_WAVE)
645 {
646 level->state = LEVEL_STATE_LOST_WAVE;
647 }
648
649 if (level->nextState == LEVEL_STATE_BUILDING)
650 {
651 level->state = LEVEL_STATE_BUILDING;
652 }
653
654 if (level->nextState == LEVEL_STATE_BUILDING_PLACEMENT)
655 {
656 level->state = LEVEL_STATE_BUILDING_PLACEMENT;
657 level->placementTransitionPosition = (Vector2){
658 level->placementX, level->placementY};
659 // initialize the spring to the current position
660 level->placementTowerSpring = (PhysicsPoint){
661 .position = (Vector3){level->placementX, 1.0f, level->placementY},
662 .velocity = (Vector3){0.0f, 0.0f, 0.0f},
663 };
664 }
665
666 if (level->nextState == LEVEL_STATE_WON_LEVEL)
667 {
668 // make something of this later
669 InitLevel(level);
670 }
671
672 level->nextState = LEVEL_STATE_NONE;
673 }
674
675 float nextSpawnTime = 0.0f;
676
677 void ResetGame()
678 {
679 InitLevel(currentLevel);
680 }
681
682 void InitGame()
683 {
684 TowerInit();
685 EnemyInit();
686 ProjectileInit();
687 ParticleInit();
688 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f);
689
690 currentLevel = levels;
691 InitLevel(currentLevel);
692 }
693
694 //# Immediate GUI functions
695
696 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth)
697 {
698 const float healthBarHeight = 6.0f;
699 const float healthBarOffset = 15.0f;
700 const float inset = 2.0f;
701 const float innerWidth = healthBarWidth - inset * 2;
702 const float innerHeight = healthBarHeight - inset * 2;
703
704 Vector2 screenPos = GetWorldToScreen(position, camera);
705 float centerX = screenPos.x - healthBarWidth * 0.5f;
706 float topY = screenPos.y - healthBarOffset;
707 DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK);
708 float healthWidth = innerWidth * healthRatio;
709 DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor);
710 }
711
712 int Button(const char *text, int x, int y, int width, int height, ButtonState *state)
713 {
714 Rectangle bounds = {x, y, width, height};
715 int isPressed = 0;
716 int isSelected = state && state->isSelected;
717 int isDisabled = state && state->isDisabled;
718 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !isDisabled)
719 {
720 Color color = isSelected ? DARKGRAY : GRAY;
721 DrawRectangle(x, y, width, height, color);
722 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
723 {
724 isPressed = 1;
725 }
726 guiState.isBlocked = 1;
727 }
728 else
729 {
730 Color color = isSelected ? WHITE : LIGHTGRAY;
731 DrawRectangle(x, y, width, height, color);
732 }
733 Font font = GetFontDefault();
734 Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1);
735 Color textColor = isDisabled ? GRAY : BLACK;
736 DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, textColor);
737 return isPressed;
738 }
739
740 //# Main game loop
741
742 void GameUpdate()
743 {
744 float dt = GetFrameTime();
745 // cap maximum delta time to 0.1 seconds to prevent large time steps
746 if (dt > 0.1f) dt = 0.1f;
747 gameTime.time += dt;
748 gameTime.deltaTime = dt;
749
750 UpdateLevel(currentLevel);
751 }
752
753 int main(void)
754 {
755 int screenWidth, screenHeight;
756 GetPreferredSize(&screenWidth, &screenHeight);
757 InitWindow(screenWidth, screenHeight, "Tower defense");
758 SetTargetFPS(30);
759
760 LoadAssets();
761 InitGame();
762
763 while (!WindowShouldClose())
764 {
765 if (IsPaused()) {
766 // canvas is not visible in browser - do nothing
767 continue;
768 }
769
770 BeginDrawing();
771 ClearBackground((Color){0x4E, 0x63, 0x26, 0xFF});
772
773 GameUpdate();
774 DrawLevel(currentLevel);
775
776 EndDrawing();
777 }
778
779 CloseWindow();
780
781 return 0;
782 }
1 #ifndef TD_TUT_2_MAIN_H
2 #define TD_TUT_2_MAIN_H
3
4 #include <inttypes.h>
5
6 #include "raylib.h"
7 #include "preferred_size.h"
8
9 //# Declarations
10
11 typedef struct PhysicsPoint
12 {
13 Vector3 position;
14 Vector3 velocity;
15 } PhysicsPoint;
16
17 #define ENEMY_MAX_PATH_COUNT 8
18 #define ENEMY_MAX_COUNT 400
19 #define ENEMY_TYPE_NONE 0
20 #define ENEMY_TYPE_MINION 1
21
22 #define PARTICLE_MAX_COUNT 400
23 #define PARTICLE_TYPE_NONE 0
24 #define PARTICLE_TYPE_EXPLOSION 1
25
26 typedef struct Particle
27 {
28 uint8_t particleType;
29 float spawnTime;
30 float lifetime;
31 Vector3 position;
32 Vector3 velocity;
33 Vector3 scale;
34 } Particle;
35
36 #define TOWER_MAX_COUNT 400
37 enum TowerType
38 {
39 TOWER_TYPE_NONE,
40 TOWER_TYPE_BASE,
41 TOWER_TYPE_ARCHER,
42 TOWER_TYPE_BALLISTA,
43 TOWER_TYPE_CATAPULT,
44 TOWER_TYPE_WALL,
45 TOWER_TYPE_COUNT
46 };
47
48 typedef struct HitEffectConfig
49 {
50 float damage;
51 float areaDamageRadius;
52 float pushbackPowerDistance;
53 } HitEffectConfig;
54
55 typedef struct TowerTypeConfig
56 {
57 float cooldown;
58 float range;
59 float projectileSpeed;
60
61 uint8_t cost;
62 uint8_t projectileType;
63 uint16_t maxHealth;
64
65 HitEffectConfig hitEffect;
66 } TowerTypeConfig;
67
68 typedef struct Tower
69 {
70 int16_t x, y;
71 uint8_t towerType;
72 Vector2 lastTargetPosition;
73 float cooldown;
74 float damage;
75 } Tower;
76
77 typedef struct GameTime
78 {
79 float time;
80 float deltaTime;
81 } GameTime;
82
83 typedef struct ButtonState {
84 char isSelected;
85 char isDisabled;
86 } ButtonState;
87
88 typedef struct GUIState {
89 int isBlocked;
90 } GUIState;
91
92 typedef enum LevelState
93 {
94 LEVEL_STATE_NONE,
95 LEVEL_STATE_BUILDING,
96 LEVEL_STATE_BUILDING_PLACEMENT,
97 LEVEL_STATE_BATTLE,
98 LEVEL_STATE_WON_WAVE,
99 LEVEL_STATE_LOST_WAVE,
100 LEVEL_STATE_WON_LEVEL,
101 LEVEL_STATE_RESET,
102 } LevelState;
103
104 typedef struct EnemyWave {
105 uint8_t enemyType;
106 uint8_t wave;
107 uint16_t count;
108 float interval;
109 float delay;
110 Vector2 spawnPosition;
111
112 uint16_t spawned;
113 float timeToSpawnNext;
114 } EnemyWave;
115
116 #define ENEMY_MAX_WAVE_COUNT 10
117
118 typedef struct Level
119 {
120 int seed;
121 LevelState state;
122 LevelState nextState;
123 Camera3D camera;
124 int placementMode;
125 int16_t placementX;
126 int16_t placementY;
127 Vector2 placementTransitionPosition;
128 PhysicsPoint placementTowerSpring;
129
130 int initialGold;
131 int playerGold;
132
133 EnemyWave waves[ENEMY_MAX_WAVE_COUNT];
134 int currentWave;
135 float waveEndTimer;
136 } Level;
137
138 typedef struct DeltaSrc
139 {
140 char x, y;
141 } DeltaSrc;
142
143 typedef struct PathfindingMap
144 {
145 int width, height;
146 float scale;
147 float *distances;
148 long *towerIndex;
149 DeltaSrc *deltaSrc;
150 float maxDistance;
151 Matrix toMapSpace;
152 Matrix toWorldSpace;
153 } PathfindingMap;
154
155 // when we execute the pathfinding algorithm, we need to store the active nodes
156 // in a queue. Each node has a position, a distance from the start, and the
157 // position of the node that we came from.
158 typedef struct PathfindingNode
159 {
160 int16_t x, y, fromX, fromY;
161 float distance;
162 } PathfindingNode;
163
164 typedef struct EnemyId
165 {
166 uint16_t index;
167 uint16_t generation;
168 } EnemyId;
169
170 typedef struct EnemyClassConfig
171 {
172 float speed;
173 float health;
174 float radius;
175 float maxAcceleration;
176 float requiredContactTime;
177 float explosionDamage;
178 float explosionRange;
179 float explosionPushbackPower;
180 int goldValue;
181 } EnemyClassConfig;
182
183 typedef struct Enemy
184 {
185 int16_t currentX, currentY;
186 int16_t nextX, nextY;
187 Vector2 simPosition;
188 Vector2 simVelocity;
189 uint16_t generation;
190 float walkedDistance;
191 float startMovingTime;
192 float damage, futureDamage;
193 float contactTime;
194 uint8_t enemyType;
195 uint8_t movePathCount;
196 Vector2 movePath[ENEMY_MAX_PATH_COUNT];
197 } Enemy;
198
199 // a unit that uses sprites to be drawn
200 #define SPRITE_UNIT_PHASE_WEAPON_IDLE 0
201 #define SPRITE_UNIT_PHASE_WEAPON_COOLDOWN 1
202 typedef struct SpriteUnit
203 {
204 Rectangle srcRect;
205 Vector2 offset;
206 int frameCount;
207 float frameDuration;
208 Rectangle srcWeaponIdleRect;
209 Vector2 srcWeaponIdleOffset;
210 Rectangle srcWeaponCooldownRect;
211 Vector2 srcWeaponCooldownOffset;
212 } SpriteUnit;
213
214 #define PROJECTILE_MAX_COUNT 1200
215 #define PROJECTILE_TYPE_NONE 0
216 #define PROJECTILE_TYPE_ARROW 1
217 #define PROJECTILE_TYPE_CATAPULT 2
218 #define PROJECTILE_TYPE_BALLISTA 3
219
220 typedef struct Projectile
221 {
222 uint8_t projectileType;
223 float shootTime;
224 float arrivalTime;
225 float distance;
226 Vector3 position;
227 Vector3 target;
228 Vector3 directionNormal;
229 EnemyId targetEnemy;
230 HitEffectConfig hitEffectConfig;
231 } Projectile;
232
233 //# Function declarations
234 float TowerGetMaxHealth(Tower *tower);
235 int Button(const char *text, int x, int y, int width, int height, ButtonState *state);
236 int EnemyAddDamageRange(Vector2 position, float range, float damage);
237 int EnemyAddDamage(Enemy *enemy, float damage);
238
239 //# Enemy functions
240 void EnemyInit();
241 void EnemyDraw();
242 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource);
243 void EnemyUpdate();
244 float EnemyGetCurrentMaxSpeed(Enemy *enemy);
245 float EnemyGetMaxHealth(Enemy *enemy);
246 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY);
247 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount);
248 EnemyId EnemyGetId(Enemy *enemy);
249 Enemy *EnemyTryResolve(EnemyId enemyId);
250 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY);
251 int EnemyAddDamage(Enemy *enemy, float damage);
252 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range);
253 int EnemyCount();
254 void EnemyDrawHealthbars(Camera3D camera);
255
256 //# Tower functions
257 void TowerInit();
258 Tower *TowerGetAt(int16_t x, int16_t y);
259 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y);
260 Tower *GetTowerByType(uint8_t towerType);
261 int GetTowerCosts(uint8_t towerType);
262 float TowerGetMaxHealth(Tower *tower);
263 void TowerDraw();
264 void TowerDrawSingle(Tower tower);
265 void TowerUpdate();
266 void TowerDrawHealthBars(Camera3D camera);
267 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase);
268
269 //# Particles
270 void ParticleInit();
271 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime);
272 void ParticleUpdate();
273 void ParticleDraw();
274
275 //# Projectiles
276 void ProjectileInit();
277 void ProjectileDraw();
278 void ProjectileUpdate();
279 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig);
280
281 //# Pathfinding map
282 void PathfindingMapInit(int width, int height, Vector3 translate, float scale);
283 float PathFindingGetDistance(int mapX, int mapY);
284 Vector2 PathFindingGetGradient(Vector3 world);
285 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY);
286 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells);
287 void PathFindingMapDraw();
288
289 //# UI
290 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth);
291
292 //# Level
293 void DrawLevelGround(Level *level);
294 void DrawEnemyPath(Level *level, Color arrowColor);
295
296 //# variables
297 extern Level *currentLevel;
298 extern Enemy enemies[ENEMY_MAX_COUNT];
299 extern int enemyCount;
300 extern EnemyClassConfig enemyClassConfigs[];
301
302 extern GUIState guiState;
303 extern GameTime gameTime;
304 extern Tower towers[TOWER_MAX_COUNT];
305 extern int towerCount;
306
307 extern Texture2D palette, spriteSheet;
308
309 #endif
1 #include "td_main.h"
2 #include <raymath.h>
3
4 // The queue is a simple array of nodes, we add nodes to the end and remove
5 // nodes from the front. We keep the array around to avoid unnecessary allocations
6 static PathfindingNode *pathfindingNodeQueue = 0;
7 static int pathfindingNodeQueueCount = 0;
8 static int pathfindingNodeQueueCapacity = 0;
9
10 // The pathfinding map stores the distances from the castle to each cell in the map.
11 static PathfindingMap pathfindingMap = {0};
12
13 void PathfindingMapInit(int width, int height, Vector3 translate, float scale)
14 {
15 // transforming between map space and world space allows us to adapt
16 // position and scale of the map without changing the pathfinding data
17 pathfindingMap.toWorldSpace = MatrixTranslate(translate.x, translate.y, translate.z);
18 pathfindingMap.toWorldSpace = MatrixMultiply(pathfindingMap.toWorldSpace, MatrixScale(scale, scale, scale));
19 pathfindingMap.toMapSpace = MatrixInvert(pathfindingMap.toWorldSpace);
20 pathfindingMap.width = width;
21 pathfindingMap.height = height;
22 pathfindingMap.scale = scale;
23 pathfindingMap.distances = (float *)MemAlloc(width * height * sizeof(float));
24 for (int i = 0; i < width * height; i++)
25 {
26 pathfindingMap.distances[i] = -1.0f;
27 }
28
29 pathfindingMap.towerIndex = (long *)MemAlloc(width * height * sizeof(long));
30 pathfindingMap.deltaSrc = (DeltaSrc *)MemAlloc(width * height * sizeof(DeltaSrc));
31 }
32
33 static void PathFindingNodePush(int16_t x, int16_t y, int16_t fromX, int16_t fromY, float distance)
34 {
35 if (pathfindingNodeQueueCount >= pathfindingNodeQueueCapacity)
36 {
37 pathfindingNodeQueueCapacity = pathfindingNodeQueueCapacity == 0 ? 256 : pathfindingNodeQueueCapacity * 2;
38 // we use MemAlloc/MemRealloc to allocate memory for the queue
39 // I am not entirely sure if MemRealloc allows passing a null pointer
40 // so we check if the pointer is null and use MemAlloc in that case
41 if (pathfindingNodeQueue == 0)
42 {
43 pathfindingNodeQueue = (PathfindingNode *)MemAlloc(pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
44 }
45 else
46 {
47 pathfindingNodeQueue = (PathfindingNode *)MemRealloc(pathfindingNodeQueue, pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
48 }
49 }
50
51 PathfindingNode *node = &pathfindingNodeQueue[pathfindingNodeQueueCount++];
52 node->x = x;
53 node->y = y;
54 node->fromX = fromX;
55 node->fromY = fromY;
56 node->distance = distance;
57 }
58
59 static PathfindingNode *PathFindingNodePop()
60 {
61 if (pathfindingNodeQueueCount == 0)
62 {
63 return 0;
64 }
65 // we return the first node in the queue; we want to return a pointer to the node
66 // so we can return 0 if the queue is empty.
67 // We should _not_ return a pointer to the element in the list, because the list
68 // may be reallocated and the pointer would become invalid. Or the
69 // popped element is overwritten by the next push operation.
70 // Using static here means that the variable is permanently allocated.
71 static PathfindingNode node;
72 node = pathfindingNodeQueue[0];
73 // we shift all nodes one position to the front
74 for (int i = 1; i < pathfindingNodeQueueCount; i++)
75 {
76 pathfindingNodeQueue[i - 1] = pathfindingNodeQueue[i];
77 }
78 --pathfindingNodeQueueCount;
79 return &node;
80 }
81
82 float PathFindingGetDistance(int mapX, int mapY)
83 {
84 if (mapX < 0 || mapX >= pathfindingMap.width || mapY < 0 || mapY >= pathfindingMap.height)
85 {
86 // when outside the map, we return the manhattan distance to the castle (0,0)
87 return fabsf((float)mapX) + fabsf((float)mapY);
88 }
89
90 return pathfindingMap.distances[mapY * pathfindingMap.width + mapX];
91 }
92
93 // transform a world position to a map position in the array;
94 // returns true if the position is inside the map
95 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY)
96 {
97 Vector3 mapPosition = Vector3Transform(worldPosition, pathfindingMap.toMapSpace);
98 *mapX = (int16_t)mapPosition.x;
99 *mapY = (int16_t)mapPosition.z;
100 return *mapX >= 0 && *mapX < pathfindingMap.width && *mapY >= 0 && *mapY < pathfindingMap.height;
101 }
102
103 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells)
104 {
105 const int castleX = 0, castleY = 0;
106 int16_t castleMapX, castleMapY;
107 if (!PathFindingFromWorldToMapPosition((Vector3){castleX, 0.0f, castleY}, &castleMapX, &castleMapY))
108 {
109 return;
110 }
111 int width = pathfindingMap.width, height = pathfindingMap.height;
112
113 // reset the distances to -1
114 for (int i = 0; i < width * height; i++)
115 {
116 pathfindingMap.distances[i] = -1.0f;
117 }
118 // reset the tower indices
119 for (int i = 0; i < width * height; i++)
120 {
121 pathfindingMap.towerIndex[i] = -1;
122 }
123 // reset the delta src
124 for (int i = 0; i < width * height; i++)
125 {
126 pathfindingMap.deltaSrc[i].x = 0;
127 pathfindingMap.deltaSrc[i].y = 0;
128 }
129
130 for (int i = 0; i < blockedCellCount; i++)
131 {
132 int16_t mapX, mapY;
133 if (!PathFindingFromWorldToMapPosition((Vector3){blockedCells[i].x, 0.0f, blockedCells[i].y}, &mapX, &mapY))
134 {
135 continue;
136 }
137 int index = mapY * width + mapX;
138 pathfindingMap.towerIndex[index] = -2;
139 }
140
141 for (int i = 0; i < towerCount; i++)
142 {
143 Tower *tower = &towers[i];
144 if (tower->towerType == TOWER_TYPE_NONE || tower->towerType == TOWER_TYPE_BASE)
145 {
146 continue;
147 }
148 int16_t mapX, mapY;
149 // technically, if the tower cell scale is not in sync with the pathfinding map scale,
150 // this would not work correctly and needs to be refined to allow towers covering multiple cells
151 // or having multiple towers in one cell; for simplicity, we assume that the tower covers exactly
152 // one cell. For now.
153 if (!PathFindingFromWorldToMapPosition((Vector3){tower->x, 0.0f, tower->y}, &mapX, &mapY))
154 {
155 continue;
156 }
157 int index = mapY * width + mapX;
158 pathfindingMap.towerIndex[index] = i;
159 }
160
161 // we start at the castle and add the castle to the queue
162 pathfindingMap.maxDistance = 0.0f;
163 pathfindingNodeQueueCount = 0;
164 PathFindingNodePush(castleMapX, castleMapY, castleMapX, castleMapY, 0.0f);
165 PathfindingNode *node = 0;
166 while ((node = PathFindingNodePop()))
167 {
168 if (node->x < 0 || node->x >= width || node->y < 0 || node->y >= height)
169 {
170 continue;
171 }
172 int index = node->y * width + node->x;
173 if (pathfindingMap.distances[index] >= 0 && pathfindingMap.distances[index] <= node->distance)
174 {
175 continue;
176 }
177
178 int deltaX = node->x - node->fromX;
179 int deltaY = node->y - node->fromY;
180 // even if the cell is blocked by a tower, we still may want to store the direction
181 // (though this might not be needed, IDK right now)
182 pathfindingMap.deltaSrc[index].x = (char) deltaX;
183 pathfindingMap.deltaSrc[index].y = (char) deltaY;
184
185 // we skip nodes that are blocked by towers or by the provided blocked cells
186 if (pathfindingMap.towerIndex[index] != -1)
187 {
188 node->distance += 8.0f;
189 }
190 pathfindingMap.distances[index] = node->distance;
191 pathfindingMap.maxDistance = fmaxf(pathfindingMap.maxDistance, node->distance);
192 PathFindingNodePush(node->x, node->y + 1, node->x, node->y, node->distance + 1.0f);
193 PathFindingNodePush(node->x, node->y - 1, node->x, node->y, node->distance + 1.0f);
194 PathFindingNodePush(node->x + 1, node->y, node->x, node->y, node->distance + 1.0f);
195 PathFindingNodePush(node->x - 1, node->y, node->x, node->y, node->distance + 1.0f);
196 }
197 }
198
199 void PathFindingMapDraw()
200 {
201 float cellSize = pathfindingMap.scale * 0.9f;
202 float highlightDistance = fmodf(GetTime() * 4.0f, pathfindingMap.maxDistance);
203 for (int x = 0; x < pathfindingMap.width; x++)
204 {
205 for (int y = 0; y < pathfindingMap.height; y++)
206 {
207 float distance = pathfindingMap.distances[y * pathfindingMap.width + x];
208 float colorV = distance < 0 ? 0 : fminf(distance / pathfindingMap.maxDistance, 1.0f);
209 Color color = distance < 0 ? BLUE : (Color){fminf(colorV, 1.0f) * 255, 0, 0, 255};
210 Vector3 position = Vector3Transform((Vector3){x, -0.25f, y}, pathfindingMap.toWorldSpace);
211 // animate the distance "wave" to show how the pathfinding algorithm expands
212 // from the castle
213 if (distance + 0.5f > highlightDistance && distance - 0.5f < highlightDistance)
214 {
215 color = BLACK;
216 }
217 DrawCube(position, cellSize, 0.1f, cellSize, color);
218 }
219 }
220 }
221
222 Vector2 PathFindingGetGradient(Vector3 world)
223 {
224 int16_t mapX, mapY;
225 if (PathFindingFromWorldToMapPosition(world, &mapX, &mapY))
226 {
227 DeltaSrc delta = pathfindingMap.deltaSrc[mapY * pathfindingMap.width + mapX];
228 return (Vector2){(float)-delta.x, (float)-delta.y};
229 }
230 // fallback to a simple gradient calculation
231 float n = PathFindingGetDistance(mapX, mapY - 1);
232 float s = PathFindingGetDistance(mapX, mapY + 1);
233 float w = PathFindingGetDistance(mapX - 1, mapY);
234 float e = PathFindingGetDistance(mapX + 1, mapY);
235 return (Vector2){w - e + 0.25f, n - s + 0.125f};
236 }
1 #include "td_main.h"
2 #include <raymath.h>
3
4 static TowerTypeConfig towerTypeConfigs[TOWER_TYPE_COUNT] = {
5 [TOWER_TYPE_BASE] = {
6 .maxHealth = 10,
7 },
8 [TOWER_TYPE_ARCHER] = {
9 .cooldown = 0.5f,
10 .range = 3.0f,
11 .cost = 6,
12 .maxHealth = 10,
13 .projectileSpeed = 4.0f,
14 .projectileType = PROJECTILE_TYPE_ARROW,
15 .hitEffect = {
16 .damage = 3.0f,
17 }
18 },
19 [TOWER_TYPE_BALLISTA] = {
20 .cooldown = 1.5f,
21 .range = 6.0f,
22 .cost = 9,
23 .maxHealth = 10,
24 .projectileSpeed = 10.0f,
25 .projectileType = PROJECTILE_TYPE_BALLISTA,
26 .hitEffect = {
27 .damage = 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 TowerDrawSingle(Tower tower)
239 {
240 if (tower.towerType == TOWER_TYPE_NONE)
241 {
242 return;
243 }
244
245 switch (tower.towerType)
246 {
247 case TOWER_TYPE_ARCHER:
248 {
249 Vector2 screenPosTower = GetWorldToScreen((Vector3){tower.x, 0.0f, tower.y}, currentLevel->camera);
250 Vector2 screenPosTarget = GetWorldToScreen((Vector3){tower.lastTargetPosition.x, 0.0f, tower.lastTargetPosition.y}, currentLevel->camera);
251 DrawModel(towerModels[TOWER_TYPE_WALL], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
252 DrawSpriteUnit(archerUnit, (Vector3){tower.x, 1.0f, tower.y}, 0, screenPosTarget.x > screenPosTower.x,
253 tower.cooldown > 0.2f ? SPRITE_UNIT_PHASE_WEAPON_COOLDOWN : SPRITE_UNIT_PHASE_WEAPON_IDLE);
254 }
255 break;
256 case TOWER_TYPE_BALLISTA:
257 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, BROWN);
258 break;
259 case TOWER_TYPE_CATAPULT:
260 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, DARKGRAY);
261 break;
262 default:
263 if (towerModels[tower.towerType].materials)
264 {
265 DrawModel(towerModels[tower.towerType], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
266 } else {
267 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY);
268 }
269 break;
270 }
271 }
272
273 void TowerDraw()
274 {
275 for (int i = 0; i < towerCount; i++)
276 {
277 TowerDrawSingle(towers[i]);
278 }
279 }
280
281 void TowerUpdate()
282 {
283 for (int i = 0; i < towerCount; i++)
284 {
285 Tower *tower = &towers[i];
286 switch (tower->towerType)
287 {
288 case TOWER_TYPE_CATAPULT:
289 case TOWER_TYPE_BALLISTA:
290 case TOWER_TYPE_ARCHER:
291 TowerGunUpdate(tower);
292 break;
293 }
294 }
295 }
296
297 void TowerDrawHealthBars(Camera3D camera)
298 {
299 for (int i = 0; i < towerCount; i++)
300 {
301 Tower *tower = &towers[i];
302 if (tower->towerType == TOWER_TYPE_NONE || tower->damage <= 0.0f)
303 {
304 continue;
305 }
306
307 Vector3 position = (Vector3){tower->x, 0.5f, tower->y};
308 float maxHealth = TowerGetMaxHealth(tower);
309 float health = maxHealth - tower->damage;
310 float healthRatio = health / maxHealth;
311
312 DrawHealthBar(camera, position, healthRatio, GREEN, 35.0f);
313 }
314 }
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 is just the visualization of the spring tip's position (red dot) and a yellow line that represents the spring that connects the tip with the base. Currently, the spring is stiff. The next steps will add the simulated behavior aspect:
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 Model greenArrowModel = {0};
20
21 Texture2D palette, spriteSheet;
22
23 Level levels[] = {
24 [0] = {
25 .state = LEVEL_STATE_BUILDING,
26 .initialGold = 20,
27 .waves[0] = {
28 .enemyType = ENEMY_TYPE_MINION,
29 .wave = 0,
30 .count = 5,
31 .interval = 2.5f,
32 .delay = 1.0f,
33 .spawnPosition = {2, 6},
34 },
35 .waves[1] = {
36 .enemyType = ENEMY_TYPE_MINION,
37 .wave = 0,
38 .count = 5,
39 .interval = 2.5f,
40 .delay = 1.0f,
41 .spawnPosition = {-2, 6},
42 },
43 .waves[2] = {
44 .enemyType = ENEMY_TYPE_MINION,
45 .wave = 1,
46 .count = 20,
47 .interval = 1.5f,
48 .delay = 1.0f,
49 .spawnPosition = {0, 6},
50 },
51 .waves[3] = {
52 .enemyType = ENEMY_TYPE_MINION,
53 .wave = 2,
54 .count = 30,
55 .interval = 1.2f,
56 .delay = 1.0f,
57 .spawnPosition = {0, 6},
58 }
59 },
60 };
61
62 Level *currentLevel = levels;
63
64 //# Game
65
66 static Model LoadGLBModel(char *filename)
67 {
68 Model model = LoadModel(TextFormat("data/%s.glb",filename));
69 for (int i = 0; i < model.materialCount; i++)
70 {
71 model.materials[i].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
72 }
73 return model;
74 }
75
76 void LoadAssets()
77 {
78 // load a sprite sheet that contains all units
79 spriteSheet = LoadTexture("data/spritesheet.png");
80 SetTextureFilter(spriteSheet, TEXTURE_FILTER_BILINEAR);
81
82 // we'll use a palette texture to colorize the all buildings and environment art
83 palette = LoadTexture("data/palette.png");
84 // The texture uses gradients on very small space, so we'll enable bilinear filtering
85 SetTextureFilter(palette, TEXTURE_FILTER_BILINEAR);
86
87 floorTileAModel = LoadGLBModel("floor-tile-a");
88 floorTileBModel = LoadGLBModel("floor-tile-b");
89 treeModel[0] = LoadGLBModel("leaftree-large-1-a");
90 treeModel[1] = LoadGLBModel("leaftree-large-1-b");
91 firTreeModel[0] = LoadGLBModel("firtree-1-a");
92 firTreeModel[1] = LoadGLBModel("firtree-1-b");
93 rockModels[0] = LoadGLBModel("rock-1");
94 rockModels[1] = LoadGLBModel("rock-2");
95 rockModels[2] = LoadGLBModel("rock-3");
96 rockModels[3] = LoadGLBModel("rock-4");
97 rockModels[4] = LoadGLBModel("rock-5");
98 grassPatchModel[0] = LoadGLBModel("grass-patch-1");
99
100 pathArrowModel = LoadGLBModel("direction-arrow-x");
101 greenArrowModel = LoadGLBModel("green-arrow");
102 }
103
104 void InitLevel(Level *level)
105 {
106 level->seed = (int)(GetTime() * 100.0f);
107
108 TowerInit();
109 EnemyInit();
110 ProjectileInit();
111 ParticleInit();
112 TowerTryAdd(TOWER_TYPE_BASE, 0, 0);
113
114 level->placementMode = 0;
115 level->state = LEVEL_STATE_BUILDING;
116 level->nextState = LEVEL_STATE_NONE;
117 level->playerGold = level->initialGold;
118 level->currentWave = 0;
119 level->placementX = -1;
120 level->placementY = 0;
121
122 Camera *camera = &level->camera;
123 camera->position = (Vector3){4.0f, 8.0f, 8.0f};
124 camera->target = (Vector3){0.0f, 0.0f, 0.0f};
125 camera->up = (Vector3){0.0f, 1.0f, 0.0f};
126 camera->fovy = 10.0f;
127 camera->projection = CAMERA_ORTHOGRAPHIC;
128 }
129
130 void DrawLevelHud(Level *level)
131 {
132 const char *text = TextFormat("Gold: %d", level->playerGold);
133 Font font = GetFontDefault();
134 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 120, 10}, font.baseSize * 2.0f, 2.0f, BLACK);
135 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 122, 8}, font.baseSize * 2.0f, 2.0f, YELLOW);
136 }
137
138 void DrawLevelReportLostWave(Level *level)
139 {
140 BeginMode3D(level->camera);
141 DrawLevelGround(level);
142 TowerDraw();
143 EnemyDraw();
144 ProjectileDraw();
145 ParticleDraw();
146 guiState.isBlocked = 0;
147 EndMode3D();
148
149 TowerDrawHealthBars(level->camera);
150
151 const char *text = "Wave lost";
152 int textWidth = MeasureText(text, 20);
153 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
154
155 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
156 {
157 level->nextState = LEVEL_STATE_RESET;
158 }
159 }
160
161 int HasLevelNextWave(Level *level)
162 {
163 for (int i = 0; i < 10; i++)
164 {
165 EnemyWave *wave = &level->waves[i];
166 if (wave->wave == level->currentWave)
167 {
168 return 1;
169 }
170 }
171 return 0;
172 }
173
174 void DrawLevelReportWonWave(Level *level)
175 {
176 BeginMode3D(level->camera);
177 DrawLevelGround(level);
178 TowerDraw();
179 EnemyDraw();
180 ProjectileDraw();
181 ParticleDraw();
182 guiState.isBlocked = 0;
183 EndMode3D();
184
185 TowerDrawHealthBars(level->camera);
186
187 const char *text = "Wave won";
188 int textWidth = MeasureText(text, 20);
189 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
190
191
192 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
193 {
194 level->nextState = LEVEL_STATE_RESET;
195 }
196
197 if (HasLevelNextWave(level))
198 {
199 if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
200 {
201 level->nextState = LEVEL_STATE_BUILDING;
202 }
203 }
204 else {
205 if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
206 {
207 level->nextState = LEVEL_STATE_WON_LEVEL;
208 }
209 }
210 }
211
212 void DrawBuildingBuildButton(Level *level, int x, int y, int width, int height, uint8_t towerType, const char *name)
213 {
214 static ButtonState buttonStates[8] = {0};
215 int cost = GetTowerCosts(towerType);
216 const char *text = TextFormat("%s: %d", name, cost);
217 buttonStates[towerType].isSelected = level->placementMode == towerType;
218 buttonStates[towerType].isDisabled = level->playerGold < cost;
219 if (Button(text, x, y, width, height, &buttonStates[towerType]))
220 {
221 level->placementMode = buttonStates[towerType].isSelected ? 0 : towerType;
222 level->nextState = LEVEL_STATE_BUILDING_PLACEMENT;
223 }
224 }
225
226 float GetRandomFloat(float min, float max)
227 {
228 int random = GetRandomValue(0, 0xfffffff);
229 return ((float)random / (float)0xfffffff) * (max - min) + min;
230 }
231
232 void DrawLevelGround(Level *level)
233 {
234 // draw checkerboard ground pattern
235 for (int x = -5; x <= 5; x += 1)
236 {
237 for (int y = -5; y <= 5; y += 1)
238 {
239 Model *model = (x + y) % 2 == 0 ? &floorTileAModel : &floorTileBModel;
240 DrawModel(*model, (Vector3){x, 0.0f, y}, 1.0f, WHITE);
241 }
242 }
243
244 int oldSeed = GetRandomValue(0, 0xfffffff);
245 SetRandomSeed(level->seed);
246 // increase probability for trees via duplicated entries
247 Model borderModels[64];
248 int maxRockCount = GetRandomValue(2, 6);
249 int maxTreeCount = GetRandomValue(10, 20);
250 int maxFirTreeCount = GetRandomValue(5, 10);
251 int maxLeafTreeCount = maxTreeCount - maxFirTreeCount;
252 int grassPatchCount = GetRandomValue(5, 30);
253
254 int modelCount = 0;
255 for (int i = 0; i < maxRockCount && modelCount < 63; i++)
256 {
257 borderModels[modelCount++] = rockModels[GetRandomValue(0, 5)];
258 }
259 for (int i = 0; i < maxLeafTreeCount && modelCount < 63; i++)
260 {
261 borderModels[modelCount++] = treeModel[GetRandomValue(0, 1)];
262 }
263 for (int i = 0; i < maxFirTreeCount && modelCount < 63; i++)
264 {
265 borderModels[modelCount++] = firTreeModel[GetRandomValue(0, 1)];
266 }
267 for (int i = 0; i < grassPatchCount && modelCount < 63; i++)
268 {
269 borderModels[modelCount++] = grassPatchModel[0];
270 }
271
272 // draw some objects around the border of the map
273 Vector3 up = {0, 1, 0};
274 // a pseudo random number generator to get the same result every time
275 const float wiggle = 0.75f;
276 const int layerCount = 3;
277 for (int layer = 0; layer < layerCount; layer++)
278 {
279 int layerPos = 6 + layer;
280 for (int x = -6 + layer; x <= 6 + layer; x += 1)
281 {
282 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)],
283 (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, -layerPos + GetRandomFloat(0.0f, wiggle)},
284 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
285 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)],
286 (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, layerPos + GetRandomFloat(0.0f, wiggle)},
287 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
288 }
289
290 for (int z = -5 + layer; z <= 5 + layer; z += 1)
291 {
292 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)],
293 (Vector3){-layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)},
294 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
295 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)],
296 (Vector3){layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)},
297 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
298 }
299 }
300
301 SetRandomSeed(oldSeed);
302 }
303
304 void DrawEnemyPath(Level *level, Color arrowColor)
305 {
306 const int castleX = 0, castleY = 0;
307 const int maxWaypointCount = 200;
308 const float timeStep = 1.0f;
309 Vector3 arrowScale = {0.75f, 0.75f, 0.75f};
310
311 // we start with a time offset to simulate the path,
312 // this way the arrows are animated in a forward moving direction
313 // The time is wrapped around the time step to get a smooth animation
314 float timeOffset = fmodf(GetTime(), timeStep);
315
316 for (int i = 0; i < ENEMY_MAX_WAVE_COUNT; i++)
317 {
318 EnemyWave *wave = &level->waves[i];
319 if (wave->wave != level->currentWave)
320 {
321 continue;
322 }
323
324 // use this dummy enemy to simulate the path
325 Enemy dummy = {
326 .enemyType = ENEMY_TYPE_MINION,
327 .simPosition = (Vector2){wave->spawnPosition.x, wave->spawnPosition.y},
328 .nextX = wave->spawnPosition.x,
329 .nextY = wave->spawnPosition.y,
330 .currentX = wave->spawnPosition.x,
331 .currentY = wave->spawnPosition.y,
332 };
333
334 float deltaTime = timeOffset;
335 for (int j = 0; j < maxWaypointCount; j++)
336 {
337 int waypointPassedCount = 0;
338 Vector2 pos = EnemyGetPosition(&dummy, deltaTime, &dummy.simVelocity, &waypointPassedCount);
339 // after the initial variable starting offset, we use a fixed time step
340 deltaTime = timeStep;
341 dummy.simPosition = pos;
342
343 // Update the dummy's position just like we do in the regular enemy update loop
344 for (int k = 0; k < waypointPassedCount; k++)
345 {
346 dummy.currentX = dummy.nextX;
347 dummy.currentY = dummy.nextY;
348 if (EnemyGetNextPosition(dummy.currentX, dummy.currentY, &dummy.nextX, &dummy.nextY) &&
349 Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
350 {
351 break;
352 }
353 }
354 if (Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
355 {
356 break;
357 }
358
359 // get the angle we need to rotate the arrow model. The velocity is just fine for this.
360 float angle = atan2f(dummy.simVelocity.x, dummy.simVelocity.y) * RAD2DEG - 90.0f;
361 DrawModelEx(pathArrowModel, (Vector3){pos.x, 0.15f, pos.y}, (Vector3){0, 1, 0}, angle, arrowScale, arrowColor);
362 }
363 }
364 }
365
366 void DrawEnemyPaths(Level *level)
367 {
368 // disable depth testing for the path arrows
369 // flush the 3D batch to draw the arrows on top of everything
370 rlDrawRenderBatchActive();
371 rlDisableDepthTest();
372 DrawEnemyPath(level, (Color){64, 64, 64, 160});
373
374 rlDrawRenderBatchActive();
375 rlEnableDepthTest();
376 DrawEnemyPath(level, WHITE);
377 }
378
379 void DrawLevelBuildingPlacementState(Level *level)
380 {
381 BeginMode3D(level->camera);
382 DrawLevelGround(level);
383
384 int blockedCellCount = 0;
385 Vector2 blockedCells[1];
386 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera);
387 float planeDistance = ray.position.y / -ray.direction.y;
388 float planeX = ray.direction.x * planeDistance + ray.position.x;
389 float planeY = ray.direction.z * planeDistance + ray.position.z;
390 int16_t mapX = (int16_t)floorf(planeX + 0.5f);
391 int16_t mapY = (int16_t)floorf(planeY + 0.5f);
392 if (level->placementMode && !guiState.isBlocked && mapX >= -5 && mapX <= 5 && mapY >= -5 && mapY <= 5 && IsMouseButtonDown(MOUSE_LEFT_BUTTON))
393 {
394 level->placementX = mapX;
395 level->placementY = mapY;
396 }
397 else
398 {
399 mapX = level->placementX;
400 mapY = level->placementY;
401 }
402 blockedCells[blockedCellCount++] = (Vector2){mapX, mapY};
403 PathFindingMapUpdate(blockedCellCount, blockedCells);
404
405 TowerDraw();
406 EnemyDraw();
407 ProjectileDraw();
408 ParticleDraw();
409 DrawEnemyPaths(level);
410
411 float dt = gameTime.deltaTime;
412
413 // smooth transition for the placement position using exponential decay
414 const float lambda = 15.0f;
415 float factor = 1.0f - expf(-lambda * dt);
416
417 level->placementTransitionPosition =
418 Vector2Lerp(
419 level->placementTransitionPosition,
420 (Vector2){mapX, mapY}, factor);
421
422 // draw the spring position for debugging the spring simulation
423 // first step: stiff spring, no simulation
424 Vector3 worldPlacementPosition = (Vector3){
425 level->placementTransitionPosition.x, 0.0f, level->placementTransitionPosition.y};
426 Vector3 springTargetPosition = (Vector3){
427 worldPlacementPosition.x, 1.0f, worldPlacementPosition.z};
428 Vector3 springPointDelta = Vector3Subtract(springTargetPosition, level->placementTowerSpring.position);
429 Vector3 velocityChange = Vector3Scale(springPointDelta, dt * 200.0f);
430 // decay velocity
431 level->placementTowerSpring.velocity = Vector3Scale(level->placementTowerSpring.velocity, 1.0f - expf(-30.0f * dt));
432 level->placementTowerSpring.velocity = Vector3Add(level->placementTowerSpring.velocity, velocityChange);
433 level->placementTowerSpring.position = Vector3Add(level->placementTowerSpring.position,
434 Vector3Scale(level->placementTowerSpring.velocity, dt));
435
436 DrawCube(level->placementTowerSpring.position, 0.1f, 0.1f, 0.1f, RED);
437 DrawLine3D(level->placementTowerSpring.position, worldPlacementPosition, YELLOW);
438
439 rlPushMatrix();
440 rlTranslatef(level->placementTransitionPosition.x, 0, level->placementTransitionPosition.y);
441 DrawCubeWires((Vector3){0.0f, 0.0f, 0.0f}, 1.0f, 0.0f, 1.0f, RED);
442
443
444 // deactivated for now to debug the spring simulation
445 // Tower dummy = {
446 // .towerType = level->placementMode,
447 // };
448 // TowerDrawSingle(dummy);
449
450 float bounce = sinf(GetTime() * 8.0f) * 0.5f + 0.5f;
451 float offset = fmaxf(0.0f, bounce - 0.4f) * 0.35f + 0.7f;
452 float squeeze = -fminf(0.0f, bounce - 0.4f) * 0.7f + 0.7f;
453 float stretch = fmaxf(0.0f, bounce - 0.3f) * 0.5f + 0.8f;
454
455 DrawModelEx(greenArrowModel, (Vector3){ offset, 0.0f, 0.0f}, (Vector3){0, 1, 0}, 90, (Vector3){squeeze, 1.0f, stretch}, WHITE);
456 DrawModelEx(greenArrowModel, (Vector3){-offset, 0.0f, 0.0f}, (Vector3){0, 1, 0}, 270, (Vector3){squeeze, 1.0f, stretch}, WHITE);
457 DrawModelEx(greenArrowModel, (Vector3){ 0.0f, 0.0f, offset}, (Vector3){0, 1, 0}, 0, (Vector3){squeeze, 1.0f, stretch}, WHITE);
458 DrawModelEx(greenArrowModel, (Vector3){ 0.0f, 0.0f, -offset}, (Vector3){0, 1, 0}, 180, (Vector3){squeeze, 1.0f, stretch}, WHITE);
459 rlPopMatrix();
460
461 guiState.isBlocked = 0;
462
463 EndMode3D();
464
465 TowerDrawHealthBars(level->camera);
466
467 if (Button("Cancel", 20, GetScreenHeight() - 40, 160, 30, 0))
468 {
469 level->nextState = LEVEL_STATE_BUILDING;
470 level->placementMode = TOWER_TYPE_NONE;
471 TraceLog(LOG_INFO, "Cancel building");
472 }
473
474 if (Button("Build", GetScreenWidth() - 180, GetScreenHeight() - 40, 160, 30, 0))
475 {
476 level->nextState = LEVEL_STATE_BUILDING;
477 if (TowerTryAdd(level->placementMode, mapX, mapY))
478 {
479 level->playerGold -= GetTowerCosts(level->placementMode);
480 level->placementMode = TOWER_TYPE_NONE;
481 }
482 }
483 }
484
485 void DrawLevelBuildingState(Level *level)
486 {
487 BeginMode3D(level->camera);
488 DrawLevelGround(level);
489
490 PathFindingMapUpdate(0, 0);
491 TowerDraw();
492 EnemyDraw();
493 ProjectileDraw();
494 ParticleDraw();
495 DrawEnemyPaths(level);
496
497 guiState.isBlocked = 0;
498
499 EndMode3D();
500
501 TowerDrawHealthBars(level->camera);
502
503 DrawBuildingBuildButton(level, 10, 10, 110, 30, TOWER_TYPE_WALL, "Wall");
504 DrawBuildingBuildButton(level, 10, 50, 110, 30, TOWER_TYPE_ARCHER, "Archer");
505 DrawBuildingBuildButton(level, 10, 90, 110, 30, TOWER_TYPE_BALLISTA, "Ballista");
506 DrawBuildingBuildButton(level, 10, 130, 110, 30, TOWER_TYPE_CATAPULT, "Catapult");
507
508 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
509 {
510 level->nextState = LEVEL_STATE_RESET;
511 }
512
513 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0))
514 {
515 level->nextState = LEVEL_STATE_BATTLE;
516 }
517
518 const char *text = "Building phase";
519 int textWidth = MeasureText(text, 20);
520 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
521 }
522
523 void InitBattleStateConditions(Level *level)
524 {
525 level->state = LEVEL_STATE_BATTLE;
526 level->nextState = LEVEL_STATE_NONE;
527 level->waveEndTimer = 0.0f;
528 for (int i = 0; i < 10; i++)
529 {
530 EnemyWave *wave = &level->waves[i];
531 wave->spawned = 0;
532 wave->timeToSpawnNext = wave->delay;
533 }
534 }
535
536 void DrawLevelBattleState(Level *level)
537 {
538 BeginMode3D(level->camera);
539 DrawLevelGround(level);
540 TowerDraw();
541 EnemyDraw();
542 ProjectileDraw();
543 ParticleDraw();
544 guiState.isBlocked = 0;
545 EndMode3D();
546
547 EnemyDrawHealthbars(level->camera);
548 TowerDrawHealthBars(level->camera);
549
550 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
551 {
552 level->nextState = LEVEL_STATE_RESET;
553 }
554
555 int maxCount = 0;
556 int remainingCount = 0;
557 for (int i = 0; i < 10; i++)
558 {
559 EnemyWave *wave = &level->waves[i];
560 if (wave->wave != level->currentWave)
561 {
562 continue;
563 }
564 maxCount += wave->count;
565 remainingCount += wave->count - wave->spawned;
566 }
567 int aliveCount = EnemyCount();
568 remainingCount += aliveCount;
569
570 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount);
571 int textWidth = MeasureText(text, 20);
572 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
573 }
574
575 void DrawLevel(Level *level)
576 {
577 switch (level->state)
578 {
579 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break;
580 case LEVEL_STATE_BUILDING_PLACEMENT: DrawLevelBuildingPlacementState(level); break;
581 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break;
582 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break;
583 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break;
584 default: break;
585 }
586
587 DrawLevelHud(level);
588 }
589
590 void UpdateLevel(Level *level)
591 {
592 if (level->state == LEVEL_STATE_BATTLE)
593 {
594 int activeWaves = 0;
595 for (int i = 0; i < 10; i++)
596 {
597 EnemyWave *wave = &level->waves[i];
598 if (wave->spawned >= wave->count || wave->wave != level->currentWave)
599 {
600 continue;
601 }
602 activeWaves++;
603 wave->timeToSpawnNext -= gameTime.deltaTime;
604 if (wave->timeToSpawnNext <= 0.0f)
605 {
606 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y);
607 if (enemy)
608 {
609 wave->timeToSpawnNext = wave->interval;
610 wave->spawned++;
611 }
612 }
613 }
614 if (GetTowerByType(TOWER_TYPE_BASE) == 0) {
615 level->waveEndTimer += gameTime.deltaTime;
616 if (level->waveEndTimer >= 2.0f)
617 {
618 level->nextState = LEVEL_STATE_LOST_WAVE;
619 }
620 }
621 else if (activeWaves == 0 && EnemyCount() == 0)
622 {
623 level->waveEndTimer += gameTime.deltaTime;
624 if (level->waveEndTimer >= 2.0f)
625 {
626 level->nextState = LEVEL_STATE_WON_WAVE;
627 }
628 }
629 }
630
631 PathFindingMapUpdate(0, 0);
632 EnemyUpdate();
633 TowerUpdate();
634 ProjectileUpdate();
635 ParticleUpdate();
636
637 if (level->nextState == LEVEL_STATE_RESET)
638 {
639 InitLevel(level);
640 }
641
642 if (level->nextState == LEVEL_STATE_BATTLE)
643 {
644 InitBattleStateConditions(level);
645 }
646
647 if (level->nextState == LEVEL_STATE_WON_WAVE)
648 {
649 level->currentWave++;
650 level->state = LEVEL_STATE_WON_WAVE;
651 }
652
653 if (level->nextState == LEVEL_STATE_LOST_WAVE)
654 {
655 level->state = LEVEL_STATE_LOST_WAVE;
656 }
657
658 if (level->nextState == LEVEL_STATE_BUILDING)
659 {
660 level->state = LEVEL_STATE_BUILDING;
661 }
662
663 if (level->nextState == LEVEL_STATE_BUILDING_PLACEMENT)
664 {
665 level->state = LEVEL_STATE_BUILDING_PLACEMENT;
666 level->placementTransitionPosition = (Vector2){
667 level->placementX, level->placementY};
668 // initialize the spring to the current position
669 level->placementTowerSpring = (PhysicsPoint){
670 .position = (Vector3){level->placementX, 1.0f, level->placementY},
671 .velocity = (Vector3){0.0f, 0.0f, 0.0f},
672 };
673 }
674
675 if (level->nextState == LEVEL_STATE_WON_LEVEL)
676 {
677 // make something of this later
678 InitLevel(level);
679 }
680
681 level->nextState = LEVEL_STATE_NONE;
682 }
683
684 float nextSpawnTime = 0.0f;
685
686 void ResetGame()
687 {
688 InitLevel(currentLevel);
689 }
690
691 void InitGame()
692 {
693 TowerInit();
694 EnemyInit();
695 ProjectileInit();
696 ParticleInit();
697 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f);
698
699 currentLevel = levels;
700 InitLevel(currentLevel);
701 }
702
703 //# Immediate GUI functions
704
705 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth)
706 {
707 const float healthBarHeight = 6.0f;
708 const float healthBarOffset = 15.0f;
709 const float inset = 2.0f;
710 const float innerWidth = healthBarWidth - inset * 2;
711 const float innerHeight = healthBarHeight - inset * 2;
712
713 Vector2 screenPos = GetWorldToScreen(position, camera);
714 float centerX = screenPos.x - healthBarWidth * 0.5f;
715 float topY = screenPos.y - healthBarOffset;
716 DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK);
717 float healthWidth = innerWidth * healthRatio;
718 DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor);
719 }
720
721 int Button(const char *text, int x, int y, int width, int height, ButtonState *state)
722 {
723 Rectangle bounds = {x, y, width, height};
724 int isPressed = 0;
725 int isSelected = state && state->isSelected;
726 int isDisabled = state && state->isDisabled;
727 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !isDisabled)
728 {
729 Color color = isSelected ? DARKGRAY : GRAY;
730 DrawRectangle(x, y, width, height, color);
731 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
732 {
733 isPressed = 1;
734 }
735 guiState.isBlocked = 1;
736 }
737 else
738 {
739 Color color = isSelected ? WHITE : LIGHTGRAY;
740 DrawRectangle(x, y, width, height, color);
741 }
742 Font font = GetFontDefault();
743 Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1);
744 Color textColor = isDisabled ? GRAY : BLACK;
745 DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, textColor);
746 return isPressed;
747 }
748
749 //# Main game loop
750
751 void GameUpdate()
752 {
753 float dt = GetFrameTime();
754 // cap maximum delta time to 0.1 seconds to prevent large time steps
755 if (dt > 0.1f) dt = 0.1f;
756 gameTime.time += dt;
757 gameTime.deltaTime = dt;
758
759 UpdateLevel(currentLevel);
760 }
761
762 int main(void)
763 {
764 int screenWidth, screenHeight;
765 GetPreferredSize(&screenWidth, &screenHeight);
766 InitWindow(screenWidth, screenHeight, "Tower defense");
767 int fps = 30;
768 SetTargetFPS(fps);
769
770 LoadAssets();
771 InitGame();
772
773 while (!WindowShouldClose())
774 {
775 if (IsPaused()) {
776 // canvas is not visible in browser - do nothing
777 continue;
778 }
779
780 if (IsKeyPressed(KEY_F))
781 {
782 fps += 5;
783 if (fps > 60) fps = 10;
784 SetTargetFPS(fps);
785 TraceLog(LOG_INFO, "FPS set to %d", fps);
786 }
787
788 BeginDrawing();
789 ClearBackground((Color){0x4E, 0x63, 0x26, 0xFF});
790
791 GameUpdate();
792 DrawLevel(currentLevel);
793
794 DrawText(TextFormat("FPS: %.1f : %d", 1.0f / (float) GetFrameTime(), fps), GetScreenWidth() - 180, 60, 20, WHITE);
795 EndDrawing();
796 }
797
798 CloseWindow();
799
800 return 0;
801 }
1 #ifndef TD_TUT_2_MAIN_H
2 #define TD_TUT_2_MAIN_H
3
4 #include <inttypes.h>
5
6 #include "raylib.h"
7 #include "preferred_size.h"
8
9 //# Declarations
10
11 typedef struct PhysicsPoint
12 {
13 Vector3 position;
14 Vector3 velocity;
15 } PhysicsPoint;
16
17 #define ENEMY_MAX_PATH_COUNT 8
18 #define ENEMY_MAX_COUNT 400
19 #define ENEMY_TYPE_NONE 0
20 #define ENEMY_TYPE_MINION 1
21
22 #define PARTICLE_MAX_COUNT 400
23 #define PARTICLE_TYPE_NONE 0
24 #define PARTICLE_TYPE_EXPLOSION 1
25
26 typedef struct Particle
27 {
28 uint8_t particleType;
29 float spawnTime;
30 float lifetime;
31 Vector3 position;
32 Vector3 velocity;
33 Vector3 scale;
34 } Particle;
35
36 #define TOWER_MAX_COUNT 400
37 enum TowerType
38 {
39 TOWER_TYPE_NONE,
40 TOWER_TYPE_BASE,
41 TOWER_TYPE_ARCHER,
42 TOWER_TYPE_BALLISTA,
43 TOWER_TYPE_CATAPULT,
44 TOWER_TYPE_WALL,
45 TOWER_TYPE_COUNT
46 };
47
48 typedef struct HitEffectConfig
49 {
50 float damage;
51 float areaDamageRadius;
52 float pushbackPowerDistance;
53 } HitEffectConfig;
54
55 typedef struct TowerTypeConfig
56 {
57 float cooldown;
58 float range;
59 float projectileSpeed;
60
61 uint8_t cost;
62 uint8_t projectileType;
63 uint16_t maxHealth;
64
65 HitEffectConfig hitEffect;
66 } TowerTypeConfig;
67
68 typedef struct Tower
69 {
70 int16_t x, y;
71 uint8_t towerType;
72 Vector2 lastTargetPosition;
73 float cooldown;
74 float damage;
75 } Tower;
76
77 typedef struct GameTime
78 {
79 float time;
80 float deltaTime;
81 } GameTime;
82
83 typedef struct ButtonState {
84 char isSelected;
85 char isDisabled;
86 } ButtonState;
87
88 typedef struct GUIState {
89 int isBlocked;
90 } GUIState;
91
92 typedef enum LevelState
93 {
94 LEVEL_STATE_NONE,
95 LEVEL_STATE_BUILDING,
96 LEVEL_STATE_BUILDING_PLACEMENT,
97 LEVEL_STATE_BATTLE,
98 LEVEL_STATE_WON_WAVE,
99 LEVEL_STATE_LOST_WAVE,
100 LEVEL_STATE_WON_LEVEL,
101 LEVEL_STATE_RESET,
102 } LevelState;
103
104 typedef struct EnemyWave {
105 uint8_t enemyType;
106 uint8_t wave;
107 uint16_t count;
108 float interval;
109 float delay;
110 Vector2 spawnPosition;
111
112 uint16_t spawned;
113 float timeToSpawnNext;
114 } EnemyWave;
115
116 #define ENEMY_MAX_WAVE_COUNT 10
117
118 typedef struct Level
119 {
120 int seed;
121 LevelState state;
122 LevelState nextState;
123 Camera3D camera;
124 int placementMode;
125 int16_t placementX;
126 int16_t placementY;
127 Vector2 placementTransitionPosition;
128 PhysicsPoint placementTowerSpring;
129
130 int initialGold;
131 int playerGold;
132
133 EnemyWave waves[ENEMY_MAX_WAVE_COUNT];
134 int currentWave;
135 float waveEndTimer;
136 } Level;
137
138 typedef struct DeltaSrc
139 {
140 char x, y;
141 } DeltaSrc;
142
143 typedef struct PathfindingMap
144 {
145 int width, height;
146 float scale;
147 float *distances;
148 long *towerIndex;
149 DeltaSrc *deltaSrc;
150 float maxDistance;
151 Matrix toMapSpace;
152 Matrix toWorldSpace;
153 } PathfindingMap;
154
155 // when we execute the pathfinding algorithm, we need to store the active nodes
156 // in a queue. Each node has a position, a distance from the start, and the
157 // position of the node that we came from.
158 typedef struct PathfindingNode
159 {
160 int16_t x, y, fromX, fromY;
161 float distance;
162 } PathfindingNode;
163
164 typedef struct EnemyId
165 {
166 uint16_t index;
167 uint16_t generation;
168 } EnemyId;
169
170 typedef struct EnemyClassConfig
171 {
172 float speed;
173 float health;
174 float radius;
175 float maxAcceleration;
176 float requiredContactTime;
177 float explosionDamage;
178 float explosionRange;
179 float explosionPushbackPower;
180 int goldValue;
181 } EnemyClassConfig;
182
183 typedef struct Enemy
184 {
185 int16_t currentX, currentY;
186 int16_t nextX, nextY;
187 Vector2 simPosition;
188 Vector2 simVelocity;
189 uint16_t generation;
190 float walkedDistance;
191 float startMovingTime;
192 float damage, futureDamage;
193 float contactTime;
194 uint8_t enemyType;
195 uint8_t movePathCount;
196 Vector2 movePath[ENEMY_MAX_PATH_COUNT];
197 } Enemy;
198
199 // a unit that uses sprites to be drawn
200 #define SPRITE_UNIT_PHASE_WEAPON_IDLE 0
201 #define SPRITE_UNIT_PHASE_WEAPON_COOLDOWN 1
202 typedef struct SpriteUnit
203 {
204 Rectangle srcRect;
205 Vector2 offset;
206 int frameCount;
207 float frameDuration;
208 Rectangle srcWeaponIdleRect;
209 Vector2 srcWeaponIdleOffset;
210 Rectangle srcWeaponCooldownRect;
211 Vector2 srcWeaponCooldownOffset;
212 } SpriteUnit;
213
214 #define PROJECTILE_MAX_COUNT 1200
215 #define PROJECTILE_TYPE_NONE 0
216 #define PROJECTILE_TYPE_ARROW 1
217 #define PROJECTILE_TYPE_CATAPULT 2
218 #define PROJECTILE_TYPE_BALLISTA 3
219
220 typedef struct Projectile
221 {
222 uint8_t projectileType;
223 float shootTime;
224 float arrivalTime;
225 float distance;
226 Vector3 position;
227 Vector3 target;
228 Vector3 directionNormal;
229 EnemyId targetEnemy;
230 HitEffectConfig hitEffectConfig;
231 } Projectile;
232
233 //# Function declarations
234 float TowerGetMaxHealth(Tower *tower);
235 int Button(const char *text, int x, int y, int width, int height, ButtonState *state);
236 int EnemyAddDamageRange(Vector2 position, float range, float damage);
237 int EnemyAddDamage(Enemy *enemy, float damage);
238
239 //# Enemy functions
240 void EnemyInit();
241 void EnemyDraw();
242 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource);
243 void EnemyUpdate();
244 float EnemyGetCurrentMaxSpeed(Enemy *enemy);
245 float EnemyGetMaxHealth(Enemy *enemy);
246 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY);
247 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount);
248 EnemyId EnemyGetId(Enemy *enemy);
249 Enemy *EnemyTryResolve(EnemyId enemyId);
250 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY);
251 int EnemyAddDamage(Enemy *enemy, float damage);
252 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range);
253 int EnemyCount();
254 void EnemyDrawHealthbars(Camera3D camera);
255
256 //# Tower functions
257 void TowerInit();
258 Tower *TowerGetAt(int16_t x, int16_t y);
259 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y);
260 Tower *GetTowerByType(uint8_t towerType);
261 int GetTowerCosts(uint8_t towerType);
262 float TowerGetMaxHealth(Tower *tower);
263 void TowerDraw();
264 void TowerDrawSingle(Tower tower);
265 void TowerUpdate();
266 void TowerDrawHealthBars(Camera3D camera);
267 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase);
268
269 //# Particles
270 void ParticleInit();
271 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime);
272 void ParticleUpdate();
273 void ParticleDraw();
274
275 //# Projectiles
276 void ProjectileInit();
277 void ProjectileDraw();
278 void ProjectileUpdate();
279 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig);
280
281 //# Pathfinding map
282 void PathfindingMapInit(int width, int height, Vector3 translate, float scale);
283 float PathFindingGetDistance(int mapX, int mapY);
284 Vector2 PathFindingGetGradient(Vector3 world);
285 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY);
286 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells);
287 void PathFindingMapDraw();
288
289 //# UI
290 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth);
291
292 //# Level
293 void DrawLevelGround(Level *level);
294 void DrawEnemyPath(Level *level, Color arrowColor);
295
296 //# variables
297 extern Level *currentLevel;
298 extern Enemy enemies[ENEMY_MAX_COUNT];
299 extern int enemyCount;
300 extern EnemyClassConfig enemyClassConfigs[];
301
302 extern GUIState guiState;
303 extern GameTime gameTime;
304 extern Tower towers[TOWER_MAX_COUNT];
305 extern int towerCount;
306
307 extern Texture2D palette, spriteSheet;
308
309 #endif
1 #include "td_main.h"
2 #include <raymath.h>
3
4 // The queue is a simple array of nodes, we add nodes to the end and remove
5 // nodes from the front. We keep the array around to avoid unnecessary allocations
6 static PathfindingNode *pathfindingNodeQueue = 0;
7 static int pathfindingNodeQueueCount = 0;
8 static int pathfindingNodeQueueCapacity = 0;
9
10 // The pathfinding map stores the distances from the castle to each cell in the map.
11 static PathfindingMap pathfindingMap = {0};
12
13 void PathfindingMapInit(int width, int height, Vector3 translate, float scale)
14 {
15 // transforming between map space and world space allows us to adapt
16 // position and scale of the map without changing the pathfinding data
17 pathfindingMap.toWorldSpace = MatrixTranslate(translate.x, translate.y, translate.z);
18 pathfindingMap.toWorldSpace = MatrixMultiply(pathfindingMap.toWorldSpace, MatrixScale(scale, scale, scale));
19 pathfindingMap.toMapSpace = MatrixInvert(pathfindingMap.toWorldSpace);
20 pathfindingMap.width = width;
21 pathfindingMap.height = height;
22 pathfindingMap.scale = scale;
23 pathfindingMap.distances = (float *)MemAlloc(width * height * sizeof(float));
24 for (int i = 0; i < width * height; i++)
25 {
26 pathfindingMap.distances[i] = -1.0f;
27 }
28
29 pathfindingMap.towerIndex = (long *)MemAlloc(width * height * sizeof(long));
30 pathfindingMap.deltaSrc = (DeltaSrc *)MemAlloc(width * height * sizeof(DeltaSrc));
31 }
32
33 static void PathFindingNodePush(int16_t x, int16_t y, int16_t fromX, int16_t fromY, float distance)
34 {
35 if (pathfindingNodeQueueCount >= pathfindingNodeQueueCapacity)
36 {
37 pathfindingNodeQueueCapacity = pathfindingNodeQueueCapacity == 0 ? 256 : pathfindingNodeQueueCapacity * 2;
38 // we use MemAlloc/MemRealloc to allocate memory for the queue
39 // I am not entirely sure if MemRealloc allows passing a null pointer
40 // so we check if the pointer is null and use MemAlloc in that case
41 if (pathfindingNodeQueue == 0)
42 {
43 pathfindingNodeQueue = (PathfindingNode *)MemAlloc(pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
44 }
45 else
46 {
47 pathfindingNodeQueue = (PathfindingNode *)MemRealloc(pathfindingNodeQueue, pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
48 }
49 }
50
51 PathfindingNode *node = &pathfindingNodeQueue[pathfindingNodeQueueCount++];
52 node->x = x;
53 node->y = y;
54 node->fromX = fromX;
55 node->fromY = fromY;
56 node->distance = distance;
57 }
58
59 static PathfindingNode *PathFindingNodePop()
60 {
61 if (pathfindingNodeQueueCount == 0)
62 {
63 return 0;
64 }
65 // we return the first node in the queue; we want to return a pointer to the node
66 // so we can return 0 if the queue is empty.
67 // We should _not_ return a pointer to the element in the list, because the list
68 // may be reallocated and the pointer would become invalid. Or the
69 // popped element is overwritten by the next push operation.
70 // Using static here means that the variable is permanently allocated.
71 static PathfindingNode node;
72 node = pathfindingNodeQueue[0];
73 // we shift all nodes one position to the front
74 for (int i = 1; i < pathfindingNodeQueueCount; i++)
75 {
76 pathfindingNodeQueue[i - 1] = pathfindingNodeQueue[i];
77 }
78 --pathfindingNodeQueueCount;
79 return &node;
80 }
81
82 float PathFindingGetDistance(int mapX, int mapY)
83 {
84 if (mapX < 0 || mapX >= pathfindingMap.width || mapY < 0 || mapY >= pathfindingMap.height)
85 {
86 // when outside the map, we return the manhattan distance to the castle (0,0)
87 return fabsf((float)mapX) + fabsf((float)mapY);
88 }
89
90 return pathfindingMap.distances[mapY * pathfindingMap.width + mapX];
91 }
92
93 // transform a world position to a map position in the array;
94 // returns true if the position is inside the map
95 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY)
96 {
97 Vector3 mapPosition = Vector3Transform(worldPosition, pathfindingMap.toMapSpace);
98 *mapX = (int16_t)mapPosition.x;
99 *mapY = (int16_t)mapPosition.z;
100 return *mapX >= 0 && *mapX < pathfindingMap.width && *mapY >= 0 && *mapY < pathfindingMap.height;
101 }
102
103 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells)
104 {
105 const int castleX = 0, castleY = 0;
106 int16_t castleMapX, castleMapY;
107 if (!PathFindingFromWorldToMapPosition((Vector3){castleX, 0.0f, castleY}, &castleMapX, &castleMapY))
108 {
109 return;
110 }
111 int width = pathfindingMap.width, height = pathfindingMap.height;
112
113 // reset the distances to -1
114 for (int i = 0; i < width * height; i++)
115 {
116 pathfindingMap.distances[i] = -1.0f;
117 }
118 // reset the tower indices
119 for (int i = 0; i < width * height; i++)
120 {
121 pathfindingMap.towerIndex[i] = -1;
122 }
123 // reset the delta src
124 for (int i = 0; i < width * height; i++)
125 {
126 pathfindingMap.deltaSrc[i].x = 0;
127 pathfindingMap.deltaSrc[i].y = 0;
128 }
129
130 for (int i = 0; i < blockedCellCount; i++)
131 {
132 int16_t mapX, mapY;
133 if (!PathFindingFromWorldToMapPosition((Vector3){blockedCells[i].x, 0.0f, blockedCells[i].y}, &mapX, &mapY))
134 {
135 continue;
136 }
137 int index = mapY * width + mapX;
138 pathfindingMap.towerIndex[index] = -2;
139 }
140
141 for (int i = 0; i < towerCount; i++)
142 {
143 Tower *tower = &towers[i];
144 if (tower->towerType == TOWER_TYPE_NONE || tower->towerType == TOWER_TYPE_BASE)
145 {
146 continue;
147 }
148 int16_t mapX, mapY;
149 // technically, if the tower cell scale is not in sync with the pathfinding map scale,
150 // this would not work correctly and needs to be refined to allow towers covering multiple cells
151 // or having multiple towers in one cell; for simplicity, we assume that the tower covers exactly
152 // one cell. For now.
153 if (!PathFindingFromWorldToMapPosition((Vector3){tower->x, 0.0f, tower->y}, &mapX, &mapY))
154 {
155 continue;
156 }
157 int index = mapY * width + mapX;
158 pathfindingMap.towerIndex[index] = i;
159 }
160
161 // we start at the castle and add the castle to the queue
162 pathfindingMap.maxDistance = 0.0f;
163 pathfindingNodeQueueCount = 0;
164 PathFindingNodePush(castleMapX, castleMapY, castleMapX, castleMapY, 0.0f);
165 PathfindingNode *node = 0;
166 while ((node = PathFindingNodePop()))
167 {
168 if (node->x < 0 || node->x >= width || node->y < 0 || node->y >= height)
169 {
170 continue;
171 }
172 int index = node->y * width + node->x;
173 if (pathfindingMap.distances[index] >= 0 && pathfindingMap.distances[index] <= node->distance)
174 {
175 continue;
176 }
177
178 int deltaX = node->x - node->fromX;
179 int deltaY = node->y - node->fromY;
180 // even if the cell is blocked by a tower, we still may want to store the direction
181 // (though this might not be needed, IDK right now)
182 pathfindingMap.deltaSrc[index].x = (char) deltaX;
183 pathfindingMap.deltaSrc[index].y = (char) deltaY;
184
185 // we skip nodes that are blocked by towers or by the provided blocked cells
186 if (pathfindingMap.towerIndex[index] != -1)
187 {
188 node->distance += 8.0f;
189 }
190 pathfindingMap.distances[index] = node->distance;
191 pathfindingMap.maxDistance = fmaxf(pathfindingMap.maxDistance, node->distance);
192 PathFindingNodePush(node->x, node->y + 1, node->x, node->y, node->distance + 1.0f);
193 PathFindingNodePush(node->x, node->y - 1, node->x, node->y, node->distance + 1.0f);
194 PathFindingNodePush(node->x + 1, node->y, node->x, node->y, node->distance + 1.0f);
195 PathFindingNodePush(node->x - 1, node->y, node->x, node->y, node->distance + 1.0f);
196 }
197 }
198
199 void PathFindingMapDraw()
200 {
201 float cellSize = pathfindingMap.scale * 0.9f;
202 float highlightDistance = fmodf(GetTime() * 4.0f, pathfindingMap.maxDistance);
203 for (int x = 0; x < pathfindingMap.width; x++)
204 {
205 for (int y = 0; y < pathfindingMap.height; y++)
206 {
207 float distance = pathfindingMap.distances[y * pathfindingMap.width + x];
208 float colorV = distance < 0 ? 0 : fminf(distance / pathfindingMap.maxDistance, 1.0f);
209 Color color = distance < 0 ? BLUE : (Color){fminf(colorV, 1.0f) * 255, 0, 0, 255};
210 Vector3 position = Vector3Transform((Vector3){x, -0.25f, y}, pathfindingMap.toWorldSpace);
211 // animate the distance "wave" to show how the pathfinding algorithm expands
212 // from the castle
213 if (distance + 0.5f > highlightDistance && distance - 0.5f < highlightDistance)
214 {
215 color = BLACK;
216 }
217 DrawCube(position, cellSize, 0.1f, cellSize, color);
218 }
219 }
220 }
221
222 Vector2 PathFindingGetGradient(Vector3 world)
223 {
224 int16_t mapX, mapY;
225 if (PathFindingFromWorldToMapPosition(world, &mapX, &mapY))
226 {
227 DeltaSrc delta = pathfindingMap.deltaSrc[mapY * pathfindingMap.width + mapX];
228 return (Vector2){(float)-delta.x, (float)-delta.y};
229 }
230 // fallback to a simple gradient calculation
231 float n = PathFindingGetDistance(mapX, mapY - 1);
232 float s = PathFindingGetDistance(mapX, mapY + 1);
233 float w = PathFindingGetDistance(mapX - 1, mapY);
234 float e = PathFindingGetDistance(mapX + 1, mapY);
235 return (Vector2){w - e + 0.25f, n - s + 0.125f};
236 }
1 #include "td_main.h"
2 #include <raymath.h>
3
4 static TowerTypeConfig towerTypeConfigs[TOWER_TYPE_COUNT] = {
5 [TOWER_TYPE_BASE] = {
6 .maxHealth = 10,
7 },
8 [TOWER_TYPE_ARCHER] = {
9 .cooldown = 0.5f,
10 .range = 3.0f,
11 .cost = 6,
12 .maxHealth = 10,
13 .projectileSpeed = 4.0f,
14 .projectileType = PROJECTILE_TYPE_ARROW,
15 .hitEffect = {
16 .damage = 3.0f,
17 }
18 },
19 [TOWER_TYPE_BALLISTA] = {
20 .cooldown = 1.5f,
21 .range = 6.0f,
22 .cost = 9,
23 .maxHealth = 10,
24 .projectileSpeed = 10.0f,
25 .projectileType = PROJECTILE_TYPE_BALLISTA,
26 .hitEffect = {
27 .damage = 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 TowerDrawSingle(Tower tower)
239 {
240 if (tower.towerType == TOWER_TYPE_NONE)
241 {
242 return;
243 }
244
245 switch (tower.towerType)
246 {
247 case TOWER_TYPE_ARCHER:
248 {
249 Vector2 screenPosTower = GetWorldToScreen((Vector3){tower.x, 0.0f, tower.y}, currentLevel->camera);
250 Vector2 screenPosTarget = GetWorldToScreen((Vector3){tower.lastTargetPosition.x, 0.0f, tower.lastTargetPosition.y}, currentLevel->camera);
251 DrawModel(towerModels[TOWER_TYPE_WALL], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
252 DrawSpriteUnit(archerUnit, (Vector3){tower.x, 1.0f, tower.y}, 0, screenPosTarget.x > screenPosTower.x,
253 tower.cooldown > 0.2f ? SPRITE_UNIT_PHASE_WEAPON_COOLDOWN : SPRITE_UNIT_PHASE_WEAPON_IDLE);
254 }
255 break;
256 case TOWER_TYPE_BALLISTA:
257 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, BROWN);
258 break;
259 case TOWER_TYPE_CATAPULT:
260 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, DARKGRAY);
261 break;
262 default:
263 if (towerModels[tower.towerType].materials)
264 {
265 DrawModel(towerModels[tower.towerType], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
266 } else {
267 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY);
268 }
269 break;
270 }
271 }
272
273 void TowerDraw()
274 {
275 for (int i = 0; i < towerCount; i++)
276 {
277 TowerDrawSingle(towers[i]);
278 }
279 }
280
281 void TowerUpdate()
282 {
283 for (int i = 0; i < towerCount; i++)
284 {
285 Tower *tower = &towers[i];
286 switch (tower->towerType)
287 {
288 case TOWER_TYPE_CATAPULT:
289 case TOWER_TYPE_BALLISTA:
290 case TOWER_TYPE_ARCHER:
291 TowerGunUpdate(tower);
292 break;
293 }
294 }
295 }
296
297 void TowerDrawHealthBars(Camera3D camera)
298 {
299 for (int i = 0; i < towerCount; i++)
300 {
301 Tower *tower = &towers[i];
302 if (tower->towerType == TOWER_TYPE_NONE || tower->damage <= 0.0f)
303 {
304 continue;
305 }
306
307 Vector3 position = (Vector3){tower->x, 0.5f, tower->y};
308 float maxHealth = TowerGetMaxHealth(tower);
309 float health = maxHealth - tower->damage;
310 float healthRatio = health / maxHealth;
311
312 DrawHealthBar(camera, position, healthRatio, GREEN, 35.0f);
313 }
314 }
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
Even though it is only a dozen lines or so, the code is quite complex, so let's add some comments to explain what is going on:
1 // current position of the base
2 Vector3 worldPlacementPosition = (Vector3){
3 level->placementTransitionPosition.x, 0.0f, level->placementTransitionPosition.y};
4 // target position of the spring tip if at rest
5 Vector3 springTargetPosition = (Vector3){
6 worldPlacementPosition.x, 1.0f, worldPlacementPosition.z};
7
8 // difference from last position to the new target position
9 Vector3 springPointDelta = Vector3Subtract(springTargetPosition, level->placementTowerSpring.position);
10 // velocity change we want to use to bring the spring tip closer to the rest position
11 Vector3 velocityChange = Vector3Scale(springPointDelta, dt * 200.0f);
12
13 // decay existing velocity, each frame
14 level->placementTowerSpring.velocity = Vector3Scale(level->placementTowerSpring.velocity, 1.0f - expf(-30.0f * dt));
15 // add the velocity change to the current velocity
16 level->placementTowerSpring.velocity = Vector3Add(level->placementTowerSpring.velocity, velocityChange);
17 // update the spring tip position using the velocity
18 level->placementTowerSpring.position = Vector3Add(level->placementTowerSpring.position,
19 Vector3Scale(level->placementTowerSpring.velocity, dt));
The used operations are just basic vector maths that uses addition, subtraction and multiplication. The complexity arises from the fact that this is executed each frame and that we have to tweak the values to get the desired effect.
The result of these calculations leads to the spring tip wobbling around when the tower is moved:

While it looks OK at 30 FPS, at lower frame rates, it becomes overly wobbly:

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

Conclusion
Using a few lines of code and math formulas inspired by physics, we have a wobbling abstraction of a spring that we will use in the next part to orient and scale the tower model. This is also going to involve some more math, but it should be a little more simple since it is going to be a quite direct application of formulas that are taught in high school geometry.
I hope you liked what you saw in this post, so you will join me next Friday for the next part, since I am sure you will like it to toy around with the final version of this gimmick!