Simple tower defense tutorial, part 15: Cartoonish tower movement (2/2)
The next problem to solve is how to draw the tower so that its top points to the tip of the spring (the red dot) and its size gets squished and squashed according to the spring's tension.
What we need is a look-at function that rotates the tower model in a way that the up direction points to the tip of the spring.
A naive approach could be to calculate the x and z rotation angles based on the difference between the tower position and the spring tip position.
Let's see how this looks like, though I believe, that there is a better solution. To compare the alignment of the orientation in comparison to the spring tip, we will draw a wired cube that represents the tower model.
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) * 300.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 // calculate x and z rotation to align the model with the spring
462 Vector3 position = {level->placementTransitionPosition.x, 0.0f, level->placementTransitionPosition.y};
463 Vector3 up = Vector3Subtract(level->placementTowerSpring.position, position);
464 float angleX = -acosf(up.z / Vector3Length(up)) * RAD2DEG + 90.0f;
465 float angleZ = acosf(up.x / Vector3Length(up)) * RAD2DEG - 90.0f;
466 rlPushMatrix();
467 rlRotatef(angleX, 1, 0, 0);
468 rlRotatef(angleZ, 0, 0, 1);
469 DrawCubeWires((Vector3){0.0f, 0.5f, 0.0f}, 0.2f, 1.0f, 0.2f, RED);
470 rlPopMatrix();
471
472 // deactivated for now to debug the spring simulation
473 // Tower dummy = {
474 // .towerType = level->placementMode,
475 // };
476 // TowerDrawSingle(dummy);
477
478 float bounce = sinf(GetTime() * 8.0f) * 0.5f + 0.5f;
479 float offset = fmaxf(0.0f, bounce - 0.4f) * 0.35f + 0.7f;
480 float squeeze = -fminf(0.0f, bounce - 0.4f) * 0.7f + 0.7f;
481 float stretch = fmaxf(0.0f, bounce - 0.3f) * 0.5f + 0.8f;
482
483 DrawModelEx(greenArrowModel, (Vector3){ offset, 0.0f, 0.0f}, (Vector3){0, 1, 0}, 90, (Vector3){squeeze, 1.0f, stretch}, WHITE);
484 DrawModelEx(greenArrowModel, (Vector3){-offset, 0.0f, 0.0f}, (Vector3){0, 1, 0}, 270, (Vector3){squeeze, 1.0f, stretch}, WHITE);
485 DrawModelEx(greenArrowModel, (Vector3){ 0.0f, 0.0f, offset}, (Vector3){0, 1, 0}, 0, (Vector3){squeeze, 1.0f, stretch}, WHITE);
486 DrawModelEx(greenArrowModel, (Vector3){ 0.0f, 0.0f, -offset}, (Vector3){0, 1, 0}, 180, (Vector3){squeeze, 1.0f, stretch}, WHITE);
487 rlPopMatrix();
488
489 guiState.isBlocked = 0;
490
491 EndMode3D();
492
493 TowerDrawHealthBars(level->camera);
494
495 if (Button("Cancel", 20, GetScreenHeight() - 40, 160, 30, 0))
496 {
497 level->nextState = LEVEL_STATE_BUILDING;
498 level->placementMode = TOWER_TYPE_NONE;
499 TraceLog(LOG_INFO, "Cancel building");
500 }
501
502 if (Button("Build", GetScreenWidth() - 180, GetScreenHeight() - 40, 160, 30, 0))
503 {
504 level->nextState = LEVEL_STATE_BUILDING;
505 if (TowerTryAdd(level->placementMode, mapX, mapY))
506 {
507 level->playerGold -= GetTowerCosts(level->placementMode);
508 level->placementMode = TOWER_TYPE_NONE;
509 }
510 }
511 }
512
513 void DrawLevelBuildingState(Level *level)
514 {
515 BeginMode3D(level->camera);
516 DrawLevelGround(level);
517
518 PathFindingMapUpdate(0, 0);
519 TowerDraw();
520 EnemyDraw();
521 ProjectileDraw();
522 ParticleDraw();
523 DrawEnemyPaths(level);
524
525 guiState.isBlocked = 0;
526
527 EndMode3D();
528
529 TowerDrawHealthBars(level->camera);
530
531 DrawBuildingBuildButton(level, 10, 10, 110, 30, TOWER_TYPE_WALL, "Wall");
532 DrawBuildingBuildButton(level, 10, 50, 110, 30, TOWER_TYPE_ARCHER, "Archer");
533 DrawBuildingBuildButton(level, 10, 90, 110, 30, TOWER_TYPE_BALLISTA, "Ballista");
534 DrawBuildingBuildButton(level, 10, 130, 110, 30, TOWER_TYPE_CATAPULT, "Catapult");
535
536 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
537 {
538 level->nextState = LEVEL_STATE_RESET;
539 }
540
541 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0))
542 {
543 level->nextState = LEVEL_STATE_BATTLE;
544 }
545
546 const char *text = "Building phase";
547 int textWidth = MeasureText(text, 20);
548 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
549 }
550
551 void InitBattleStateConditions(Level *level)
552 {
553 level->state = LEVEL_STATE_BATTLE;
554 level->nextState = LEVEL_STATE_NONE;
555 level->waveEndTimer = 0.0f;
556 for (int i = 0; i < 10; i++)
557 {
558 EnemyWave *wave = &level->waves[i];
559 wave->spawned = 0;
560 wave->timeToSpawnNext = wave->delay;
561 }
562 }
563
564 void DrawLevelBattleState(Level *level)
565 {
566 BeginMode3D(level->camera);
567 DrawLevelGround(level);
568 TowerDraw();
569 EnemyDraw();
570 ProjectileDraw();
571 ParticleDraw();
572 guiState.isBlocked = 0;
573 EndMode3D();
574
575 EnemyDrawHealthbars(level->camera);
576 TowerDrawHealthBars(level->camera);
577
578 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
579 {
580 level->nextState = LEVEL_STATE_RESET;
581 }
582
583 int maxCount = 0;
584 int remainingCount = 0;
585 for (int i = 0; i < 10; i++)
586 {
587 EnemyWave *wave = &level->waves[i];
588 if (wave->wave != level->currentWave)
589 {
590 continue;
591 }
592 maxCount += wave->count;
593 remainingCount += wave->count - wave->spawned;
594 }
595 int aliveCount = EnemyCount();
596 remainingCount += aliveCount;
597
598 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount);
599 int textWidth = MeasureText(text, 20);
600 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
601 }
602
603 void DrawLevel(Level *level)
604 {
605 switch (level->state)
606 {
607 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break;
608 case LEVEL_STATE_BUILDING_PLACEMENT: DrawLevelBuildingPlacementState(level); break;
609 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break;
610 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break;
611 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break;
612 default: break;
613 }
614
615 DrawLevelHud(level);
616 }
617
618 void UpdateLevel(Level *level)
619 {
620 if (level->state == LEVEL_STATE_BATTLE)
621 {
622 int activeWaves = 0;
623 for (int i = 0; i < 10; i++)
624 {
625 EnemyWave *wave = &level->waves[i];
626 if (wave->spawned >= wave->count || wave->wave != level->currentWave)
627 {
628 continue;
629 }
630 activeWaves++;
631 wave->timeToSpawnNext -= gameTime.deltaTime;
632 if (wave->timeToSpawnNext <= 0.0f)
633 {
634 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y);
635 if (enemy)
636 {
637 wave->timeToSpawnNext = wave->interval;
638 wave->spawned++;
639 }
640 }
641 }
642 if (GetTowerByType(TOWER_TYPE_BASE) == 0) {
643 level->waveEndTimer += gameTime.deltaTime;
644 if (level->waveEndTimer >= 2.0f)
645 {
646 level->nextState = LEVEL_STATE_LOST_WAVE;
647 }
648 }
649 else if (activeWaves == 0 && EnemyCount() == 0)
650 {
651 level->waveEndTimer += gameTime.deltaTime;
652 if (level->waveEndTimer >= 2.0f)
653 {
654 level->nextState = LEVEL_STATE_WON_WAVE;
655 }
656 }
657 }
658
659 PathFindingMapUpdate(0, 0);
660 EnemyUpdate();
661 TowerUpdate();
662 ProjectileUpdate();
663 ParticleUpdate();
664
665 if (level->nextState == LEVEL_STATE_RESET)
666 {
667 InitLevel(level);
668 }
669
670 if (level->nextState == LEVEL_STATE_BATTLE)
671 {
672 InitBattleStateConditions(level);
673 }
674
675 if (level->nextState == LEVEL_STATE_WON_WAVE)
676 {
677 level->currentWave++;
678 level->state = LEVEL_STATE_WON_WAVE;
679 }
680
681 if (level->nextState == LEVEL_STATE_LOST_WAVE)
682 {
683 level->state = LEVEL_STATE_LOST_WAVE;
684 }
685
686 if (level->nextState == LEVEL_STATE_BUILDING)
687 {
688 level->state = LEVEL_STATE_BUILDING;
689 }
690
691 if (level->nextState == LEVEL_STATE_BUILDING_PLACEMENT)
692 {
693 level->state = LEVEL_STATE_BUILDING_PLACEMENT;
694 level->placementTransitionPosition = (Vector2){
695 level->placementX, level->placementY};
696 // initialize the spring to the current position
697 level->placementTowerSpring = (PhysicsPoint){
698 .position = (Vector3){level->placementX, 1.0f, level->placementY},
699 .velocity = (Vector3){0.0f, 0.0f, 0.0f},
700 };
701 }
702
703 if (level->nextState == LEVEL_STATE_WON_LEVEL)
704 {
705 // make something of this later
706 InitLevel(level);
707 }
708
709 level->nextState = LEVEL_STATE_NONE;
710 }
711
712 float nextSpawnTime = 0.0f;
713
714 void ResetGame()
715 {
716 InitLevel(currentLevel);
717 }
718
719 void InitGame()
720 {
721 TowerInit();
722 EnemyInit();
723 ProjectileInit();
724 ParticleInit();
725 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f);
726
727 currentLevel = levels;
728 InitLevel(currentLevel);
729 }
730
731 //# Immediate GUI functions
732
733 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth)
734 {
735 const float healthBarHeight = 6.0f;
736 const float healthBarOffset = 15.0f;
737 const float inset = 2.0f;
738 const float innerWidth = healthBarWidth - inset * 2;
739 const float innerHeight = healthBarHeight - inset * 2;
740
741 Vector2 screenPos = GetWorldToScreen(position, camera);
742 float centerX = screenPos.x - healthBarWidth * 0.5f;
743 float topY = screenPos.y - healthBarOffset;
744 DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK);
745 float healthWidth = innerWidth * healthRatio;
746 DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor);
747 }
748
749 int Button(const char *text, int x, int y, int width, int height, ButtonState *state)
750 {
751 Rectangle bounds = {x, y, width, height};
752 int isPressed = 0;
753 int isSelected = state && state->isSelected;
754 int isDisabled = state && state->isDisabled;
755 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !isDisabled)
756 {
757 Color color = isSelected ? DARKGRAY : GRAY;
758 DrawRectangle(x, y, width, height, color);
759 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
760 {
761 isPressed = 1;
762 }
763 guiState.isBlocked = 1;
764 }
765 else
766 {
767 Color color = isSelected ? WHITE : LIGHTGRAY;
768 DrawRectangle(x, y, width, height, color);
769 }
770 Font font = GetFontDefault();
771 Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1);
772 Color textColor = isDisabled ? GRAY : BLACK;
773 DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, textColor);
774 return isPressed;
775 }
776
777 //# Main game loop
778
779 void GameUpdate()
780 {
781 UpdateLevel(currentLevel);
782 }
783
784 int main(void)
785 {
786 int screenWidth, screenHeight;
787 GetPreferredSize(&screenWidth, &screenHeight);
788 InitWindow(screenWidth, screenHeight, "Tower defense");
789 float gamespeed = 1.0f;
790 SetTargetFPS(30);
791
792 LoadAssets();
793 InitGame();
794
795 float pause = 1.0f;
796
797 while (!WindowShouldClose())
798 {
799 if (IsPaused()) {
800 // canvas is not visible in browser - do nothing
801 continue;
802 }
803
804 if (IsKeyPressed(KEY_T))
805 {
806 gamespeed += 0.1f;
807 if (gamespeed > 1.0f) gamespeed = 0.1f;
808 }
809
810 if (IsKeyPressed(KEY_P))
811 {
812 pause = pause > 0.5f ? 0.0f : 1.0f;
813 }
814
815 float dt = GetFrameTime() * gamespeed * pause;
816 // cap maximum delta time to 0.1 seconds to prevent large time steps
817 if (dt > 0.1f) dt = 0.1f;
818 gameTime.time += dt;
819 gameTime.deltaTime = dt;
820 gameTime.frameCount += 1;
821
822 float fixedTimeDiff = gameTime.time - gameTime.fixedTimeStart;
823 gameTime.fixedStepCount = (uint8_t)(fixedTimeDiff / gameTime.fixedDeltaTime);
824
825 BeginDrawing();
826 ClearBackground((Color){0x4E, 0x63, 0x26, 0xFF});
827
828 GameUpdate();
829 DrawLevel(currentLevel);
830
831 DrawText(TextFormat("Speed: %.1f", gamespeed), GetScreenWidth() - 180, 60, 20, WHITE);
832 EndDrawing();
833
834 gameTime.fixedTimeStart += gameTime.fixedStepCount * gameTime.fixedDeltaTime;
835 }
836
837 CloseWindow();
838
839 return 0;
840 }
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 solution works well when moving the on a single axis, but when moving the tower diagonally, the red wire cube is not aligned anymore - you can test this by slowing the game time scale down by pressing T and pause the game by pressing P.

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

What is really nice now is that the spawn and placement animation utilizes the same spring simulation that we use for the tower movement. The simulation parameters are a little adjusted to make the initial jump and the fall look a little squishier than the regular tower movement, but apart from that, it is the same simulation. For the starting phase, the spring is initialized at height and the parameters are changed:
1 float damping = 0.5f;
2 float springStiffness = 300.0f;
3 float springDecay = 95.0f;
4 float minHeight = 0.35f;
5
6 if (level->placementPhase == PLACEMENT_PHASE_STARTING)
7 {
8 damping = 1.0f;
9 springDecay = 90.0f;
10 springStiffness = 100.0f;
11 minHeight = 0.70f;
12 }
When placing the tower, the floating height of the tower is also adjusted to make it look as if the tower is smashed down:
1 // let the tower float up and down. Consider this height in the spring simulation as well
2 float towerFloatHeight = sinf(gameTime.time * 4.0f) * 0.2f + 0.3f;
3
4 if (level->placementPhase == PLACEMENT_PHASE_PLACING)
5 {
6 // The bouncing spring needs a bit of outro time to look nice and complete.
7 // So we scale the time so that the first 2/3rd of the placing phase handles the motion
8 // and the last 1/3rd is the outro physics (bouncing)
9 float t = fminf(1.0f, level->placementTimer / placementDuration * 1.5f);
10 // let towerFloatHeight describe parabola curve, starting at towerFloatHeight and ending at 0
11 float linearBlendHeight = (1.0f - t) * towerFloatHeight;
12 float parabola = (1.0f - ((t - 0.5f) * (t - 0.5f) * 4.0f)) * 2.0f;
13 towerFloatHeight = linearBlendHeight + parabola;
14 }
Moving and placing the tower feels now pretty satisfying. Though ... this could be juiced up even more! A little screenshake when placing the tower would be nice, as well as some dust particles? Let's do this - at some point in the future 😅.
Conclusion
In this part, we applied the deformation to the tower model using vector math. In the end it is nothing more than a combination of rotating and scaling the model.
Cross and dot products are the among the most basic yet highly useful functions in my experience when it comes to 3D math and I hope that the visualization helped to understand how these functions can be used in practice.
In the next part, we will add a few more different enemy types to the game.